mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
surface: add timer-based scrolling during selection (#4422)
Adds a timer to continuously scroll during selection when outside the viewport, 15ms per line. Currently the scrolling behavior requires you to jiggle the mouse to continuously scroll upwards/downwards when selecting text. ### Before https://github.com/user-attachments/assets/18e6c547-ed04-4098-88b4-35360f8c8c3c ### After https://github.com/user-attachments/assets/46d5a6fc-b38e-46cf-b00f-52c8bc289f52
This commit is contained in:
@@ -138,6 +138,9 @@ child_exited: bool = false,
|
||||
/// to let us know.
|
||||
focused: bool = true,
|
||||
|
||||
/// Used to determine whether to continuously scroll.
|
||||
selection_scroll_active: bool = false,
|
||||
|
||||
/// The effect of an input event. This can be used by callers to take
|
||||
/// the appropriate action after an input event. For example, key
|
||||
/// input can be forwarded to the OS for further processing if it
|
||||
@@ -945,9 +948,51 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
log.warn("apprt failed to ring bell={}", .{err});
|
||||
};
|
||||
},
|
||||
|
||||
.selection_scroll_tick => |active| {
|
||||
self.selection_scroll_active = active;
|
||||
try self.selectionScrollTick();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.left_click_count == 0) return;
|
||||
|
||||
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();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const t: *terminal.Terminal = self.renderer_state.terminal;
|
||||
|
||||
// Scroll the viewport as required
|
||||
try t.scrollViewport(.{ .delta = delta });
|
||||
|
||||
// Next, trigger our drag behavior
|
||||
const pin = t.screen.pages.pin(.{
|
||||
.viewport = .{
|
||||
.x = pos_vp.x,
|
||||
.y = pos_vp.y,
|
||||
},
|
||||
}) orelse {
|
||||
if (comptime std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
};
|
||||
try self.dragLeftClickSingle(pin, pos.x);
|
||||
|
||||
// We modified our viewport and selection so we need to queue
|
||||
// a render.
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void {
|
||||
// Mark our flag that we exited immediately
|
||||
self.child_exited = true;
|
||||
@@ -3233,6 +3278,15 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
|
||||
if (button == .left and action == .release) {
|
||||
// Stop selection scrolling when releasing the left mouse button
|
||||
// but only when selection scrolling is active.
|
||||
if (self.selection_scroll_active) {
|
||||
self.io.queueMessage(
|
||||
.{ .selection_scroll = false },
|
||||
.unlocked,
|
||||
);
|
||||
}
|
||||
|
||||
// The selection clipboard is only updated for left-click drag when
|
||||
// the left button is released. This is to avoid the clipboard
|
||||
// being updated on every mouse move which would be noisy.
|
||||
@@ -3786,6 +3840,15 @@ 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.x >= 1 and pos.y >= 1 and self.selection_scroll_active) {
|
||||
self.io.queueMessage(
|
||||
.{ .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.
|
||||
@@ -3868,13 +3931,16 @@ 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.
|
||||
// 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.io.queueMessage(
|
||||
.{ .selection_scroll = true },
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert to points
|
||||
|
@@ -79,6 +79,13 @@ pub const Message = union(enum) {
|
||||
color: terminal.color.RGB,
|
||||
},
|
||||
|
||||
/// Notifies the surface that a tick of the timer that is timing
|
||||
/// out selection scrolling has occurred. "selection scrolling"
|
||||
/// is when the user has clicked and dragged the mouse outside
|
||||
/// the viewport of the terminal and the terminal is scrolling
|
||||
/// the viewport to follow the mouse cursor.
|
||||
selection_scroll_tick: bool,
|
||||
|
||||
/// The terminal has reported a change in the working directory.
|
||||
pwd_change: WriteReq,
|
||||
|
||||
|
@@ -124,6 +124,9 @@ flags: packed struct {
|
||||
/// to true based on termios state.
|
||||
password_input: bool = false,
|
||||
|
||||
/// True if the terminal should perform selection scrolling.
|
||||
selection_scroll: bool = false,
|
||||
|
||||
/// Dirty flags for the renderer.
|
||||
dirty: Dirty = .{},
|
||||
} = .{},
|
||||
|
@@ -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,57 @@ 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 self = cb.self;
|
||||
|
||||
// Send the tick to the main surface
|
||||
_ = cb.io.surface_mailbox.push(
|
||||
.{ .selection_scroll_tick = self.scroll_active },
|
||||
.{ .instant = {} },
|
||||
);
|
||||
|
||||
if (self.scroll_active) self.scroll.run(
|
||||
&self.loop,
|
||||
&self.scroll_c,
|
||||
selection_scroll_ms,
|
||||
CallbackData,
|
||||
cb,
|
||||
selectionScrollCallback,
|
||||
);
|
||||
|
||||
return .disarm;
|
||||
}
|
||||
|
@@ -48,6 +48,12 @@ pub const Message = union(enum) {
|
||||
/// Scroll the viewport
|
||||
scroll_viewport: terminal.Terminal.ScrollViewport,
|
||||
|
||||
/// Selection scrolling. If this is set to true then the termio
|
||||
/// thread starts a timer that will trigger a `selection_scroll_tick`
|
||||
/// message back to the surface. This ping/pong is because the
|
||||
/// surface thread doesn't have access to an event loop from libghostty.
|
||||
selection_scroll: bool,
|
||||
|
||||
/// Jump forward/backward n prompts.
|
||||
jump_to_prompt: isize,
|
||||
|
||||
|
Reference in New Issue
Block a user