mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-28 07:45:20 +00:00
SelectionGesture: drag events
This commit is contained in:
683
src/Surface.zig
683
src/Surface.zig
@@ -250,12 +250,7 @@ const Mouse = struct {
|
||||
|
||||
/// Return the left-click pin only if it still belongs to the active screen.
|
||||
fn activeLeftClickPin(self: *const Mouse, screens: *const terminal.ScreenSet) ?*terminal.Pin {
|
||||
const gesture = &self.selection_gesture;
|
||||
const pin = gesture.left_click_pin orelse return null;
|
||||
if (gesture.left_click_screen != screens.active_key) return null;
|
||||
if (screens.generation(gesture.left_click_screen) != gesture.left_click_screen_generation) return null;
|
||||
_ = screens.get(gesture.left_click_screen) orelse return null;
|
||||
return pin;
|
||||
return self.selection_gesture.validatedLeftClickPin(screens);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1180,9 +1175,14 @@ fn selectionScrollTick(self: *Surface) !void {
|
||||
// don't do anything.
|
||||
if (self.mouse.selection_gesture.left_click_count == 0) return;
|
||||
|
||||
const delta: isize = switch (self.mouse.selection_gesture.left_drag_autoscroll) {
|
||||
.none => return,
|
||||
.up => -1,
|
||||
.down => 1,
|
||||
};
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
const pos_vp = self.posToViewport(pos.x, pos.y);
|
||||
const delta: isize = if (pos.y < 0) -1 else 1;
|
||||
|
||||
// We need our locked state for the remainder
|
||||
self.renderer_state.mutex.lock();
|
||||
@@ -1212,7 +1212,22 @@ fn selectionScrollTick(self: *Surface) !void {
|
||||
if (comptime std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
};
|
||||
try self.dragLeftClickSingle(pin, pos.x);
|
||||
if (self.mouse.selection_gesture.drag(t, .{
|
||||
.pin = pin,
|
||||
.xpos = pos.x,
|
||||
.ypos = pos.y,
|
||||
.rectangle = SurfaceMouse.isRectangleSelectState(self.mouse.mods),
|
||||
.geometry = .{
|
||||
.columns = @intCast(self.size.grid().columns),
|
||||
.cell_width = self.size.cell.width,
|
||||
.padding_left = self.size.padding.left,
|
||||
.screen_height = self.size.screen.height,
|
||||
},
|
||||
})) |sel| {
|
||||
try self.io.terminal.screens.active.select(sel);
|
||||
} else {
|
||||
try self.io.terminal.screens.active.select(null);
|
||||
}
|
||||
|
||||
// We modified our viewport and selection so we need to queue
|
||||
// a render.
|
||||
@@ -3807,6 +3822,7 @@ pub fn mouseButtonCallback(
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
self.mouse.selection_gesture.left_drag_autoscroll = .none;
|
||||
|
||||
// The selection clipboard is only updated for left-click drag when
|
||||
// the left button is released. This is to avoid the clipboard
|
||||
@@ -4515,15 +4531,6 @@ pub fn cursorPosCallback(
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// Stop selection scrolling when inside the viewport within a 1px buffer
|
||||
// for fullscreen windows, but only when selection scrolling is active.
|
||||
if (pos.y >= 1 and self.selection_scroll_active) {
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
|
||||
// Update our mouse state. We set this to null initially because we only
|
||||
// want to set it when we're not selecting or doing any other mouse
|
||||
// event.
|
||||
@@ -4606,25 +4613,6 @@ pub fn cursorPosCallback(
|
||||
// All roads lead to requiring a re-render at this point.
|
||||
try self.queueRender();
|
||||
|
||||
// If our y is negative, we're above the window. In this case, we scroll
|
||||
// up. The amount we scroll up is dependent on how negative we are.
|
||||
// We allow for a 1 pixel buffer at the top and bottom to detect
|
||||
// scroll even in full screen windows.
|
||||
// Note: one day, we can change this from distance to time based if we want.
|
||||
//log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen });
|
||||
const max_y: f32 = @floatFromInt(self.size.screen.height);
|
||||
|
||||
// If the mouse is outside the viewport and we have the left
|
||||
// mouse button pressed then we need to start the scroll timer.
|
||||
if ((pos.y <= 1 or pos.y > max_y - 1) and
|
||||
!self.selection_scroll_active)
|
||||
{
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = true },
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert to points
|
||||
const screen: *terminal.Screen = t.screens.active;
|
||||
const pin = screen.pages.pin(.{
|
||||
@@ -4637,9 +4625,37 @@ pub fn cursorPosCallback(
|
||||
return;
|
||||
};
|
||||
|
||||
const drag_selection = self.mouse.selection_gesture.drag(t, .{
|
||||
.pin = pin,
|
||||
.xpos = pos.x,
|
||||
.ypos = pos.y,
|
||||
.rectangle = SurfaceMouse.isRectangleSelectState(self.mouse.mods),
|
||||
.geometry = .{
|
||||
.columns = @intCast(self.size.grid().columns),
|
||||
.cell_width = self.size.cell.width,
|
||||
.padding_left = self.size.padding.left,
|
||||
.screen_height = self.size.screen.height,
|
||||
},
|
||||
});
|
||||
|
||||
switch (self.mouse.selection_gesture.left_drag_autoscroll) {
|
||||
.none => if (self.selection_scroll_active) {
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
);
|
||||
},
|
||||
.up, .down => if (!self.selection_scroll_active) {
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = true },
|
||||
.locked,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
// Handle dragging depending on click count
|
||||
switch (self.mouse.selection_gesture.left_click_count) {
|
||||
1 => try self.dragLeftClickSingle(pin, pos.x),
|
||||
1 => try self.io.terminal.screens.active.select(drag_selection),
|
||||
2 => try self.dragLeftClickDouble(pin),
|
||||
3 => try self.dragLeftClickTriple(pin),
|
||||
0 => unreachable, // handled above
|
||||
@@ -4726,172 +4742,6 @@ fn dragLeftClickTriple(
|
||||
try self.io.terminal.screens.active.select(sel);
|
||||
}
|
||||
|
||||
fn dragLeftClickSingle(
|
||||
self: *Surface,
|
||||
drag_pin: terminal.Pin,
|
||||
drag_x: f64,
|
||||
) !void {
|
||||
// This logic is in a separate function so that it can be unit tested.
|
||||
const click_pin: terminal.Pin = pin: {
|
||||
const set: *terminal.ScreenSet = &self.io.terminal.screens;
|
||||
const tracked = self.mouse.activeLeftClickPin(set) orelse return;
|
||||
break :pin tracked.*;
|
||||
};
|
||||
try self.io.terminal.screens.active.select(mouseSelection(
|
||||
click_pin,
|
||||
drag_pin,
|
||||
@intFromFloat(@max(0.0, self.mouse.selection_gesture.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
|
||||
//
|
||||
// ## 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.
|
||||
|
||||
// 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(@round(
|
||||
@as(f64, @floatFromInt(size.cell.width)) * 0.6,
|
||||
));
|
||||
|
||||
// We use this to clamp the pixel positions below.
|
||||
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_frac = @min(max_x, drag_x -| size.padding.left) % size.cell.width;
|
||||
|
||||
// We figure out the fractional part of the click x position similarly.
|
||||
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(mods);
|
||||
|
||||
// Whether the click pin and drag pin are equal.
|
||||
const same_pin = drag_pin.eql(click_pin);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
break :ebs drag_pin.before(click_pin);
|
||||
};
|
||||
|
||||
// Whether or not 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 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
|
||||
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))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Clamp selection to the screen area, don't
|
||||
// let it extend past the last written row.
|
||||
|
||||
return .init(
|
||||
start_pin,
|
||||
end_pin,
|
||||
rectangle_selection,
|
||||
);
|
||||
}
|
||||
|
||||
/// Call to notify Ghostty that the color scheme for the terminal has
|
||||
/// changed.
|
||||
pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void {
|
||||
@@ -6220,436 +6070,9 @@ fn presentSurface(self: *Surface) !void {
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 = .{
|
||||
.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, .{ .cols = 10, .rows = 5, .max_scrollback = 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;
|
||||
|
||||
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 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, .{ .cols = 10, .rows = 5, .max_scrollback = 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_pos,
|
||||
drag_x_pos,
|
||||
mods,
|
||||
size,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get information about the process(es) running within the surface. Returns
|
||||
/// `null` if there was an error getting the information or the information is
|
||||
/// not available on a particular platform.
|
||||
pub fn getProcessInfo(self: *Surface, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return self.io.getProcessInfo(info);
|
||||
}
|
||||
|
||||
test "Surface: selection logic" {
|
||||
// We disable format to make these easier to
|
||||
// read by pairing sets of coordinates per line.
|
||||
// zig fmt: off
|
||||
|
||||
// -- 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
|
||||
);
|
||||
|
||||
// -- 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
|
||||
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
|
||||
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" {
|
||||
// We disable format to make these easier to
|
||||
// read by pairing sets of coordinates per line.
|
||||
// zig fmt: off
|
||||
|
||||
// -- 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
|
||||
);
|
||||
|
||||
// -- 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
|
||||
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
|
||||
try testMouseSelection(
|
||||
0.0, 4, // click
|
||||
9.9, 2, // drag
|
||||
0, 4, // expected start
|
||||
9, 2, // expected end
|
||||
true, //rectangle selection
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const PageList = @import("PageList.zig");
|
||||
const Pin = PageList.Pin;
|
||||
const Screen = @import("Screen.zig");
|
||||
const ScreenSet = @import("ScreenSet.zig");
|
||||
const Selection = @import("Selection.zig");
|
||||
const Terminal = @import("Terminal.zig");
|
||||
|
||||
/// The tracked pin of the initial left click along with the screen
|
||||
@@ -30,6 +32,27 @@ left_click_time: ?std.time.Instant,
|
||||
left_click_xpos: f64,
|
||||
left_click_ypos: f64,
|
||||
|
||||
/// The current autoscroll state for the active left-click drag gesture.
|
||||
left_drag_autoscroll: Autoscroll,
|
||||
|
||||
/// The direction that selection dragging should autoscroll the viewport.
|
||||
/// This is derived from the most recent drag position relative to the
|
||||
/// surface bounds and reset whenever there is no active drag gesture.
|
||||
///
|
||||
/// When autoscroll is non-none, the caller should setup a timer
|
||||
/// to periodically scroll the screen the desired direction a certain
|
||||
/// amount. The timer and amount is up to the caller but reasonable
|
||||
/// defaults are approximately one row every 15 milliseconds.
|
||||
///
|
||||
/// This is used to implement selection above/below the viewport that
|
||||
/// wants to drag the viewport.
|
||||
pub const Autoscroll = enum { none, up, down };
|
||||
|
||||
/// Distance from the top or bottom surface edge, in pixels, where dragging
|
||||
/// should request autoscroll. This preserves the historical 1px buffer used
|
||||
/// so fullscreen-edge drags can still trigger autoscroll.
|
||||
const autoscroll_buffer: f64 = 1;
|
||||
|
||||
pub const init: SelectionGesture = .{
|
||||
.left_click_pin = null,
|
||||
.left_click_count = 0,
|
||||
@@ -38,6 +61,7 @@ pub const init: SelectionGesture = .{
|
||||
.left_click_screen_generation = 0,
|
||||
.left_click_xpos = 0,
|
||||
.left_click_ypos = 0,
|
||||
.left_drag_autoscroll = .none,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *SelectionGesture, t: *Terminal) void {
|
||||
@@ -53,9 +77,24 @@ pub fn deinit(self: *SelectionGesture, t: *Terminal) void {
|
||||
pub fn reset(self: *SelectionGesture, t: *Terminal) void {
|
||||
self.left_click_count = 0;
|
||||
self.left_click_time = null;
|
||||
self.left_drag_autoscroll = .none;
|
||||
self.untrackPin(t);
|
||||
}
|
||||
|
||||
/// Return the tracked left-click pin only if it still belongs to the active
|
||||
/// screen instance. This validates both the screen key and generation so a pin
|
||||
/// from a removed, recycled, or inactive screen is never exposed to callers.
|
||||
pub fn validatedLeftClickPin(
|
||||
self: *const SelectionGesture,
|
||||
screens: *const ScreenSet,
|
||||
) ?*Pin {
|
||||
const pin = self.left_click_pin orelse return null;
|
||||
if (self.left_click_screen != screens.active_key) return null;
|
||||
if (screens.generation(self.left_click_screen) != self.left_click_screen_generation) return null;
|
||||
_ = screens.get(self.left_click_screen) orelse return null;
|
||||
return pin;
|
||||
}
|
||||
|
||||
pub const Press = struct {
|
||||
/// The time when the press event occurred. Use a monotonic timer.
|
||||
/// This can be null if you're on a system that doesn't support
|
||||
@@ -101,6 +140,78 @@ pub fn press(
|
||||
try self.pressInitial(t, p);
|
||||
}
|
||||
|
||||
pub const Drag = struct {
|
||||
/// The cell where the current drag position is. This is used
|
||||
/// synchronously to calculate the selection and is not tracked.
|
||||
pin: Pin,
|
||||
|
||||
/// The x/y value of the drag relative to the surface with (0,0) being
|
||||
/// top-left.
|
||||
xpos: f64,
|
||||
ypos: f64,
|
||||
|
||||
/// True if the current drag should produce a rectangular selection.
|
||||
rectangle: bool,
|
||||
|
||||
/// Geometry required for selection threshold and autoscroll calculations.
|
||||
geometry: Geometry,
|
||||
|
||||
/// Display geometry needed to translate surface-relative pointer positions
|
||||
/// into selection behavior.
|
||||
pub const Geometry = struct {
|
||||
/// The number of columns in the rendered terminal grid.
|
||||
columns: u32,
|
||||
|
||||
/// The width of one terminal cell in surface pixels.
|
||||
cell_width: u32,
|
||||
|
||||
/// The left padding before the terminal grid begins, in surface pixels.
|
||||
padding_left: u32,
|
||||
|
||||
/// The height of the rendered terminal surface in surface pixels.
|
||||
screen_height: u32,
|
||||
};
|
||||
};
|
||||
|
||||
/// Record a drag event and return the current untracked drag selection.
|
||||
pub fn drag(
|
||||
self: *SelectionGesture,
|
||||
t: *Terminal,
|
||||
d: Drag,
|
||||
) ?Selection {
|
||||
// If we aren't currently clicked then we don't do any dragging
|
||||
// behavior.
|
||||
if (self.left_click_count == 0) {
|
||||
assert(self.left_drag_autoscroll == .none);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get our click pin. We get a validated pin because if our
|
||||
// screen changed out from under us then we aren't actually
|
||||
// clicking anymore.
|
||||
const click_pin = self.validatedLeftClickPin(&t.screens) orelse
|
||||
return null;
|
||||
|
||||
// Determine if we should autoscroll. If our drag position is above
|
||||
// the top, we go up. If its below the bottom we go down. Easy.
|
||||
const max_y: f64 = @floatFromInt(d.geometry.screen_height);
|
||||
self.left_drag_autoscroll = if (d.ypos <= autoscroll_buffer)
|
||||
.up
|
||||
else if (d.ypos > max_y - autoscroll_buffer)
|
||||
.down
|
||||
else
|
||||
.none;
|
||||
|
||||
return dragSelection(
|
||||
click_pin.*,
|
||||
d.pin,
|
||||
@intFromFloat(@max(0, self.left_click_xpos)),
|
||||
@intFromFloat(@max(0, d.xpos)),
|
||||
d.rectangle,
|
||||
d.geometry,
|
||||
);
|
||||
}
|
||||
|
||||
fn pressInitial(
|
||||
self: *SelectionGesture,
|
||||
t: *Terminal,
|
||||
@@ -125,6 +236,7 @@ fn pressInitial(
|
||||
self.left_click_xpos = p.xpos;
|
||||
self.left_click_ypos = p.ypos;
|
||||
self.left_click_time = p.time;
|
||||
self.left_drag_autoscroll = .none;
|
||||
}
|
||||
|
||||
fn pressRepeat(
|
||||
@@ -166,12 +278,155 @@ fn pressRepeat(
|
||||
}
|
||||
|
||||
self.left_click_time = time;
|
||||
self.left_drag_autoscroll = .none;
|
||||
self.left_click_count = @min(
|
||||
self.left_click_count + 1,
|
||||
3, // We only support triple clicks max
|
||||
);
|
||||
}
|
||||
|
||||
/// Calculates the appropriate selection given pins and pixel x positions for
|
||||
/// the click point and the drag point, as well as selection mode and geometry.
|
||||
fn dragSelection(
|
||||
click_pin: Pin,
|
||||
drag_pin: Pin,
|
||||
click_x: u32,
|
||||
drag_x: u32,
|
||||
rectangle_selection: bool,
|
||||
geometry: Drag.Geometry,
|
||||
) ?Selection {
|
||||
// Explanation:
|
||||
//
|
||||
// # 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.
|
||||
|
||||
// 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(@round(
|
||||
@as(f64, @floatFromInt(geometry.cell_width)) * 0.6,
|
||||
));
|
||||
|
||||
// We use this to clamp the pixel positions below.
|
||||
const max_x = geometry.columns * geometry.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_frac = @min(max_x, drag_x -| geometry.padding_left) % geometry.cell_width;
|
||||
|
||||
// We figure out the fractional part of the click x position similarly.
|
||||
const click_x_frac = @min(max_x, click_x -| geometry.padding_left) % geometry.cell_width;
|
||||
|
||||
// Whether the click pin and drag pin are equal.
|
||||
const same_pin = drag_pin.eql(click_pin);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
break :ebs drag_pin.before(click_pin);
|
||||
};
|
||||
|
||||
// Whether or not 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 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
|
||||
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))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Clamp selection to the screen area, don't
|
||||
// let it extend past the last written row.
|
||||
|
||||
return .init(
|
||||
start_pin,
|
||||
end_pin,
|
||||
rectangle_selection,
|
||||
);
|
||||
}
|
||||
|
||||
fn untrackPin(self: *SelectionGesture, t: *Terminal) void {
|
||||
// Can't untrack unless we have a pin.
|
||||
const pin = self.left_click_pin orelse return;
|
||||
@@ -200,6 +455,435 @@ fn testPress(t: *Terminal, x: u16, y: u32, time: ?std.time.Instant) Press {
|
||||
};
|
||||
}
|
||||
|
||||
fn testDrag(t: *Terminal, x: u16, y: u32, xpos: f64, ypos: f64) Drag {
|
||||
return .{
|
||||
.pin = t.screens.active.pages.pin(.{ .active = .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
} }).?,
|
||||
.xpos = xpos,
|
||||
.ypos = ypos,
|
||||
.rectangle = false,
|
||||
.geometry = .{
|
||||
.columns = 5,
|
||||
.cell_width = 10,
|
||||
.padding_left = 0,
|
||||
.screen_height = 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Utility function for the unit tests for drag 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 geometry 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 testDragSelection(
|
||||
click_x: f64,
|
||||
click_y: u32,
|
||||
drag_x: f64,
|
||||
drag_y: u32,
|
||||
start_x: u16,
|
||||
start_y: u32,
|
||||
end_x: u16,
|
||||
end_y: u32,
|
||||
rect: bool,
|
||||
) !void {
|
||||
assert(@import("builtin").is_test);
|
||||
|
||||
// Our screen size is 10x5 cells that are
|
||||
// 10x20 px, with 5px padding on all sides.
|
||||
const geometry: Drag.Geometry = .{
|
||||
.columns = 10,
|
||||
.cell_width = 10,
|
||||
.padding_left = 5,
|
||||
.screen_height = 110,
|
||||
};
|
||||
var screen = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
|
||||
defer screen.deinit();
|
||||
|
||||
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(geometry.cell_width);
|
||||
const click_x_pos: u32 =
|
||||
@as(u32, @intFromFloat(@floor(click_x * cell_width_f64))) +
|
||||
geometry.padding_left;
|
||||
const drag_x_pos: u32 =
|
||||
@as(u32, @intFromFloat(@floor(drag_x * cell_width_f64))) +
|
||||
geometry.padding_left;
|
||||
|
||||
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 testing.expectEqualDeep(Selection{
|
||||
.bounds = .{ .untracked = .{
|
||||
.start = start_pin,
|
||||
.end = end_pin,
|
||||
} },
|
||||
.rectangle = rect,
|
||||
}, dragSelection(
|
||||
click_pin,
|
||||
drag_pin,
|
||||
click_x_pos,
|
||||
drag_x_pos,
|
||||
rect,
|
||||
geometry,
|
||||
));
|
||||
}
|
||||
|
||||
/// Like `testDragSelection` but checks that the resulting selection is null.
|
||||
///
|
||||
/// See `testDragSelection` for more details.
|
||||
fn testDragSelectionIsNull(
|
||||
click_x: f64,
|
||||
click_y: u32,
|
||||
drag_x: f64,
|
||||
drag_y: u32,
|
||||
rect: bool,
|
||||
) !void {
|
||||
assert(@import("builtin").is_test);
|
||||
|
||||
// Our screen size is 10x5 cells that are
|
||||
// 10x20 px, with 5px padding on all sides.
|
||||
const geometry: Drag.Geometry = .{
|
||||
.columns = 10,
|
||||
.cell_width = 10,
|
||||
.padding_left = 5,
|
||||
.screen_height = 110,
|
||||
};
|
||||
var screen = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
|
||||
defer screen.deinit();
|
||||
|
||||
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(geometry.cell_width);
|
||||
const click_x_pos: u32 =
|
||||
@as(u32, @intFromFloat(@floor(click_x * cell_width_f64))) +
|
||||
geometry.padding_left;
|
||||
const drag_x_pos: u32 =
|
||||
@as(u32, @intFromFloat(@floor(drag_x * cell_width_f64))) +
|
||||
geometry.padding_left;
|
||||
|
||||
try testing.expectEqual(
|
||||
null,
|
||||
dragSelection(
|
||||
click_pin,
|
||||
drag_pin,
|
||||
click_x_pos,
|
||||
drag_x_pos,
|
||||
rect,
|
||||
geometry,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
test "SelectionGesture drag selection logic" {
|
||||
// We disable format to make these easier to
|
||||
// read by pairing sets of coordinates per line.
|
||||
// zig fmt: off
|
||||
|
||||
// -- LTR
|
||||
// single cell selection
|
||||
try testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelectionIsNull(
|
||||
3.0, 3, // click
|
||||
3.1, 3, // drag
|
||||
false, // regular selection
|
||||
);
|
||||
// empty selection (single cell on only right half)
|
||||
try testDragSelectionIsNull(
|
||||
3.8, 3, // click
|
||||
3.9, 3, // drag
|
||||
false, // regular selection
|
||||
);
|
||||
// empty selection (between two cells, not crossing threshold)
|
||||
try testDragSelectionIsNull(
|
||||
3.9, 3, // click
|
||||
4.0, 3, // drag
|
||||
false, // regular selection
|
||||
);
|
||||
|
||||
// -- RTL
|
||||
// single cell selection
|
||||
try testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelectionIsNull(
|
||||
3.1, 3, // click
|
||||
3.0, 3, // drag
|
||||
false, // regular selection
|
||||
);
|
||||
// empty selection (single cell on only right half)
|
||||
try testDragSelectionIsNull(
|
||||
3.9, 3, // click
|
||||
3.8, 3, // drag
|
||||
false, // regular selection
|
||||
);
|
||||
// empty selection (between two cells, not crossing threshold)
|
||||
try testDragSelectionIsNull(
|
||||
4.0, 3, // click
|
||||
3.9, 3, // drag
|
||||
false, // regular selection
|
||||
);
|
||||
|
||||
// -- Wrapping
|
||||
// LTR, wrap excluded cells
|
||||
try testDragSelection(
|
||||
9.9, 2, // click
|
||||
0.0, 4, // drag
|
||||
0, 3, // expected start
|
||||
9, 3, // expected end
|
||||
false, // regular selection
|
||||
);
|
||||
// RTL, wrap excluded cells
|
||||
try testDragSelection(
|
||||
0.0, 4, // click
|
||||
9.9, 2, // drag
|
||||
9, 3, // expected start
|
||||
0, 3, // expected end
|
||||
false, // regular selection
|
||||
);
|
||||
}
|
||||
|
||||
test "SelectionGesture rectangle drag selection logic" {
|
||||
// We disable format to make these easier to
|
||||
// read by pairing sets of coordinates per line.
|
||||
// zig fmt: off
|
||||
|
||||
// -- LTR
|
||||
// single column selection
|
||||
try testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelectionIsNull(
|
||||
3.0, 2, // click
|
||||
3.1, 4, // drag
|
||||
true, //rectangle selection
|
||||
);
|
||||
// empty selection (single column on only right half)
|
||||
try testDragSelectionIsNull(
|
||||
3.8, 2, // click
|
||||
3.9, 4, // drag
|
||||
true, //rectangle selection
|
||||
);
|
||||
// empty selection (between two columns, not crossing threshold)
|
||||
try testDragSelectionIsNull(
|
||||
3.9, 2, // click
|
||||
4.0, 4, // drag
|
||||
true, //rectangle selection
|
||||
);
|
||||
|
||||
// -- RTL
|
||||
// single column selection
|
||||
try testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelection(
|
||||
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 testDragSelectionIsNull(
|
||||
3.1, 2, // click
|
||||
3.0, 4, // drag
|
||||
true, //rectangle selection
|
||||
);
|
||||
// empty selection (single column on only right half)
|
||||
try testDragSelectionIsNull(
|
||||
3.9, 2, // click
|
||||
3.8, 4, // drag
|
||||
true, //rectangle selection
|
||||
);
|
||||
// empty selection (between two columns, not crossing threshold)
|
||||
try testDragSelectionIsNull(
|
||||
4.0, 2, // click
|
||||
3.9, 4, // drag
|
||||
true, //rectangle selection
|
||||
);
|
||||
|
||||
// -- Wrapping
|
||||
// LTR, do not wrap
|
||||
try testDragSelection(
|
||||
9.9, 2, // click
|
||||
0.0, 4, // drag
|
||||
9, 2, // expected start
|
||||
0, 4, // expected end
|
||||
true, //rectangle selection
|
||||
);
|
||||
// RTL, do not wrap
|
||||
try testDragSelection(
|
||||
0.0, 4, // click
|
||||
9.9, 2, // drag
|
||||
0, 4, // expected start
|
||||
9, 2, // expected end
|
||||
true, //rectangle selection
|
||||
);
|
||||
}
|
||||
|
||||
test "SelectionGesture press records initial click" {
|
||||
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
@@ -216,6 +900,33 @@ test "SelectionGesture press records initial click" {
|
||||
try testing.expectEqual(@as(f64, 2), gesture.left_click_ypos);
|
||||
}
|
||||
|
||||
test "SelectionGesture drag returns selection and records autoscroll" {
|
||||
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var gesture: SelectionGesture = .init;
|
||||
defer gesture.deinit(&t);
|
||||
|
||||
var press_event = testPress(&t, 1, 1, try std.time.Instant.now());
|
||||
press_event.xpos = 10;
|
||||
try gesture.press(&t, press_event);
|
||||
|
||||
const sel = gesture.drag(&t, testDrag(&t, 3, 1, 39, 50)).?;
|
||||
try testing.expectEqual(.none, gesture.left_drag_autoscroll);
|
||||
|
||||
try testing.expectEqualDeep(Selection.init(
|
||||
t.screens.active.pages.pin(.{ .active = .{ .x = 1, .y = 1 } }).?,
|
||||
t.screens.active.pages.pin(.{ .active = .{ .x = 3, .y = 1 } }).?,
|
||||
false,
|
||||
), sel);
|
||||
|
||||
_ = gesture.drag(&t, testDrag(&t, 3, 1, 39, 1));
|
||||
try testing.expectEqual(.up, gesture.left_drag_autoscroll);
|
||||
|
||||
_ = gesture.drag(&t, testDrag(&t, 3, 1, 39, 100));
|
||||
try testing.expectEqual(.down, gesture.left_drag_autoscroll);
|
||||
}
|
||||
|
||||
test "SelectionGesture repeat increments click count" {
|
||||
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
Reference in New Issue
Block a user