diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index a701a29f8..e10387ef9 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -37,6 +37,9 @@ const Coalesce = struct { /// if the running program hasn't already. const sync_reset_ms = 1000; +/// The number of milliseconds between each movement during selection scrolling. +const selection_scroll_ms = 15; + /// Allocator used for some state alloc: std.mem.Allocator, @@ -53,6 +56,11 @@ wakeup_c: xev.Completion = .{}, stop: xev.Async, stop_c: xev.Completion = .{}, +/// This is used for timer-based 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 = .{}, @@ -92,6 +100,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(); @@ -104,6 +116,7 @@ pub fn init( .alloc = alloc, .loop = loop, .stop = stop_h, + .scroll = scroll_h, .coalesce = coalesce_h, .sync_reset = sync_reset_h, }; @@ -112,6 +125,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(); @@ -308,6 +322,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, @@ -446,3 +467,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 surface = cb.io.surface_mailbox.surface; + const pos = try 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 e497a298f..8057d226a 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -48,6 +48,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,