mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
core!: modify scroll behavior (#6052)
Modify the scroll behavior to better match other terminals, as well as provide a better overall experience. Before this PR, ghostty would scale non-precision scroll events dependent on the screen size. This is in line with kitty, but no other terminal. Ghostty also was the only terminal to send *more than one* wheel event. ``` # 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 | ``` This PR modifies Ghostty to behave like foot and alacritty. For an improved user experience, we only use the configured multiplier for non-precision scrolls. This multiplier now *only applies* to viewport scrolling and alternate scroll mode. The default value has been updated to 3.0. GTK also now supports precision scrolling.
This commit is contained in:
@ -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.
|
||||
@ -2348,22 +2355,11 @@ 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_isize: isize = @intFromFloat(@round(yoff));
|
||||
break :y .{ .delta = y_delta_isize };
|
||||
}
|
||||
|
||||
@ -2372,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
|
||||
@ -2404,15 +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(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_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;
|
||||
@ -2440,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
|
||||
@ -2466,7 +2464,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(multiplier);
|
||||
for (0..multiplied.magnitude()) |_| {
|
||||
self.io.queueMessage(.{ .write_stable = seq }, .locked);
|
||||
}
|
||||
}
|
||||
@ -2502,10 +2502,12 @@ pub fn scrollCallback(
|
||||
}
|
||||
|
||||
if (y.delta != 0) {
|
||||
// We multiply by the multiplier when scrolling the viewport
|
||||
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.
|
||||
try self.io.terminal.scrollViewport(.{ .delta = y.delta * -1 });
|
||||
try self.io.terminal.scrollViewport(.{ .delta = multiplied.delta * -1 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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});
|
||||
|
@ -609,10 +609,8 @@ 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.
|
||||
@"mouse-scroll-multiplier": f64 = 1.0,
|
||||
/// 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
|
||||
/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0
|
||||
|
Reference in New Issue
Block a user