diff --git a/src/Surface.zig b/src/Surface.zig index 8abbcecea..fded5b137 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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(); } diff --git a/src/terminal/SelectionGesture.zig b/src/terminal/SelectionGesture.zig index 4e7db013b..28663ac08 100644 --- a/src/terminal/SelectionGesture.zig +++ b/src/terminal/SelectionGesture.zig @@ -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);