Merge pull request #163 from mitchellh/precision-scroll

Precision Scroll Devices (i.e. Magic Mouse) and Momentum-Based Scrolling
This commit is contained in:
Mitchell Hashimoto
2023-06-29 12:04:19 -07:00
committed by GitHub
7 changed files with 208 additions and 28 deletions

View File

@ -55,6 +55,21 @@ typedef enum {
GHOSTTY_MOUSE_MIDDLE,
} ghostty_input_mouse_button_e;
typedef enum {
GHOSTTY_MOUSE_MOMENTUM_NONE,
GHOSTTY_MOUSE_MOMENTUM_BEGAN,
GHOSTTY_MOUSE_MOMENTUM_STATIONARY,
GHOSTTY_MOUSE_MOMENTUM_CHANGED,
GHOSTTY_MOUSE_MOMENTUM_ENDED,
GHOSTTY_MOUSE_MOMENTUM_CANCELLED,
GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
} ghostty_input_mouse_momentum_e;
// This is a packed struct (see src/input/mouse.zig) but the C standard
// afaik doesn't let us reliably define packed structs so we build it up
// from scratch.
typedef int ghostty_input_scroll_mods_t;
typedef enum {
GHOSTTY_MODS_NONE = 0,
GHOSTTY_MODS_SHIFT = 1 << 0,
@ -265,7 +280,7 @@ void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_inpu
void ghostty_surface_char(ghostty_surface_t, uint32_t);
void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e);
void ghostty_surface_mouse_pos(ghostty_surface_t, double, double);
void ghostty_surface_mouse_scroll(ghostty_surface_t, double, double);
void ghostty_surface_mouse_scroll(ghostty_surface_t, double, double, ghostty_input_scroll_mods_t);
void ghostty_surface_ime_point(ghostty_surface_t, double *, double *);
void ghostty_surface_request_close(ghostty_surface_t);
void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e);

View File

