Merge pull request #2431 from ghostty-org/push-xrvpptonzowr

core: simplify scroll math, fix horizontal scroll direction on macOS
This commit is contained in:
Mitchell Hashimoto
2024-10-10 16:46:26 -07:00
committed by GitHub
2 changed files with 89 additions and 49 deletions

View File

@ -2030,6 +2030,27 @@ pub fn refreshCallback(self: *Surface) !void {
try self.queueRender();
}
// The amount to scroll. This structure is always normalized so that
// negative is down, left and positive is up, right. Note that INTERNALLY,
// vertical scroll on our terminal uses positive for down (right is not
// supported by our screen since scrollback is only vertical).
const ScrollAmount = struct {
delta: isize = 0,
pub fn direction(self: ScrollAmount) enum { down_left, up_right } {
return if (self.delta < 0) .down_left else .up_right;
}
pub fn magnitude(self: ScrollAmount) usize {
return @abs(self.delta);
}
};
/// Mouse scroll event. Negative is down, left. Positive is up, right.
///
/// "Natural scrolling" is a macOS term for inverting the scroll direction.
/// This should be handled by the apprt implementation. At this layer,
/// negative is always down, left.
pub fn scrollCallback(
self: *Surface,
xoff: f64,
@ -2045,22 +2066,23 @@ pub fn scrollCallback(
// Always show the mouse again if it is hidden
if (self.mouse.hidden) self.showMouse();
const ScrollAmount = struct {
// Positive is up, right
sign: isize = 1,
delta_unsigned: usize = 0,
delta: isize = 0,
};
const y: ScrollAmount = if (yoff == 0) .{} else y: {
// Non-precision scrolling is easy to calculate.
// 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.
if (!scroll_mods.precision) {
const y_sign: isize = if (yoff > 0) -1 else 1;
const grid_rows: f64 = @floatFromInt(self.grid_size.rows);
const y_delta_f64 = @round((grid_rows * self.config.mouse_scroll_multiplier) / 15.0);
const y_delta_unsigned: usize = @max(1, @as(usize, @intFromFloat(y_delta_f64)));
const y_delta: isize = y_sign * @as(isize, @intCast(y_delta_unsigned));
break :y .{ .sign = y_sign, .delta_unsigned = y_delta_unsigned, .delta = y_delta };
// Calculate our magnitude of scroll. This is constant (not
// dependent on yoff).
const grid_rows_f64: f64 = @floatFromInt(self.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 };
}
// Precision scrolling is more complicated. We need to maintain state
@ -2068,17 +2090,14 @@ 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 = yoff * self.config.mouse_scroll_multiplier;
const yoff_adjusted: f64 = yoff * self.config.mouse_scroll_multiplier;
// Add our previously saved pending amount to the offset to get the
// new offset value.
//
// NOTE: we currently multiply by -1 because macOS sends the opposite
// of what we expect. This is jank we should audit our sign usage and
// carefully document what we expect so this can work cross platform.
// Right now this isn't important because macOS is the only high-precision
// scroller.
const poff = self.mouse.pending_scroll_y + (yoff_adjusted * -1);
// new offset value. The signs of the pending and yoff should match
// so that we move further away from zero, but we don't assert
// this because in theory a user could scroll in the opposite
// direction and undo a pending scroll.
const poff: f64 = self.mouse.pending_scroll_y + yoff_adjusted;
// If the new offset is less than a single unit of scroll, we save
// the new pending value and do not scroll yet.
@ -2090,26 +2109,28 @@ pub fn scrollCallback(
// We scroll by the number of rows in the offset and save the remainder
const amount = poff / cell_size;
assert(@abs(amount) >= 1);
self.mouse.pending_scroll_y = poff - (amount * cell_size);
break :y .{
.sign = if (yoff_adjusted > 0) 1 else -1,
.delta_unsigned = @intFromFloat(@abs(amount)),
.delta = @intFromFloat(amount),
};
// Round towards zero.
const delta: isize = @intFromFloat(@trunc(amount));
assert(@abs(delta) >= 1);
break :y .{ .delta = delta };
};
// For detailed comments see the y calculation above.
const x: ScrollAmount = if (xoff == 0) .{} else x: {
if (!scroll_mods.precision) {
const x_sign: isize = if (xoff < 0) -1 else 1;
const x_delta_unsigned: usize = @intFromFloat(@round(1 * self.config.mouse_scroll_multiplier));
const x_delta: isize = x_sign * @as(isize, @intCast(x_delta_unsigned));
break :x .{ .sign = x_sign, .delta_unsigned = x_delta_unsigned, .delta = x_delta };
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));
break :x .{ .delta = x_delta_isize };
}
const xoff_adjusted = xoff * self.config.mouse_scroll_multiplier;
const poff = self.mouse.pending_scroll_x + (xoff_adjusted * -1);
const xoff_adjusted: f64 = xoff * self.config.mouse_scroll_multiplier;
const poff: f64 = self.mouse.pending_scroll_x + xoff_adjusted;
const cell_size: f64 = @floatFromInt(self.cell_size.width);
if (@abs(poff) < cell_size) {
self.mouse.pending_scroll_x = poff;
@ -2117,15 +2138,14 @@ pub fn scrollCallback(
}
const amount = poff / cell_size;
assert(@abs(amount) >= 1);
self.mouse.pending_scroll_x = poff - (amount * cell_size);
break :x .{
.delta_unsigned = @intFromFloat(@abs(amount)),
.delta = @intFromFloat(amount),
};
const delta: isize = @intFromFloat(@trunc(amount));
assert(@abs(delta) >= 1);
break :x .{ .delta = delta };
};
// log.info("scroll: delta_y={} delta_x={}", .{ y.delta, x.delta });
// log.info("SCROLL: delta_y={} delta_x={}", .{ y.delta, x.delta });
{
self.renderer_state.mutex.lock();
@ -2146,19 +2166,25 @@ pub fn scrollCallback(
self.io.terminal.flags.mouse_event == .none and
self.io.terminal.modes.get(.mouse_alternate_scroll))
{
if (y.delta_unsigned > 0) {
if (y.delta != 0) {
// When we send mouse events as cursor keys we always
// clear the selection.
try self.setSelection(null);
const seq = if (self.io.terminal.modes.get(.cursor_keys)) seq: {
// cursor key: application mode
break :seq if (y.delta < 0) "\x1bOA" else "\x1bOB";
break :seq switch (y.direction()) {
.up_right => "\x1bOA",
.down_left => "\x1bOB",
};
} else seq: {
// cursor key: normal mode
break :seq if (y.delta < 0) "\x1b[A" else "\x1b[B";
break :seq switch (y.direction()) {
.up_right => "\x1b[A",
.down_left => "\x1b[B",
};
};
for (0..y.delta_unsigned) |_| {
for (0..y.magnitude()) |_| {
self.io.queueMessage(.{ .write_stable = seq }, .locked);
}
}
@ -2174,12 +2200,18 @@ pub fn scrollCallback(
if (self.io.terminal.flags.mouse_event != .none) {
if (y.delta != 0) {
const pos = try self.rt_surface.getCursorPos();
try self.mouseReport(if (y.delta < 0) .four else .five, .press, self.mouse.mods, pos);
try self.mouseReport(switch (y.direction()) {
.up_right => .four,
.down_left => .five,
}, .press, self.mouse.mods, pos);
}
if (x.delta != 0) {
const pos = try self.rt_surface.getCursorPos();
try self.mouseReport(if (x.delta > 0) .six else .seven, .press, self.mouse.mods, pos);
try self.mouseReport(switch (x.direction()) {
.up_right => .six,
.down_left => .seven,
}, .press, self.mouse.mods, pos);
}
// If mouse reporting is on, we do not want to scroll the
@ -2187,8 +2219,12 @@ pub fn scrollCallback(
return;
}
// Modify our viewport, this requires a lock since it affects rendering
try self.io.terminal.scrollViewport(.{ .delta = y.delta });
if (y.delta != 0) {
// 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.queueRender();

View File

@ -1379,7 +1379,11 @@ fn gtkMouseScroll(
const scroll_mods: input.ScrollMods = .{};
self.core_surface.scrollCallback(
scaled.x,
// 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,
scroll_mods,
) catch |err| {