From 34388ab5df393ef1b9e7b5a0a5f4097565634c76 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 2 Mar 2025 07:14:29 -0600 Subject: [PATCH 1/6] surface: calculate scroll amount directly from yoff/xoff for non-precision scrolls Calculate the scroll amount for non-precision scrolls as a direct multiple of yoff. This fixes an issue where Ghostty sends scroll wheel events (or arrow keys if in alternate scroll mode) that are variable, dependent on the screen size. I checked multiple terminals, and each responds to a single wheel click by sending only a single wheel / arrow key - independent of screen size. ```sh printf "\x1b[?1049h" printf "\x1b[?1007h" cat -v ``` Using the above procedure, with varying screen sizes: ``` # 50% Screen height | terminal | arrows keys sent| wheels events sent| |------------|-----------------|-------------------| | alacritty | 3 | 1 | | foot | 3 | 1 | | xterm | 5 | 1 | | kitty | 3 | 1 | | ghostty | 2 | 2 | # 100% Screen height | terminal | arrows keys sent| wheels events sent| |------------|-----------------|-------------------| | alacritty | 3 | 1 | | foot | 3 | 1 | | xterm | 5 | 1 | | kitty | 5 | 1 | | ghostty | 3 | 3 | ``` Both ghostty and kitty scale the number of arrow keys sent in proportion to the screen size. However, when mouse reporting is on, only ghostty does this. This commit makes Ghostty behave like foot, and more generally removes the dependence on screen size. --- src/Surface.zig | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 6651dd8c2..d64b32eb5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2348,22 +2348,12 @@ pub fn scrollCallback( if (self.mouse.hidden) self.showMouse(); const y: ScrollAmount = if (yoff == 0) .{} else y: { - // Non-precision scrolling is easy to calculate. We don't use - // the given offset at all and instead just treat a positive - // as a scroll up and a negative as a scroll down and scroll in - // steps. + // Non-precision scrolls don't accumulate. We cast that raw yoff to an isize and interpret + // it as the number of lines to scroll. if (!scroll_mods.precision) { - // Calculate our magnitude of scroll. This is constant (not - // dependent on yoff). - const grid_size = self.size.grid(); - const grid_rows_f64: f64 = @floatFromInt(grid_size.rows); - const y_delta_f64: f64 = @round((grid_rows_f64 * self.config.mouse_scroll_multiplier) / 15.0); - const y_delta_usize: usize = @max(1, @as(usize, @intFromFloat(y_delta_f64))); - - // Calculate our direction of scroll based on the sign of yoff. - const y_sign: isize = if (yoff >= 0) 1 else -1; - const y_delta_isize: isize = y_sign * @as(isize, @intCast(y_delta_usize)); - + // Calculate our magnitude of scroll. This is a direct multiple of yoff + const y_delta_f64: f64 = @round(yoff * self.config.mouse_scroll_multiplier); + const y_delta_isize: isize = @intFromFloat(y_delta_f64); break :y .{ .delta = y_delta_isize }; } @@ -2404,10 +2394,8 @@ pub fn scrollCallback( // For detailed comments see the y calculation above. const x: ScrollAmount = if (xoff == 0) .{} else x: { if (!scroll_mods.precision) { - const x_delta_f64: f64 = @round(1 * self.config.mouse_scroll_multiplier); - const x_delta_usize: usize = @max(1, @as(usize, @intFromFloat(x_delta_f64))); - const x_sign: isize = if (xoff >= 0) 1 else -1; - const x_delta_isize: isize = x_sign * @as(isize, @intCast(x_delta_usize)); + const x_delta_f64: f64 = @round(xoff * self.config.mouse_scroll_multiplier); + const x_delta_isize: isize = @intFromFloat(x_delta_f64); break :x .{ .delta = x_delta_isize }; } From dbba3f1a604ed12cb980e200e5891070db9c8ca6 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 2 Mar 2025 07:58:05 -0600 Subject: [PATCH 2/6] scroll: don't use multiplier for wheel events When we report mouse scroll wheel events, they should not be multiplied. Refactor the scrollCallback to only use a multiplier for viewport or alternate scroll reports. --- src/Surface.zig | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index d64b32eb5..37b24160d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2325,6 +2325,13 @@ const ScrollAmount = struct { pub fn magnitude(self: ScrollAmount) usize { return @abs(self.delta); } + + pub fn multiplied(self: ScrollAmount, multiplier: f64) ScrollAmount { + const delta_f64: f64 = @floatFromInt(self.delta); + const delta_adjusted: f64 = delta_f64 * multiplier; + const delta_isize: isize = @intFromFloat(@round(delta_adjusted)); + return .{ .delta = delta_isize }; + } }; /// Mouse scroll event. Negative is down, left. Positive is up, right. @@ -2352,8 +2359,7 @@ pub fn scrollCallback( // it as the number of lines to scroll. if (!scroll_mods.precision) { // Calculate our magnitude of scroll. This is a direct multiple of yoff - const y_delta_f64: f64 = @round(yoff * self.config.mouse_scroll_multiplier); - const y_delta_isize: isize = @intFromFloat(y_delta_f64); + const y_delta_isize: isize = @intFromFloat(@round(yoff)); break :y .{ .delta = y_delta_isize }; } @@ -2362,7 +2368,7 @@ pub fn scrollCallback( // tiny amount so that we can scroll by a full row when we have enough. // Adjust our offset by the multiplier - const yoff_adjusted: f64 = yoff * self.config.mouse_scroll_multiplier; + const yoff_adjusted: f64 = yoff; // Add our previously saved pending amount to the offset to get the // new offset value. The signs of the pending and yoff should match @@ -2394,13 +2400,11 @@ pub fn scrollCallback( // For detailed comments see the y calculation above. const x: ScrollAmount = if (xoff == 0) .{} else x: { if (!scroll_mods.precision) { - const x_delta_f64: f64 = @round(xoff * self.config.mouse_scroll_multiplier); - const x_delta_isize: isize = @intFromFloat(x_delta_f64); + const x_delta_isize: isize = @intFromFloat(@round(xoff)); break :x .{ .delta = x_delta_isize }; } - const xoff_adjusted: f64 = xoff * self.config.mouse_scroll_multiplier; - const poff: f64 = self.mouse.pending_scroll_x + xoff_adjusted; + const poff: f64 = self.mouse.pending_scroll_x + xoff; const cell_size: f64 = @floatFromInt(self.size.cell.width); if (@abs(poff) < cell_size) { self.mouse.pending_scroll_x = poff; @@ -2454,7 +2458,9 @@ pub fn scrollCallback( .down_left => "\x1b[B", }; }; - for (0..y.magnitude()) |_| { + // We multiple by the scroll multiplier when reporting arrows + const multiplied = y.multiplied(self.config.mouse_scroll_multiplier); + for (0..multiplied.magnitude()) |_| { self.io.queueMessage(.{ .write_stable = seq }, .locked); } } @@ -2490,10 +2496,12 @@ pub fn scrollCallback( } if (y.delta != 0) { + // We multiply by the multiplier when scrolling the viewport + const multiplied = y.multiplied(self.config.mouse_scroll_multiplier); // Modify our viewport, this requires a lock since it affects // rendering. We have to switch signs here because our delta // is negative down but our viewport is positive down. - try self.io.terminal.scrollViewport(.{ .delta = y.delta * -1 }); + try self.io.terminal.scrollViewport(.{ .delta = multiplied.delta * -1 }); } } From 6e751d2d7e3bd21945b9a7f77701026848aa01af Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 2 Mar 2025 08:03:09 -0600 Subject: [PATCH 3/6] config: default mouse-scroll-multiplier to 3.0 Make Ghostty behave like other terminals by multiplying scrolls by 3.0. This only affects when we are reporting arrow keys (alternate scroll mode) or when we are scrolling the scrollback. --- src/config/Config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index d2c033cdc..589460a18 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -612,7 +612,7 @@ palette: Palette = .{}, /// A value of "1" (default) scrolls the default amount. A value of "2" scrolls /// double the default amount. A value of "0.5" scrolls half the default amount. /// Et cetera. -@"mouse-scroll-multiplier": f64 = 1.0, +@"mouse-scroll-multiplier": f64 = 3.0, /// The opacity level (opposite of transparency) of the background. A value of /// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 From c1e87e712211e098ea0f392aa1e25805d533a239 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 2 Mar 2025 08:35:25 -0600 Subject: [PATCH 4/6] scroll: only use multiplier for non-precision scrolls Precision scrolls don't require a multiplier to behave nicely. However, wheel scrolls feel extremely slow without one. We apply the multiplier to wheel scrolls only --- src/Surface.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 37b24160d..f75017053 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2432,6 +2432,12 @@ pub fn scrollCallback( try self.setSelection(null); } + // We never use a multiplier for precision scrolls. + const multiplier: f64 = if (scroll_mods.precision) + 1.0 + else + self.config.mouse_scroll_multiplier; + // If we're in alternate screen with alternate scroll enabled, then // we convert to cursor keys. This only happens if we're: // (1) alt screen (2) no explicit mouse reporting and (3) alt @@ -2459,7 +2465,7 @@ pub fn scrollCallback( }; }; // We multiple by the scroll multiplier when reporting arrows - const multiplied = y.multiplied(self.config.mouse_scroll_multiplier); + const multiplied = y.multiplied(multiplier); for (0..multiplied.magnitude()) |_| { self.io.queueMessage(.{ .write_stable = seq }, .locked); } @@ -2497,7 +2503,7 @@ pub fn scrollCallback( if (y.delta != 0) { // We multiply by the multiplier when scrolling the viewport - const multiplied = y.multiplied(self.config.mouse_scroll_multiplier); + const multiplied = y.multiplied(multiplier); // Modify our viewport, this requires a lock since it affects // rendering. We have to switch signs here because our delta // is negative down but our viewport is positive down. From 68a2478317edd1d8d982443bf032bb22de6d01d0 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 2 Mar 2025 08:36:47 -0600 Subject: [PATCH 5/6] gtk: enable non-discrete scrolling Remove the flag which reports all scrolls as discrete scrolls. This enables precision scrolling in GTK. We have to track a flag between continuous scroll events. --- src/apprt/gtk/Surface.zig | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 7d471ff75..ba2e4d244 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -304,6 +304,9 @@ cgroup_path: ?[]const u8 = null, /// Our context menu. context_menu: Menu(Surface, "context_menu", false), +/// True when we have a precision scroll in progress +precision_scroll: bool = false, + /// The state of the key event while we're doing IM composition. /// See gtkKeyPressed for detailed descriptions. pub const IMKeyEvent = enum { @@ -418,10 +421,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { c.gtk_widget_add_controller(@ptrCast(@alignCast(overlay)), ec_motion); // Scroll events - const ec_scroll = c.gtk_event_controller_scroll_new( - c.GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | - c.GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, - ); + const ec_scroll = c.gtk_event_controller_scroll_new(c.GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); errdefer c.g_object_unref(ec_scroll); c.gtk_widget_add_controller(@ptrCast(overlay), ec_scroll); @@ -532,6 +532,8 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { _ = c.g_signal_connect_data(ec_motion, "motion", c.G_CALLBACK(>kMouseMotion), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_motion, "leave", c.G_CALLBACK(>kMouseLeave), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_scroll, "scroll", c.G_CALLBACK(>kMouseScroll), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_scroll, "scroll-begin", c.G_CALLBACK(>kMouseScrollPrecisionBegin), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_scroll, "scroll-end", c.G_CALLBACK(>kMouseScrollPrecisionEnd), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "preedit-start", c.G_CALLBACK(>kInputPreeditStart), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(>kInputPreeditChanged), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "preedit-end", c.G_CALLBACK(>kInputPreeditEnd), self, null, c.G_CONNECT_DEFAULT); @@ -1523,6 +1525,22 @@ fn gtkMouseLeave( }; } +fn gtkMouseScrollPrecisionBegin( + _: *c.GtkEventControllerScroll, + ud: ?*anyopaque, +) callconv(.C) void { + const self = userdataSelf(ud.?); + self.precision_scroll = true; +} + +fn gtkMouseScrollPrecisionEnd( + _: *c.GtkEventControllerScroll, + ud: ?*anyopaque, +) callconv(.C) void { + const self = userdataSelf(ud.?); + self.precision_scroll = false; +} + fn gtkMouseScroll( _: *c.GtkEventControllerScroll, x: c.gdouble, @@ -1533,15 +1551,17 @@ fn gtkMouseScroll( const scaled = self.scaledCoordinates(x, y); // GTK doesn't support any of the scroll mods. - const scroll_mods: input.ScrollMods = .{}; + const scroll_mods: input.ScrollMods = .{ .precision = self.precision_scroll }; + // Multiply precision scrolls by 10 to get a better response from touchpad scrolling + const multiplier: f64 = if (self.precision_scroll) 10 else 1; self.core_surface.scrollCallback( // We invert because we apply natural scrolling to the values. // This behavior has existed for years without Linux users complaining // but I suspect we'll have to make this configurable in the future // or read a system setting. - scaled.x * -1, - scaled.y * -1, + scaled.x * -1 * multiplier, + scaled.y * -1 * multiplier, scroll_mods, ) catch |err| { log.err("error in scroll callback err={}", .{err}); From 30a49d0458809d660324edd36aa411756b0572ed Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 2 Mar 2025 08:46:11 -0600 Subject: [PATCH 6/6] fixup! config: default mouse-scroll-multiplier to 3.0 --- src/config/Config.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 589460a18..c2749b45c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -609,9 +609,7 @@ palette: Palette = .{}, /// than 0.01 or greater than 10,000 will be clamped to the nearest valid /// value. /// -/// A value of "1" (default) scrolls the default amount. A value of "2" scrolls -/// double the default amount. A value of "0.5" scrolls half the default amount. -/// Et cetera. +/// A value of "3" (default) scrolls 3 lines per tick. @"mouse-scroll-multiplier": f64 = 3.0, /// The opacity level (opposite of transparency) of the background. A value of