diff --git a/src/Surface.zig b/src/Surface.zig index c359efd8ad..54070b1c33 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3329,6 +3329,17 @@ pub fn cursorPosCallback( // Mark the link's row as dirty, but continue with updating the // mouse state below so we can scroll when our position is negative. self.renderer_state.terminal.screen.dirty.hyperlink_hover = true; + + // Stop selection scrolling when releasing the left mouse button + // outside the viewport. + if (self.mouse.click_state[@intFromEnum(input.MouseButton.left)] == .release and self.io_thread.scroll_active) { + self.io.queueMessage(.{ .selection_scroll = false }, .unlocked); + } + } + + // Stop selection scrolling when inside the viewport. + if (pos.x >= 0 and pos.y >= 0 and self.io_thread.scroll_active) { + self.io.queueMessage(.{ .selection_scroll = false }, .unlocked); } // Always show the mouse again if it is hidden @@ -3432,13 +3443,12 @@ pub fn cursorPosCallback( // 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 (pos.y <= 1 or pos.y > max_y - 1) { - const delta: isize = if (pos.y < 0) -1 else 1; - try self.io.terminal.scrollViewport(.{ .delta = delta }); - // TODO: We want a timer or something to repeat while we're still - // at this cursor position. Right now, the user has to jiggle their - // mouse in order to scroll. + // Only send a message when outside the viewport and + // selection scrolling is not currently active. + if ((pos.y <= 1 or pos.y > max_y - 1) and !self.io_thread.scroll_active) { + // TODO: the selection region ideally should keep up with this + self.io.queueMessage(.{ .selection_scroll = true }, .locked); } // Convert to points diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index d80046737a..2bf81947c6 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -35,6 +35,7 @@ const Coalesce = struct { /// The number of milliseconds before we reset the synchronized output flag /// if the running program hasn't already. const sync_reset_ms = 1000; +const selection_scroll_ms = 15; /// Allocator used for some state alloc: std.mem.Allocator, @@ -52,6 +53,11 @@ wakeup_c: xev.Completion = .{}, stop: xev.Async, stop_c: xev.Completion = .{}, +/// The timer used for selection scrolling +scroll: xev.Timer, +scroll_c: xev.Completion = .{}, +scroll_active: bool = false, + /// This is used to coalesce resize events. coalesce: xev.Timer, coalesce_c: xev.Completion = .{}, @@ -91,6 +97,10 @@ pub fn init( var stop_h = try xev.Async.init(); errdefer stop_h.deinit(); + // This timer is used for selection scrolling. + var scroll_h = try xev.Timer.init(); + errdefer scroll_h.deinit(); + // This timer is used to coalesce resize events. var coalesce_h = try xev.Timer.init(); errdefer coalesce_h.deinit(); @@ -103,6 +113,7 @@ pub fn init( .alloc = alloc, .loop = loop, .stop = stop_h, + .scroll = scroll_h, .coalesce = coalesce_h, .sync_reset = sync_reset_h, }; @@ -111,6 +122,7 @@ pub fn init( /// Clean up the thread. This is only safe to call once the thread /// completes executing; the caller must join prior to this. pub fn deinit(self: *Thread) void { + self.scroll.deinit(); self.coalesce.deinit(); self.sync_reset.deinit(); self.stop.deinit(); @@ -280,6 +292,13 @@ fn drainMailbox( .size_report => |v| try io.sizeReport(data, v), .clear_screen => |v| try io.clearScreen(data, v.history), .scroll_viewport => |v| try io.scrollViewport(v), + .selection_scroll => |v| { + if (v) { + self.startScrollTimer(cb); + } else { + self.stopScrollTimer(); + } + }, .jump_to_prompt => |v| try io.jumpToPrompt(v), .start_synchronized_output => self.startSynchronizedOutput(cb), .linefeed_mode => |v| self.flags.linefeed_mode = v, @@ -419,3 +438,48 @@ fn stopCallback( cb_.?.self.loop.stop(); return .disarm; } + +fn startScrollTimer(self: *Thread, cb: *CallbackData) void { + self.scroll_active = true; + + // Start the timer which loops + self.scroll.run(&self.loop, &self.scroll_c, selection_scroll_ms, CallbackData, cb, selectionScrollCallback); +} + +fn stopScrollTimer(self: *Thread) void { + // This will stop the scrolling on the next iteration. + self.scroll_active = false; +} + +fn selectionScrollCallback( + cb_: ?*CallbackData, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch |err| switch (err) { + error.Canceled => {}, + else => { + log.warn("error during selection scroll callback err={}", .{err}); + return .disarm; + }, + }; + + const cb = cb_ orelse return .disarm; + + const pos = try cb.io.surface_mailbox.surface.rt_surface.getCursorPos(); + const delta: isize = if (pos.y < 0) -1 else 1; + + try cb.io.terminal.scrollViewport(.{ .delta = delta }); + + // Notify the renderer that it should repaint immediately after scrolling + cb.io.renderer_wakeup.notify() catch {}; + + const self = cb.self; + + if (self.scroll_active) { + self.scroll.run(&self.loop, &self.scroll_c, selection_scroll_ms, CallbackData, cb, selectionScrollCallback); + } + + return .disarm; +} diff --git a/src/termio/message.zig b/src/termio/message.zig index 44381b2284..0ab5ac8235 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -47,6 +47,9 @@ pub const Message = union(enum) { /// Scroll the viewport scroll_viewport: terminal.Terminal.ScrollViewport, + /// Selection scrolling + selection_scroll: bool, + /// Jump forward/backward n prompts. jump_to_prompt: isize,