From 1ce654494504dedde11b0bbe7ae1493a9db45c4d Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 25 May 2025 19:00:28 -0600 Subject: [PATCH 1/6] Wrap comment at 80 cols --- src/terminal/point.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/terminal/point.zig b/src/terminal/point.zig index 12b71014b..f2544f90c 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -3,10 +3,12 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const size = @import("size.zig"); -/// The possible reference locations for a point. When someone says "(42, 80)" in the context of a terminal, that could mean multiple -/// things: it is in the current visible viewport? the current active -/// area of the screen where the cursor is? the entire scrollback history? -/// etc. This tag is used to differentiate those cases. +/// The possible reference locations for a point. When someone says "(42, 80)" +/// in the context of a terminal, that could mean multiple things: it is in the +/// current visible viewport? the current active area of the screen where the +/// cursor is? the entire scrollback history? etc. +/// +/// This tag is used to differentiate those cases. pub const Tag = enum { /// Top-left is part of the active area where a running program can /// jump the cursor and make changes. The active area is the "editable" From 58592d3f65aff9d055ce264fac5e50776233aa52 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 25 May 2025 19:01:39 -0600 Subject: [PATCH 2/6] GTK: Don't clamp cursorpos, allow negative values Other apprts don't do this, so this should be consistent. --- src/apprt/gtk/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 30a3d28f7..1ee00ff1b 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1563,7 +1563,7 @@ fn gtkMouseMotion( const scaled = self.scaledCoordinates(x, y); const pos: apprt.CursorPos = .{ - .x = @floatCast(@max(0, scaled.x)), + .x = @floatCast(scaled.x), .y = @floatCast(scaled.y), }; From ecdac8c8c1993b9daaf27745a8ed5583e4ac57d1 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 25 May 2025 19:24:29 -0600 Subject: [PATCH 3/6] terminal: rework selection logic in core surface This logic is cleaner and produces better behavior when selecting by dragging the mouse outside the bounds of the surface, previously when doing this on the left side of the surface selections would include the first cell of the next row, this is no longer the case. This introduces methods on PageList.Pin which move a pin left or right while wrapping to the prev/next row, or clamping to the ends of the row. These need unit tests. --- src/Surface.zig | 274 +++++++++++++++++++------------------- src/terminal/PageList.zig | 68 ++++++++++ 2 files changed, 205 insertions(+), 137 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 32f7487d3..3726c37c7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3678,163 +3678,163 @@ fn dragLeftClickSingle( drag_pin: terminal.Pin, xpos: f64, ) !void { - // NOTE(mitchellh): This logic super sucks. There has to be an easier way - // to calculate this, but this is good for a v1. Selection isn't THAT - // common so its not like this performance heavy code is running that - // often. - // TODO: unit test this, this logic sucks + // TODO: Unit tests for this logic, maybe extract it out to a pure + // function so that it can be tested without mocking state. - // If we were selecting, and we switched directions, then we restart - // calculations because it forces us to reconsider if the first cell is - // selected. - self.checkResetSelSwitch(drag_pin); - - // Our logic for determining if the starting cell is selected: + // Explanation: // - // - The "xboundary" is 60% the width of a cell from the left. We choose - // 60% somewhat arbitrarily based on feeling. - // - If we started our click left of xboundary, backwards selections - // can NEVER select the current char. - // - If we started our click right of xboundary, backwards selections - // ALWAYS selected the current char, but we must move the cursor - // left of the xboundary. - // - Inverted logic for forwards selections. + // # Normal selections // + // ## Left-to-right selections + // - The clicked cell is included if it was clicked to the left of its + // threshold point and the drag location is right of the threshold point. + // - The cell under the cursor (the "drag cell") is included if the drag + // location is right of its threshold point. + // + // ## Right-to-left selections + // - The clicked cell is included if it was clicked to the right of its + // threshold point and the drag location is left of the threshold point. + // - The cell under the cursor (the "drag cell") is included if the drag + // location is left of its threshold point. + // + // # Rectangular selections + // + // Rectangular selections are handled similarly, except that + // entire columns are considered rather than individual cells. - // Our clicking point const click_pin = self.mouse.left_click_pin.?.*; - // the boundary point at which we consider selection or non-selection - const cell_width_f64: f64 = @floatFromInt(self.size.cell.width); - const cell_xboundary = cell_width_f64 * 0.6; + // We only include cells in the selection if the threshold point lies + // between the start and end points of the selection. A threshold of + // 60% of the cell width was chosen empirically because it felt good. + const threshold_point: u32 = @intFromFloat( + @as(f64, @floatFromInt(self.size.cell.width)) * 0.6, + ); - // first xpos of the clicked cell adjusted for padding - const left_padding_f64: f64 = @as(f64, @floatFromInt(self.size.padding.left)); - const cell_xstart = @as(f64, @floatFromInt(click_pin.x)) * cell_width_f64; - const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart - left_padding_f64; + // We use this to clamp the pixel positions below. + const max_x = self.size.grid().columns * self.size.cell.width - 1; - // If this is the same cell, then we only start the selection if weve - // moved past the boundary point the opposite direction from where we - // started. - if (click_pin.eql(drag_pin)) { - // Ensuring to adjusting the cursor position for padding - const cell_xpos = xpos - cell_xstart - left_padding_f64; - const selected: bool = if (cell_start_xpos < cell_xboundary) - cell_xpos >= cell_xboundary - else - cell_xpos < cell_xboundary; + // We need to know how far across in the cell the drag pos is, so + // we subtract the padding and then take it modulo the cell width. + const drag_x = @min( + max_x, + @as(u32, @intFromFloat(@max(0.0, xpos))) -| self.size.padding.left, + ); + const drag_x_frac = drag_x % self.size.cell.width; - try self.setSelection(if (selected) terminal.Selection.init( - drag_pin, - drag_pin, - SurfaceMouse.isRectangleSelectState(self.mouse.mods), - ) else null); + // We figure out the fractional part of the click x position similarly. + // + // NOTE: This click_x position may be incorrect for the current location + // of the click pin, since it's a tracked pin that can move, so we + // should only use this for the fractional position not absolute. + const click_x = @min( + max_x, + @as(u32, @intFromFloat(@max(0.0, self.mouse.left_click_xpos))) -| + self.size.padding.left, + ); + const click_x_frac = click_x % self.size.cell.width; - return; - } + // Whether or not this is a rectangular selection. + const rectangle_selection = + SurfaceMouse.isRectangleSelectState(self.mouse.mods); - // If this is a different cell and we haven't started selection, - // we determine the starting cell first. - if (self.io.terminal.screen.selection == null) { - // - If we're moving to a point before the start, then we select - // the starting cell if we started after the boundary, else - // we start selection of the prior cell. - // - Inverse logic for a point after the start. - const start: terminal.Pin = if (dragLeftClickBefore( - drag_pin, - click_pin, - self.mouse.mods, - )) start: { - if (cell_start_xpos >= cell_xboundary) break :start click_pin; - if (click_pin.x > 0) break :start click_pin.left(1); - var start = click_pin.up(1) orelse click_pin; - start.x = self.io.terminal.screen.pages.cols - 1; - break :start start; - } else start: { - if (cell_start_xpos < cell_xboundary) break :start click_pin; - if (click_pin.x < self.io.terminal.screen.pages.cols - 1) - break :start click_pin.right(1); - var start = click_pin.down(1) orelse click_pin; - start.x = 0; - break :start start; - }; + // Whether the click pin and drag pin are equal. + const same_pin = drag_pin.eql(click_pin); - try self.setSelection(terminal.Selection.init( - start, - drag_pin, - SurfaceMouse.isRectangleSelectState(self.mouse.mods), - )); - return; - } + // Whether or not the end point of our selection is before the start point. + const end_before_start = ebs: { + if (same_pin) { + break :ebs drag_x_frac < click_x_frac; + } - // TODO: detect if selection point is passed the point where we've - // actually written data before and disallow it. - - // We moved! Set the selection end point. The start point should be - // set earlier. - assert(self.io.terminal.screen.selection != null); - const sel = self.io.terminal.screen.selection.?; - try self.setSelection(terminal.Selection.init( - sel.start(), - drag_pin, - sel.rectangle, - )); -} - -// Resets the selection if we switched directions, depending on the select -// mode. See dragLeftClickSingle for more details. -fn checkResetSelSwitch( - self: *Surface, - drag_pin: terminal.Pin, -) void { - const screen = &self.io.terminal.screen; - const sel = screen.selection orelse return; - const sel_start = sel.start(); - const sel_end = sel.end(); - - var reset: bool = false; - if (sel.rectangle) { - // When we're in rectangle mode, we reset the selection relative to - // the click point depending on the selection mode we're in, with - // the exception of single-column selections, which we always reset - // on if we drift. - if (sel_start.x == sel_end.x) { - reset = drag_pin.x != sel_start.x; - } else { - reset = switch (sel.order(screen)) { - .forward => drag_pin.x < sel_start.x or drag_pin.before(sel_start), - .reverse => drag_pin.x > sel_start.x or sel_start.before(drag_pin), - .mirrored_forward => drag_pin.x > sel_start.x or drag_pin.before(sel_start), - .mirrored_reverse => drag_pin.x < sel_start.x or sel_start.before(drag_pin), + // Special handling for rectangular selections, we only use x position. + if (rectangle_selection) { + break :ebs switch (std.math.order(drag_pin.x, click_pin.x)) { + .eq => drag_x_frac < click_x_frac, + .lt => true, + .gt => false, }; } - } else { - // Normal select uses simpler logic that is just based on the - // selection start/end. - reset = if (sel_end.before(sel_start)) - sel_start.before(drag_pin) + + break :ebs drag_pin.before(click_pin); + }; + + // Whether or not the the click pin cell + // should be included in the selection. + const include_click_cell = if (end_before_start) + click_x_frac >= threshold_point + else + click_x_frac < threshold_point; + + // Whether or not the the drag pin cell + // should be included in the selection. + const include_drag_cell = if (end_before_start) + drag_x_frac < threshold_point + else + drag_x_frac >= threshold_point; + + // If the click cell should be included in the selection then it's the + // start, otherwise we get the previous or next cell to it depending on + // the type and direction of the selection. + const start_pin = + if (include_click_cell) + click_pin + else if (end_before_start) + if (rectangle_selection) + click_pin.leftClamp(1) + else + click_pin.leftWrap(1) orelse click_pin + else if (rectangle_selection) + click_pin.rightClamp(1) else - drag_pin.before(sel_start); + click_pin.rightWrap(1) orelse click_pin; + + // Likewise for the end pin with the drag cell. + const end_pin = + if (include_drag_cell) + drag_pin + else if (end_before_start) + if (rectangle_selection) + drag_pin.rightClamp(1) + else + drag_pin.rightWrap(1) orelse drag_pin + else if (rectangle_selection) + drag_pin.leftClamp(1) + else + drag_pin.leftWrap(1) orelse drag_pin; + + // If the click cell is the same as the drag cell and the click cell + // shouldn't be included, or if the cells are adjacent such that the + // start or end pin becomes the other cell, and that cell should not + // be included, then we have no selection, so we set it to null. + // + // If in rectangular selection mode, we compare columns as well. + // + // TODO(qwerasd): this can/should probably be refactored, it's a bit + // repetitive and does excess work in rectangle mode. + if ((!include_click_cell and same_pin) or + (!include_click_cell and rectangle_selection and click_pin.x == drag_pin.x) or + (!include_click_cell and end_pin.eql(click_pin)) or + (!include_click_cell and rectangle_selection and end_pin.x == click_pin.x) or + (!include_drag_cell and start_pin.eql(drag_pin)) or + (!include_drag_cell and rectangle_selection and start_pin.x == drag_pin.x)) + { + try self.setSelection(null); + return; } - // Nullifying a selection can't fail. - if (reset) self.setSelection(null) catch unreachable; -} + // TODO: Clamp selection to the screen area, don't + // let it extend past the last written row. -// Handles how whether or not the drag screen point is before the click point. -// When we are in rectangle select, we only interpret the x axis to determine -// where to start the selection (before or after the click point). See -// dragLeftClickSingle for more details. -fn dragLeftClickBefore( - drag_pin: terminal.Pin, - click_pin: terminal.Pin, - mods: input.Mods, -) bool { - if (mods.ctrlOrSuper() and mods.alt) { - return drag_pin.x < click_pin.x; - } + try self.setSelection( + terminal.Selection.init( + start_pin, + end_pin, + rectangle_selection, + ), + ); - return drag_pin.before(click_pin); + return; } /// Call to notify Ghostty that the color scheme for the terminal has diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 300af8e13..a0eb3edd1 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3572,6 +3572,74 @@ pub const Pin = struct { return result; } + /// Move the pin left n columns, stopping at the start of the row. + pub fn leftClamp(self: Pin, n: size.CellCountInt) Pin { + var result = self; + result.x -|= n; + return result; + } + + /// Move the pin right n columns, stopping at the end of the row. + pub fn rightClamp(self: Pin, n: size.CellCountInt) Pin { + var result = self; + result.x = @min(self.x +| n, self.node.data.size.cols - 1); + return result; + } + + /// Move the pin left n cells, wrapping to the previous row as needed. + /// + /// If the offset goes beyond the top of the screen, returns null. + /// + /// TODO: Unit tests. + pub fn leftWrap(self: Pin, n: usize) ?Pin { + // NOTE: This assumes that all pages have the same width, which may + // be violated under certain circumstances by incomplete reflow. + const cols = self.node.data.size.cols; + const remaining_in_row = self.x; + + if (n <= remaining_in_row) return self.left(n); + + const extra_after_remaining = n - remaining_in_row; + + const rows_off = 1 + extra_after_remaining / cols; + + switch (self.upOverflow(rows_off)) { + .offset => |v| { + var result = v; + result.x = @intCast(cols - extra_after_remaining % cols); + return result; + }, + .overflow => return null, + } + } + + /// Move the pin right n cells, wrapping to the next row as needed. + /// + /// If the offset goes beyond the bottom of the screen, returns null. + /// + /// TODO: Unit tests. + pub fn rightWrap(self: Pin, n: usize) ?Pin { + // NOTE: This assumes that all pages have the same width, which may + // be violated under certain circumstances by incomplete reflow. + const cols = self.node.data.size.cols; + const remaining_in_row = cols - self.x - 1; + + if (n <= remaining_in_row) return self.right(n); + + const extra_after_remaining = n - remaining_in_row; + + const rows_off = 1 + extra_after_remaining / cols; + + switch (self.downOverflow(rows_off)) { + .offset => |v| { + var result = v; + result.x = @intCast(extra_after_remaining % cols - 1); + return result; + }, + .overflow => return null, + } + } + /// Move the pin down a certain number of rows, or return null if /// the pin goes beyond the end of the screen. pub fn down(self: Pin, n: usize) ?Pin { From 4d11673318e91d020655b1c4313de96c1084be47 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 26 May 2025 12:33:36 -0600 Subject: [PATCH 4/6] unit test mouse selection logic Adds many test cases for expected behavior of the selection logic, this will allow changes to be made more confidently in the future without fear of regressions. --- src/Surface.zig | 1164 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1128 insertions(+), 36 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3726c37c7..ffe39d46d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3676,11 +3676,29 @@ fn dragLeftClickTriple( fn dragLeftClickSingle( self: *Surface, drag_pin: terminal.Pin, - xpos: f64, + drag_x: f64, ) !void { - // TODO: Unit tests for this logic, maybe extract it out to a pure - // function so that it can be tested without mocking state. + // This logic is in a separate function so that it can be unit tested. + try self.setSelection(mouseSelection( + self.mouse.left_click_pin.?.*, + drag_pin, + @intFromFloat(@max(0.0, self.mouse.left_click_xpos)), + @intFromFloat(@max(0.0, drag_x)), + self.mouse.mods, + self.size, + )); +} +/// Calculates the appropriate selection given pins and pixel x positions for +/// the click point and the drag point, as well as mouse mods and screen size. +fn mouseSelection( + click_pin: terminal.Pin, + drag_pin: terminal.Pin, + click_x: u32, + drag_x: u32, + mods: input.Mods, + size: rendererpkg.Size, +) ?terminal.Selection { // Explanation: // // # Normal selections @@ -3702,41 +3720,25 @@ fn dragLeftClickSingle( // Rectangular selections are handled similarly, except that // entire columns are considered rather than individual cells. - const click_pin = self.mouse.left_click_pin.?.*; - // We only include cells in the selection if the threshold point lies // between the start and end points of the selection. A threshold of // 60% of the cell width was chosen empirically because it felt good. - const threshold_point: u32 = @intFromFloat( - @as(f64, @floatFromInt(self.size.cell.width)) * 0.6, - ); + const threshold_point: u32 = @intFromFloat(@round( + @as(f64, @floatFromInt(size.cell.width)) * 0.6, + )); // We use this to clamp the pixel positions below. - const max_x = self.size.grid().columns * self.size.cell.width - 1; + const max_x = size.grid().columns * size.cell.width - 1; // We need to know how far across in the cell the drag pos is, so // we subtract the padding and then take it modulo the cell width. - const drag_x = @min( - max_x, - @as(u32, @intFromFloat(@max(0.0, xpos))) -| self.size.padding.left, - ); - const drag_x_frac = drag_x % self.size.cell.width; + const drag_x_frac = @min(max_x, drag_x -| size.padding.left) % size.cell.width; // We figure out the fractional part of the click x position similarly. - // - // NOTE: This click_x position may be incorrect for the current location - // of the click pin, since it's a tracked pin that can move, so we - // should only use this for the fractional position not absolute. - const click_x = @min( - max_x, - @as(u32, @intFromFloat(@max(0.0, self.mouse.left_click_xpos))) -| - self.size.padding.left, - ); - const click_x_frac = click_x % self.size.cell.width; + const click_x_frac = @min(max_x, click_x -| size.padding.left) % size.cell.width; // Whether or not this is a rectangular selection. - const rectangle_selection = - SurfaceMouse.isRectangleSelectState(self.mouse.mods); + const rectangle_selection = SurfaceMouse.isRectangleSelectState(mods); // Whether the click pin and drag pin are equal. const same_pin = drag_pin.eql(click_pin); @@ -3819,22 +3821,17 @@ fn dragLeftClickSingle( (!include_drag_cell and start_pin.eql(drag_pin)) or (!include_drag_cell and rectangle_selection and start_pin.x == drag_pin.x)) { - try self.setSelection(null); - return; + return null; } // TODO: Clamp selection to the screen area, don't // let it extend past the last written row. - try self.setSelection( - terminal.Selection.init( - start_pin, - end_pin, - rectangle_selection, - ), + return terminal.Selection.init( + start_pin, + end_pin, + rectangle_selection, ); - - return; } /// Call to notify Ghostty that the color scheme for the terminal has @@ -4819,3 +4816,1098 @@ fn presentSurface(self: *Surface) !void { {}, ); } + +test "Surface: selection logic" { + // Our screen size is 10x5 cells that are + // 10x20 px, with 5px padding on all sides. + const size: rendererpkg.Size = .{ + .cell = .{ .width = 10, .height = 20 }, + .padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 }, + .screen = .{ .width = 110, .height = 110 }, + }; + var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); + defer screen.deinit(); + + // We are testing normal selection logic here so no mods. + const mods: input.Mods = .{}; + + const expectEqual = std.testing.expectEqualDeep; + + // LTR, including click and drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including click pin cell but not drag pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin; + const end_pin = drag_pin.left(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including drag pin cell but not click pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At rightmost px in cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At rightmost px in cell. + + const start_pin = click_pin.right(1); + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including neither click nor drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin.right(1); + const end_pin = drag_pin.left(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, empty selection (single cell on only left half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = click_pin; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 1; // At px 1 within the cell. + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // LTR, empty selection (single cell on only right half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = click_pin; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 2; // 1px left of right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // LTR, empty selection (between two cells, not crossing threshold) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 4, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // --- RTL + + // RTL, including click and drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including click pin cell but not drag pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin; + const end_pin = drag_pin.right(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including drag pin cell but not click pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within cell. + + const start_pin = click_pin.left(1); + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including neither click nor drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin.left(1); + const end_pin = drag_pin.right(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, empty selection (single cell on only left half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = click_pin; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 1; // At px 1 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // RTL, empty selection (single cell on only right half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = click_pin; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 2; // 1px left of right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // RTL, empty selection (between two cells, not crossing threshold) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 4, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // -- Wrapping + + // LTR, wrap excluded cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + + const start_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 3 }, + }) orelse unreachable; + const end_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 3 }, + }) orelse unreachable; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, wrap excluded cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 4 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 2 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + const start_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 3 }, + }) orelse unreachable; + const end_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 3 }, + }) orelse unreachable; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } +} + +test "Surface: rectangle selection logic" { + // Our screen size is 10x5 cells that are + // 10x20 px, with 5px padding on all sides. + const size: rendererpkg.Size = .{ + .cell = .{ .width = 10, .height = 20 }, + .padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 }, + .screen = .{ .width = 110, .height = 110 }, + }; + var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); + defer screen.deinit(); + + // We hold ctrl and alt so that this test is platform-agnostic. + const mods: input.Mods = .{ + .ctrl = true, + .alt = true, + }; + + try std.testing.expect(SurfaceMouse.isRectangleSelectState(mods)); + + const expectEqual = std.testing.expectEqualDeep; + + // LTR, including click and drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including click pin cell but not drag pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin; + const end_pin = drag_pin.left(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including drag pin cell but not click pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At rightmost px in cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At rightmost px in cell. + + const start_pin = click_pin.right(1); + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including neither click nor drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin.right(1); + const end_pin = drag_pin.left(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, empty selection (single column on only left half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 1; // At px 1 within the cell. + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // LTR, empty selection (single column on only right half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 2; // 1px left of right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // LTR, empty selection (between two columns, not crossing threshold) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 4, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // --- RTL + + // RTL, including click and drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including click pin cell but not drag pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin; + const end_pin = drag_pin.right(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including drag pin cell but not click pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within cell. + + const start_pin = click_pin.left(1); + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including neither click nor drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin.left(1); + const end_pin = drag_pin.right(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, empty selection (single column on only left half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 1; // At px 1 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 1; // At px 0 within the cell. + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // RTL, empty selection (single column on only right half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 2; // 1px left of right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // RTL, empty selection (between two cells, not crossing threshold) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 4, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // -- Wrapping + + // LTR, do not wrap + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, do not wrap + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 4 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 2 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } +} From 6aa84d0e92ad4b88692a6da8c2834821574b5773 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 26 May 2025 14:31:59 -0600 Subject: [PATCH 5/6] test: introduce helper function for mouse selection tests Removes a lot of repeated code and makes the test cases easier to understand at a glance. --- src/Surface.zig | 1448 +++++++++++++---------------------------------- 1 file changed, 390 insertions(+), 1058 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index ffe39d46d..8b4f58496 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4817,7 +4817,29 @@ fn presentSurface(self: *Surface) !void { ); } -test "Surface: selection logic" { +/// Utility function for the unit tests for mouse selection logic. +/// +/// Tests a click and drag on a 10x5 cell grid, x positions are given in +/// fractional cells, e.g. 3.1 would be 10% through the cell at x = 3. +/// +/// NOTE: The size tested with has 10px wide cells, meaning only one digit +/// after the decimal place has any meaning, e.g. 3.14 is equal to 3.1. +/// +/// The provided start_x/y and end_x/y are the expected start and end points +/// of the resulting selection. +fn testMouseSelection( + click_x: f64, + click_y: u32, + drag_x: f64, + drag_y: u32, + start_x: terminal.size.CellCountInt, + start_y: u32, + end_x: terminal.size.CellCountInt, + end_y: u32, + rect: bool, +) !void { + assert(builtin.is_test); + // Our screen size is 10x5 cells that are // 10x20 px, with 5px padding on all sides. const size: rendererpkg.Size = .{ @@ -4828,1086 +4850,396 @@ test "Surface: selection logic" { var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); defer screen.deinit(); - // We are testing normal selection logic here so no mods. - const mods: input.Mods = .{}; + // We hold both ctrl and alt for rectangular + // select so that this test is platform agnostic. + const mods: input.Mods = .{ + .ctrl = rect, + .alt = rect, + }; - const expectEqual = std.testing.expectEqualDeep; + try std.testing.expectEqual(rect, SurfaceMouse.isRectangleSelectState(mods)); - // LTR, including click and drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = @intFromFloat(@floor(click_x)), .y = click_y }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = @intFromFloat(@floor(drag_x)), .y = drag_y }, + }) orelse unreachable; - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. + const cell_width_f64: f64 = @floatFromInt(size.cell.width); + const click_x_pos: u32 = + @as(u32, @intFromFloat(@floor(click_x * cell_width_f64))) + + size.padding.left; + const drag_x_pos: u32 = + @as(u32, @intFromFloat(@floor(drag_x * cell_width_f64))) + + size.padding.left; - const start_pin = click_pin; - const end_pin = drag_pin; + const start_pin = screen.pages.pin(.{ + .viewport = .{ .x = start_x, .y = start_y }, + }) orelse unreachable; + const end_pin = screen.pages.pin(.{ + .viewport = .{ .x = end_x, .y = end_y }, + }) orelse unreachable; - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( + try std.testing.expectEqualDeep(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = rect, + }, mouseSelection( + click_pin, + drag_pin, + click_x_pos, + drag_x_pos, + mods, + size, + )); +} + +/// Like `testMouseSelection` but checks that the resulting selection is null. +/// +/// See `testMouseSelection` for more details. +fn testMouseSelectionIsNull( + click_x: f64, + click_y: u32, + drag_x: f64, + drag_y: u32, + rect: bool, +) !void { + assert(builtin.is_test); + + // Our screen size is 10x5 cells that are + // 10x20 px, with 5px padding on all sides. + const size: rendererpkg.Size = .{ + .cell = .{ .width = 10, .height = 20 }, + .padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 }, + .screen = .{ .width = 110, .height = 110 }, + }; + var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); + defer screen.deinit(); + + // We hold both ctrl and alt for rectangular + // select so that this test is platform agnostic. + const mods: input.Mods = .{ + .ctrl = rect, + .alt = rect, + }; + + try std.testing.expectEqual(rect, SurfaceMouse.isRectangleSelectState(mods)); + + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = @intFromFloat(@floor(click_x)), .y = click_y }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = @intFromFloat(@floor(drag_x)), .y = drag_y }, + }) orelse unreachable; + + const cell_width_f64: f64 = @floatFromInt(size.cell.width); + const click_x_pos: u32 = + @as(u32, @intFromFloat(@floor(click_x * cell_width_f64))) + + size.padding.left; + const drag_x_pos: u32 = + @as(u32, @intFromFloat(@floor(drag_x * cell_width_f64))) + + size.padding.left; + + try std.testing.expectEqual( + null, + mouseSelection( click_pin, drag_pin, - click_x, - drag_x, + click_x_pos, + drag_x_pos, mods, size, - )); - } + ), + ); +} - // LTR, including click pin cell but not drag pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; +test "Surface: selection logic" { + // We disable format to make these easier to + // read by pairing sets of coordinates per line. + // zig fmt: off - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. + // -- LTR + // single cell selection + try testMouseSelection( + 3.0, 3, // click + 3.9, 3, // drag + 3, 3, // expected start + 3, 3, // expected end + false, // regular selection + ); + // including click and drag pin cells + try testMouseSelection( + 3.0, 3, // click + 5.9, 3, // drag + 3, 3, // expected start + 5, 3, // expected end + false, // regular selection + ); + // including click pin cell but not drag pin cell + try testMouseSelection( + 3.0, 3, // click + 5.0, 3, // drag + 3, 3, // expected start + 4, 3, // expected end + false, // regular selection + ); + // including drag pin cell but not click pin cell + try testMouseSelection( + 3.9, 3, // click + 5.9, 3, // drag + 4, 3, // expected start + 5, 3, // expected end + false, // regular selection + ); + // including neither click nor drag pin cells + try testMouseSelection( + 3.9, 3, // click + 5.0, 3, // drag + 4, 3, // expected start + 4, 3, // expected end + false, // regular selection + ); + // empty selection (single cell on only left half) + try testMouseSelectionIsNull( + 3.0, 3, // click + 3.1, 3, // drag + false, // regular selection + ); + // empty selection (single cell on only right half) + try testMouseSelectionIsNull( + 3.8, 3, // click + 3.9, 3, // drag + false, // regular selection + ); + // empty selection (between two cells, not crossing threshold) + try testMouseSelectionIsNull( + 3.9, 3, // click + 4.0, 3, // drag + false, // regular selection + ); - const start_pin = click_pin; - const end_pin = drag_pin.left(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including drag pin cell but not click pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At rightmost px in cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At rightmost px in cell. - - const start_pin = click_pin.right(1); - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including neither click nor drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin.right(1); - const end_pin = drag_pin.left(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, empty selection (single cell on only left half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = click_pin; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 1; // At px 1 within the cell. - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // LTR, empty selection (single cell on only right half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = click_pin; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 2; // 1px left of right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // LTR, empty selection (between two cells, not crossing threshold) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 4, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // --- RTL - - // RTL, including click and drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including click pin cell but not drag pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin; - const end_pin = drag_pin.right(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including drag pin cell but not click pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within cell. - - const start_pin = click_pin.left(1); - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including neither click nor drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin.left(1); - const end_pin = drag_pin.right(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, empty selection (single cell on only left half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = click_pin; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 1; // At px 1 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // RTL, empty selection (single cell on only right half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = click_pin; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 2; // 1px left of right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // RTL, empty selection (between two cells, not crossing threshold) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 4, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } + // -- RTL + // single cell selection + try testMouseSelection( + 3.9, 3, // click + 3.0, 3, // drag + 3, 3, // expected start + 3, 3, // expected end + false, // regular selection + ); + // including click and drag pin cells + try testMouseSelection( + 5.9, 3, // click + 3.0, 3, // drag + 5, 3, // expected start + 3, 3, // expected end + false, // regular selection + ); + // including click pin cell but not drag pin cell + try testMouseSelection( + 5.9, 3, // click + 3.9, 3, // drag + 5, 3, // expected start + 4, 3, // expected end + false, // regular selection + ); + // including drag pin cell but not click pin cell + try testMouseSelection( + 5.0, 3, // click + 3.0, 3, // drag + 4, 3, // expected start + 3, 3, // expected end + false, // regular selection + ); + // including neither click nor drag pin cells + try testMouseSelection( + 5.0, 3, // click + 3.9, 3, // drag + 4, 3, // expected start + 4, 3, // expected end + false, // regular selection + ); + // empty selection (single cell on only left half) + try testMouseSelectionIsNull( + 3.1, 3, // click + 3.0, 3, // drag + false, // regular selection + ); + // empty selection (single cell on only right half) + try testMouseSelectionIsNull( + 3.9, 3, // click + 3.8, 3, // drag + false, // regular selection + ); + // empty selection (between two cells, not crossing threshold) + try testMouseSelectionIsNull( + 4.0, 3, // click + 3.9, 3, // drag + false, // regular selection + ); // -- Wrapping - // LTR, wrap excluded cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - - const start_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 3 }, - }) orelse unreachable; - const end_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 3 }, - }) orelse unreachable; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - + try testMouseSelection( + 9.9, 2, // click + 0.0, 4, // drag + 0, 3, // expected start + 9, 3, // expected end + false, // regular selection + ); // RTL, wrap excluded cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 4 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 2 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - const start_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 3 }, - }) orelse unreachable; - const end_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 3 }, - }) orelse unreachable; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } + try testMouseSelection( + 0.0, 4, // click + 9.9, 2, // drag + 9, 3, // expected start + 0, 3, // expected end + false, // regular selection + ); } test "Surface: rectangle selection logic" { - // Our screen size is 10x5 cells that are - // 10x20 px, with 5px padding on all sides. - const size: rendererpkg.Size = .{ - .cell = .{ .width = 10, .height = 20 }, - .padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 }, - .screen = .{ .width = 110, .height = 110 }, - }; - var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); - defer screen.deinit(); + // We disable format to make these easier to + // read by pairing sets of coordinates per line. + // zig fmt: off - // We hold ctrl and alt so that this test is platform-agnostic. - const mods: input.Mods = .{ - .ctrl = true, - .alt = true, - }; + // -- LTR + // single column selection + try testMouseSelection( + 3.0, 2, // click + 3.9, 4, // drag + 3, 2, // expected start + 3, 4, // expected end + true, //rectangle selection + ); + // including click and drag pin columns + try testMouseSelection( + 3.0, 2, // click + 5.9, 4, // drag + 3, 2, // expected start + 5, 4, // expected end + true, //rectangle selection + ); + // including click pin column but not drag pin column + try testMouseSelection( + 3.0, 2, // click + 5.0, 4, // drag + 3, 2, // expected start + 4, 4, // expected end + true, //rectangle selection + ); + // including drag pin column but not click pin column + try testMouseSelection( + 3.9, 2, // click + 5.9, 4, // drag + 4, 2, // expected start + 5, 4, // expected end + true, //rectangle selection + ); + // including neither click nor drag pin columns + try testMouseSelection( + 3.9, 2, // click + 5.0, 4, // drag + 4, 2, // expected start + 4, 4, // expected end + true, //rectangle selection + ); + // empty selection (single column on only left half) + try testMouseSelectionIsNull( + 3.0, 2, // click + 3.1, 4, // drag + true, //rectangle selection + ); + // empty selection (single column on only right half) + try testMouseSelectionIsNull( + 3.8, 2, // click + 3.9, 4, // drag + true, //rectangle selection + ); + // empty selection (between two columns, not crossing threshold) + try testMouseSelectionIsNull( + 3.9, 2, // click + 4.0, 4, // drag + true, //rectangle selection + ); - try std.testing.expect(SurfaceMouse.isRectangleSelectState(mods)); - - const expectEqual = std.testing.expectEqualDeep; - - // LTR, including click and drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including click pin cell but not drag pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin; - const end_pin = drag_pin.left(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including drag pin cell but not click pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At rightmost px in cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At rightmost px in cell. - - const start_pin = click_pin.right(1); - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including neither click nor drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin.right(1); - const end_pin = drag_pin.left(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, empty selection (single column on only left half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 1; // At px 1 within the cell. - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // LTR, empty selection (single column on only right half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 2; // 1px left of right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // LTR, empty selection (between two columns, not crossing threshold) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 4, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // --- RTL - - // RTL, including click and drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including click pin cell but not drag pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin; - const end_pin = drag_pin.right(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including drag pin cell but not click pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within cell. - - const start_pin = click_pin.left(1); - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including neither click nor drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin.left(1); - const end_pin = drag_pin.right(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, empty selection (single column on only left half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 1; // At px 1 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 1; // At px 0 within the cell. - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // RTL, empty selection (single column on only right half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 2; // 1px left of right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // RTL, empty selection (between two cells, not crossing threshold) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 4, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } + // -- RTL + // single column selection + try testMouseSelection( + 3.9, 2, // click + 3.0, 4, // drag + 3, 2, // expected start + 3, 4, // expected end + true, //rectangle selection + ); + // including click and drag pin columns + try testMouseSelection( + 5.9, 2, // click + 3.0, 4, // drag + 5, 2, // expected start + 3, 4, // expected end + true, //rectangle selection + ); + // including click pin column but not drag pin column + try testMouseSelection( + 5.9, 2, // click + 3.9, 4, // drag + 5, 2, // expected start + 4, 4, // expected end + true, //rectangle selection + ); + // including drag pin column but not click pin column + try testMouseSelection( + 5.0, 2, // click + 3.0, 4, // drag + 4, 2, // expected start + 3, 4, // expected end + true, //rectangle selection + ); + // including neither click nor drag pin columns + try testMouseSelection( + 5.0, 2, // click + 3.9, 4, // drag + 4, 2, // expected start + 4, 4, // expected end + true, //rectangle selection + ); + // empty selection (single column on only left half) + try testMouseSelectionIsNull( + 3.1, 2, // click + 3.0, 4, // drag + true, //rectangle selection + ); + // empty selection (single column on only right half) + try testMouseSelectionIsNull( + 3.9, 2, // click + 3.8, 4, // drag + true, //rectangle selection + ); + // empty selection (between two columns, not crossing threshold) + try testMouseSelectionIsNull( + 4.0, 2, // click + 3.9, 4, // drag + true, //rectangle selection + ); // -- Wrapping - // LTR, do not wrap - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - + try testMouseSelection( + 9.9, 2, // click + 0.0, 4, // drag + 9, 2, // expected start + 0, 4, // expected end + true, //rectangle selection + ); // RTL, do not wrap - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 4 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 2 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } + try testMouseSelection( + 0.0, 4, // click + 9.9, 2, // drag + 0, 4, // expected start + 9, 2, // expected end + true, //rectangle selection + ); } From ba02f0ae22b06fa7e0f1a5b7b38f073c935b8c1e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 May 2025 09:45:31 -0700 Subject: [PATCH 6/6] decl literal --- src/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 8b4f58496..0a2885dff 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3827,7 +3827,7 @@ fn mouseSelection( // TODO: Clamp selection to the screen area, don't // let it extend past the last written row. - return terminal.Selection.init( + return .init( start_pin, end_pin, rectangle_selection,