@ -291,14 +291,44 @@ extension Ghostty {
override func scrollWheel(with event: NSEvent) {
guard let surface = self.surface else { return }
// Builds up the "input.ScrollMods" bitmask
var mods: Int32 = 0
var x = event.scrollingDeltaX
var y = event.scrollingDeltaY
if event.hasPreciseScrollingDeltas {
x *= 0.1
y *= 0.1
mods = 1
// We do a 2x speed multiplier. This is subjective, it "feels" better to me.
x *= 2;
y *= 2;
// TODO(mitchellh): do we have to scale the x/y here by window scale factor?
}
ghostty_surface_mouse_scroll(surface, x, y)
// Determine our momentum value
var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE
switch (event.momentumPhase) {
case .began:
momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN
case .stationary:
momentum = GHOSTTY_MOUSE_MOMENTUM_STATIONARY
case .changed:
momentum = GHOSTTY_MOUSE_MOMENTUM_CHANGED
case .ended:
momentum = GHOSTTY_MOUSE_MOMENTUM_ENDED
case .cancelled:
momentum = GHOSTTY_MOUSE_MOMENTUM_CANCELLED
case .mayBegin:
momentum = GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
default:
break
}
// Pack our momentum value into the mods bitmask
mods |= Int32(momentum.rawValue) << 1
ghostty_surface_mouse_scroll(surface, x, y, mods)
}
override func keyDown(with event: NSEvent) {

View File

@ -127,6 +127,10 @@ const Mouse = struct {
/// The last x/y sent for mouse reports.
event_point: terminal.point.Viewport = .{},
/// Pending scroll amounts for high-precision scrolls
pending_scroll_x: f64 = 0,
pending_scroll_y: f64 = 0,
};
/// The configuration that a surface has, this is copied from the main
@ -1268,7 +1272,12 @@ pub fn refreshCallback(self: *Surface) !void {
try self.queueRender();
}
pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) !void {
pub fn scrollCallback(
self: *Surface,
xoff: f64,
yoff: f64,
scroll_mods: input.ScrollMods,
) !void {
const tracy = trace(@src());
defer tracy.end();
@ -1283,18 +1292,82 @@ pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) !void {
} else |_| {}
}
// log.info("SCROLL: {} {}", .{ xoff, yoff });
// log.info("SCROLL: xoff={} yoff={} mods={}", .{ xoff, yoff, scroll_mods });
// Positive is up
const y_sign: isize = if (yoff > 0) -1 else 1;
const y_delta_unsigned: usize = if (yoff == 0) 0 else @max(@divFloor(self.grid_size.rows, 15), 1);
const y_delta: isize = y_sign * @intCast(isize, y_delta_unsigned);
const ScrollAmount = struct {
// Positive is up, right
sign: isize = 1,
delta_unsigned: usize = 0,
delta: isize = 0,
};
// Positive is right
const x_sign: isize = if (xoff < 0) -1 else 1;
const x_delta_unsigned: usize = if (xoff == 0) 0 else 1;
const x_delta: isize = x_sign * @intCast(isize, x_delta_unsigned);
log.info("scroll: delta_y={} delta_x={}", .{ y_delta, x_delta });
const y: ScrollAmount = if (yoff == 0) .{} else y: {
// Non-precision scrolling is easy to calculate.
if (!scroll_mods.precision) {
const y_sign: isize = if (yoff > 0) -1 else 1;
const y_delta_unsigned: usize = @max(@divFloor(self.grid_size.rows, 15), 1);
const y_delta: isize = y_sign * @intCast(isize, y_delta_unsigned);
break :y .{ .sign = y_sign, .delta_unsigned = y_delta_unsigned, .delta = y_delta };
}
// Precision scrolling is more complicated. We need to maintain state
// to build up a pending scroll amount if we're only scrolling by a
// tiny amount so that we can scroll by a full row when we have enough.
// Add our previously saved pending amount to the offset to get the
// new offset value.
//
// NOTE: we currently mutiply 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 * -1);
// If the new offset is less than a single unit of scroll, we save
// the new pending value and do not scroll yet.
const cell_size = self.cell_size.height;
if (@fabs(poff) < cell_size) {
self.mouse.pending_scroll_y = poff;
break :y .{};
}
// We scroll by the number of rows in the offset and save the remainder
const amount = poff / cell_size;
self.mouse.pending_scroll_y = poff - (amount * cell_size);
break :y .{
.delta_unsigned = @intFromFloat(usize, @fabs(amount)),
.delta = @intFromFloat(isize, amount),
};
};
// 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 = 1;
const x_delta: isize = x_sign * @intCast(isize, x_delta_unsigned);
break :x .{ .sign = x_sign, .delta_unsigned = x_delta_unsigned, .delta = x_delta };
}
const poff = self.mouse.pending_scroll_x + (xoff * -1);
const cell_size = self.cell_size.width;
if (@fabs(poff) < cell_size) {
self.mouse.pending_scroll_x = poff;
break :x .{};
}
const amount = poff / cell_size;
self.mouse.pending_scroll_x = poff - (amount * cell_size);
break :x .{
.delta_unsigned = @intFromFloat(usize, @fabs(amount)),
.delta = @intFromFloat(isize, amount),
};
};
log.info("scroll: delta_y={} delta_x={}", .{ y.delta, x.delta });
{
self.renderer_state.mutex.lock();
@ -1315,18 +1388,18 @@ pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) !void {
self.io.terminal.modes.mouse_event == .none and
self.io.terminal.modes.mouse_alternate_scroll)
{
if (y_delta_unsigned > 0) {
const seq = if (y_delta < 0) "\x1bOA" else "\x1bOB";
for (0..y_delta_unsigned) |_| {
if (y.delta_unsigned > 0) {
const seq = if (y.delta < 0) "\x1bOA" else "\x1bOB";
for (0..y.delta_unsigned) |_| {
_ = self.io_thread.mailbox.push(.{
.write_stable = seq,
}, .{ .forever = {} });
}
}
if (x_delta_unsigned > 0) {
const seq = if (x_delta < 0) "\x1bOC" else "\x1bOD";
for (0..x_delta_unsigned) |_| {
if (x.delta_unsigned > 0) {
const seq = if (x.delta < 0) "\x1bOC" else "\x1bOD";
for (0..x.delta_unsigned) |_| {
_ = self.io_thread.mailbox.push(.{
.write_stable = seq,
}, .{ .forever = {} });
@ -1343,7 +1416,7 @@ pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) !void {
// the normal logic.
// Modify our viewport, this requires a lock since it affects rendering
try self.io.terminal.scrollViewport(.{ .delta = y_delta });
try self.io.terminal.scrollViewport(.{ .delta = y.delta });
// If we're scrolling up or down, then send a mouse event. This requires
// a lock since we read terminal state.

View File

@ -299,8 +299,13 @@ pub const Surface = struct {
};
}
pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) void {
self.core_surface.scrollCallback(xoff, yoff) catch |err| {
pub fn scrollCallback(
self: *Surface,
xoff: f64,
yoff: f64,
mods: input.ScrollMods,
) void {
self.core_surface.scrollCallback(xoff, yoff, mods) catch |err| {
log.err("error in scroll callback err={}", .{err});
return;
};
@ -502,8 +507,17 @@ pub const CAPI = struct {
surface.cursorPosCallback(x, y);
}
export fn ghostty_surface_mouse_scroll(surface: *Surface, x: f64, y: f64) void {
surface.scrollCallback(x, y);
export fn ghostty_surface_mouse_scroll(
surface: *Surface,
x: f64,
y: f64,
scroll_mods: c_int,
) void {
surface.scrollCallback(
x,
y,
@bitCast(input.ScrollMods, @truncate(u8, @bitCast(c_uint, scroll_mods))),
);
}
export fn ghostty_surface_ime_point(surface: *Surface, x: *f64, y: *f64) void {

View File

@ -731,8 +731,11 @@ pub const Surface = struct {
const tracy = trace(@src());
defer tracy.end();
// Glfw doesn't support any of the scroll mods.
const scroll_mods: input.ScrollMods = .{};
const core_win = window.getUserPointer(CoreSurface) orelse return;
core_win.scrollCallback(xoff, yoff) catch |err| {
core_win.scrollCallback(xoff, yoff, scroll_mods) catch |err| {
log.err("error in scroll callback err={}", .{err});
return;
};

View File

@ -1049,7 +1049,11 @@ pub const Surface = struct {
ud: ?*anyopaque,
) callconv(.C) void {
const self = userdataSelf(ud.?);
self.core_surface.scrollCallback(x, y * -1) catch |err| {
// GTK doesn't support any of the scroll mods.
const scroll_mods: input.ScrollMods = .{};
self.core_surface.scrollCallback(x, y * -1, scroll_mods) catch |err| {
log.err("error in scroll callback err={}", .{err});
return;
};

View File

@ -1,3 +1,5 @@
const std = @import("std");
/// The state of a mouse button.
///
/// This is backed by a c_int so we can use this as-is for our embedding API.
@ -45,3 +47,42 @@ pub const MouseButton = enum(c_int) {
ten = 10,
eleven = 11,
};
/// The "momentum" of a mouse scroll event. This matches the macOS events
/// because it is the only reliable source right now of momentum events.
/// This is used to handle "intertial scrolling" (i.e. flicking).
///
/// https://developer.apple.com/documentation/appkit/nseventphase
pub const MouseMomentum = enum(u3) {
none = 0,
began = 1,
stationary = 2,
changed = 3,
ended = 4,
cancelled = 5,
may_begin = 6,
};
/// The bitmask for mods for scroll events.
pub const ScrollMods = packed struct(u8) {
/// True if this is a high-precision scroll event. For example, Apple
/// devices such as Magic Mouse, trackpads, etc. are high-precision
/// and send very detailed scroll events.
precision: bool = false,
/// The momentum phase (if available, supported) of the scroll event.
/// This is used to handle "intertial scrolling" (i.e. flicking).
momentum: MouseMomentum = .none,
_padding: u4 = 0,
// For our own understanding
test {
const testing = std.testing;
try testing.expectEqual(@bitCast(u8, ScrollMods{}), @as(u8, 0b0));
try testing.expectEqual(
@bitCast(u8, ScrollMods{ .precision = true }),
@as(u8, 0b0000_0001),
);
}
};