terminal: SelectionGesture autoscrollTick

This commit is contained in:
Mitchell Hashimoto
2026-05-26 21:21:08 -07:00
parent 141c7d44d2
commit df98b6d983
2 changed files with 106 additions and 31 deletions

View File

@@ -1171,15 +1171,15 @@ fn selectionScrollTick(self: *Surface) !void {
// If we're no longer active then we don't do anything.
if (!self.selection_scroll_active) return;
// If we don't have a left mouse button down then we
// 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,
};
// If our gesture doesn't want autoscrolling then disable it.
const was_autoscrolling = self.mouse.selection_gesture.left_drag_autoscroll != .none;
if (!was_autoscrolling) {
self.queueIo(
.{ .selection_scroll = false },
.unlocked,
);
return;
}
const pos = try self.rt_surface.getCursorPos();
const pos_vp = self.posToViewport(pos.x, pos.y);
@@ -1189,20 +1189,6 @@ fn selectionScrollTick(self: *Surface) !void {
defer self.renderer_state.mutex.unlock();
const t: *terminal.Terminal = self.renderer_state.terminal;
// If our left-click pin no longer belongs to the active screen, we stop
// our selection scroll.
if (self.mouse.activeLeftClickPin(&t.screens) == null) {
self.queueIo(
.{ .selection_scroll = false },
.locked,
);
return;
}
// Scroll the viewport as required
t.scrollViewport(.{ .delta = delta });
// Next, trigger our drag behavior
const pin = t.screens.active.pages.pin(.{
.viewport = .{
.x = pos_vp.x,
@@ -1212,7 +1198,8 @@ fn selectionScrollTick(self: *Surface) !void {
if (comptime std.debug.runtime_safety) unreachable;
return;
};
if (self.mouse.selection_gesture.drag(t, .{
const selection = self.mouse.selection_gesture.autoscrollTick(t, .{
.pin = pin,
.xpos = pos.x,
.ypos = pos.y,
@@ -1224,14 +1211,23 @@ fn selectionScrollTick(self: *Surface) !void {
.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);
});
// If we're no longer autoscrolling for whatever reason, disable it.
if (self.mouse.selection_gesture.left_drag_autoscroll == .none) {
self.queueIo(
.{ .selection_scroll = false },
.locked,
);
}
// If our left click was invalidated, ignore the result. This isn't
// strictly necessary but its a nice to have.
if (self.mouse.selection_gesture.left_click_count == 0) return;
// We modified our viewport and selection so we need to queue
// a render.
try self.io.terminal.screens.active.select(selection);
try self.queueRender();
}

View File

@@ -46,9 +46,8 @@ left_drag_autoscroll: Autoscroll,
/// 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.
/// to periodically call autoscrollTick. The timer interval is up to the
/// caller but reasonable defaults are approximately every 15 milliseconds.
///
/// This is used to implement selection above/below the viewport that
/// wants to drag the viewport.
@@ -241,6 +240,37 @@ pub fn drag(
};
}
/// Record a selection autoscroll tick for the active left-click drag gesture.
/// This scrolls the viewport in the active autoscroll direction and then
/// continues the drag at the provided position.
pub fn autoscrollTick(
self: *SelectionGesture,
t: *Terminal,
d: Drag,
) ?Selection {
if (self.left_click_count == 0) {
assert(self.left_drag_autoscroll == .none);
return null;
}
const delta: isize = switch (self.left_drag_autoscroll) {
.none => return null,
.up => -1,
.down => 1,
};
// If our click pin no longer belongs to the active screen, the gesture is
// no longer valid. Stop it so callers can stop their autoscroll timer
// without clearing the current selection as if this were a real drag.
_ = self.validatedLeftClickPin(&t.screens) orelse {
self.reset(t);
return null;
};
t.scrollViewport(.{ .delta = delta });
return self.drag(t, d);
}
pub const Release = struct {
/// The cell where the release occurred, if the release position mapped to
/// a valid cell. This is used synchronously to update gesture state and is
@@ -1114,6 +1144,55 @@ test "SelectionGesture drag autoscroll edge boundaries" {
try testing.expectEqual(.down, gesture.left_drag_autoscroll);
}
test "SelectionGesture autoscroll tick scrolls and continues drag" {
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);
_ = gesture.drag(&t, testDrag(&t, 3, 1, 39, 100));
try testing.expectEqual(.down, gesture.left_drag_autoscroll);
const sel = gesture.autoscrollTick(&t, testDrag(&t, 3, 2, 39, 100)).?;
try testing.expectEqual(.down, gesture.left_drag_autoscroll);
try testing.expectEqual(true, gesture.left_click_dragged);
try testing.expectEqualDeep(Selection.init(
testPin(&t, 1, 1),
testPin(&t, 3, 2),
false,
), sel);
}
test "SelectionGesture autoscroll tick stops with invalidated click" {
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);
_ = gesture.drag(&t, testDrag(&t, 2, 1, 20, 1));
try testing.expectEqual(.up, gesture.left_drag_autoscroll);
_ = try t.screens.getInit(testing.allocator, .alternate, .{
.cols = t.cols,
.rows = t.rows,
});
t.screens.switchTo(.alternate);
try testing.expectEqual(null, gesture.autoscrollTick(&t, testDrag(&t, 2, 1, 20, 1)));
try testing.expectEqual(.none, gesture.left_drag_autoscroll);
try testing.expectEqual(@as(u3, 0), gesture.left_click_count);
}
test "SelectionGesture drag with invalidated click returns null" {
var t = try Terminal.init(testing.allocator, .{ .cols = 5, .rows = 5 });
defer t.deinit(testing.allocator);