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 {
|
pub fn magnitude(self: ScrollAmount) usize {
|
||||||
return @abs(self.delta);
|
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.
|
/// Mouse scroll event. Negative is down, left. Positive is up, right.
|
||||||
@ -2348,22 +2355,11 @@ pub fn scrollCallback(
|
|||||||
if (self.mouse.hidden) self.showMouse();
|
if (self.mouse.hidden) self.showMouse();
|
||||||
|
|
||||||
const y: ScrollAmount = if (yoff == 0) .{} else y: {
|
const y: ScrollAmount = if (yoff == 0) .{} else y: {
|
||||||
// Non-precision scrolling is easy to calculate. We don't use
|
// Non-precision scrolls don't accumulate. We cast that raw yoff to an isize and interpret
|
||||||
// the given offset at all and instead just treat a positive
|
// it as the number of lines to scroll.
|
||||||
// as a scroll up and a negative as a scroll down and scroll in
|
|
||||||
// steps.
|
|
||||||
if (!scroll_mods.precision) {
|
if (!scroll_mods.precision) {
|
||||||
// Calculate our magnitude of scroll. This is constant (not
|
// Calculate our magnitude of scroll. This is a direct multiple of yoff
|
||||||
// dependent on yoff).
|
const y_delta_isize: isize = @intFromFloat(@round(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));
|
|
||||||
|
|
||||||
break :y .{ .delta = y_delta_isize };
|
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.
|
// tiny amount so that we can scroll by a full row when we have enough.
|
||||||
|
|
||||||
// Adjust our offset by the multiplier
|
// 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
|
// Add our previously saved pending amount to the offset to get the
|
||||||
// new offset value. The signs of the pending and yoff should match
|
// 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.
|
// For detailed comments see the y calculation above.
|
||||||
const x: ScrollAmount = if (xoff == 0) .{} else x: {
|
const x: ScrollAmount = if (xoff == 0) .{} else x: {
|
||||||
if (!scroll_mods.precision) {
|
if (!scroll_mods.precision) {
|
||||||
const x_delta_f64: f64 = @round(1 * self.config.mouse_scroll_multiplier);
|
const x_delta_isize: isize = @intFromFloat(@round(xoff));
|
||||||
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));
|
|
||||||
break :x .{ .delta = x_delta_isize };
|
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;
|
||||||
const poff: f64 = self.mouse.pending_scroll_x + xoff_adjusted;
|
|
||||||
const cell_size: f64 = @floatFromInt(self.size.cell.width);
|
const cell_size: f64 = @floatFromInt(self.size.cell.width);
|
||||||
if (@abs(poff) < cell_size) {
|
if (@abs(poff) < cell_size) {
|
||||||
self.mouse.pending_scroll_x = poff;
|
self.mouse.pending_scroll_x = poff;
|
||||||
@ -2440,6 +2432,12 @@ pub fn scrollCallback(
|
|||||||
try self.setSelection(null);
|
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
|
// If we're in alternate screen with alternate scroll enabled, then
|
||||||
// we convert to cursor keys. This only happens if we're:
|
// we convert to cursor keys. This only happens if we're:
|
||||||
// (1) alt screen (2) no explicit mouse reporting and (3) alt
|
// (1) alt screen (2) no explicit mouse reporting and (3) alt
|
||||||
@ -2466,7 +2464,9 @@ pub fn scrollCallback(
|
|||||||
.down_left => "\x1b[B",
|
.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);
|
self.io.queueMessage(.{ .write_stable = seq }, .locked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2502,10 +2502,12 @@ pub fn scrollCallback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (y.delta != 0) {
|
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
|
// Modify our viewport, this requires a lock since it affects
|
||||||
// rendering. We have to switch signs here because our delta
|
// rendering. We have to switch signs here because our delta
|
||||||
// is negative down but our viewport is positive down.
|
// 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.
|
/// Our context menu.
|
||||||
context_menu: Menu(Surface, "context_menu", false),
|
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.
|
/// The state of the key event while we're doing IM composition.
|
||||||
/// See gtkKeyPressed for detailed descriptions.
|
/// See gtkKeyPressed for detailed descriptions.
|
||||||
pub const IMKeyEvent = enum {
|
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);
|
c.gtk_widget_add_controller(@ptrCast(@alignCast(overlay)), ec_motion);
|
||||||
|
|
||||||
// Scroll events
|
// Scroll events
|
||||||
const ec_scroll = c.gtk_event_controller_scroll_new(
|
const ec_scroll = c.gtk_event_controller_scroll_new(c.GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES);
|
||||||
c.GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES |
|
|
||||||
c.GTK_EVENT_CONTROLLER_SCROLL_DISCRETE,
|
|
||||||
);
|
|
||||||
errdefer c.g_object_unref(ec_scroll);
|
errdefer c.g_object_unref(ec_scroll);
|
||||||
c.gtk_widget_add_controller(@ptrCast(overlay), 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, "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_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", 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-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-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);
|
_ = 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(
|
fn gtkMouseScroll(
|
||||||
_: *c.GtkEventControllerScroll,
|
_: *c.GtkEventControllerScroll,
|
||||||
x: c.gdouble,
|
x: c.gdouble,
|
||||||
@ -1533,15 +1551,17 @@ fn gtkMouseScroll(
|
|||||||
const scaled = self.scaledCoordinates(x, y);
|
const scaled = self.scaledCoordinates(x, y);
|
||||||
|
|
||||||
// GTK doesn't support any of the scroll mods.
|
// 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(
|
self.core_surface.scrollCallback(
|
||||||
// We invert because we apply natural scrolling to the values.
|
// We invert because we apply natural scrolling to the values.
|
||||||
// This behavior has existed for years without Linux users complaining
|
// This behavior has existed for years without Linux users complaining
|
||||||
// but I suspect we'll have to make this configurable in the future
|
// but I suspect we'll have to make this configurable in the future
|
||||||
// or read a system setting.
|
// or read a system setting.
|
||||||
scaled.x * -1,
|
scaled.x * -1 * multiplier,
|
||||||
scaled.y * -1,
|
scaled.y * -1 * multiplier,
|
||||||
scroll_mods,
|
scroll_mods,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err("error in scroll callback err={}", .{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
|
/// than 0.01 or greater than 10,000 will be clamped to the nearest valid
|
||||||
/// value.
|
/// value.
|
||||||
///
|
///
|
||||||
/// A value of "1" (default) scrolls the default amount. A value of "2" scrolls
|
/// A value of "3" (default) scrolls 3 lines per tick.
|
||||||
/// double the default amount. A value of "0.5" scrolls half the default amount.
|
@"mouse-scroll-multiplier": f64 = 3.0,
|
||||||
/// Et cetera.
|
|
||||||
@"mouse-scroll-multiplier": f64 = 1.0,
|
|
||||||
|
|
||||||
/// The opacity level (opposite of transparency) of the background. A value of
|
/// 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
|
/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0
|
||||||
|
Reference in New Issue
Block a user