From 4d11673318e91d020655b1c4313de96c1084be47 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 26 May 2025 12:33:36 -0600 Subject: [PATCH] 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, + )); + } +}