From 20f46d5c082c725cd4e096407c9efb6cc5f62bf4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Jun 2023 10:38:15 -0700 Subject: [PATCH 1/5] define precision scrolling and momentum structs, C API --- include/ghostty.h | 17 +++++++++- macos/Sources/Ghostty/SurfaceView.swift | 2 +- src/apprt/embedded.zig | 8 ++++- src/input/mouse.zig | 41 +++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 77259202a..163044c1b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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); diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 1655eb849..1d48c1597 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -298,7 +298,7 @@ extension Ghostty { y *= 0.1 } - ghostty_surface_mouse_scroll(surface, x, y) + ghostty_surface_mouse_scroll(surface, x, y, 0) } override func keyDown(with event: NSEvent) { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 36e7d3e77..cc171b3b0 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -502,7 +502,13 @@ pub const CAPI = struct { surface.cursorPosCallback(x, y); } - export fn ghostty_surface_mouse_scroll(surface: *Surface, x: f64, y: f64) void { + export fn ghostty_surface_mouse_scroll( + surface: *Surface, + x: f64, + y: f64, + scroll_mods: c_int, + ) void { + _ = scroll_mods; surface.scrollCallback(x, y); } diff --git a/src/input/mouse.zig b/src/input/mouse.zig index 224abef35..2073cbb18 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -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), + ); + } +}; From 68631a1ccde79cbe7e713b1a0e07551351cfa7bd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Jun 2023 10:41:42 -0700 Subject: [PATCH 2/5] apprt: plumb through scroll mods to core, don't handle them yet --- src/Surface.zig | 9 ++++++++- src/apprt/embedded.zig | 16 ++++++++++++---- src/apprt/glfw.zig | 5 ++++- src/apprt/gtk.zig | 6 +++++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 0e910a768..dc40e3920 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1268,7 +1268,14 @@ 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 { + _ = scroll_mods; + const tracy = trace(@src()); defer tracy.end(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index cc171b3b0..9dd835e52 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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; }; @@ -508,8 +513,11 @@ pub const CAPI = struct { y: f64, scroll_mods: c_int, ) void { - _ = scroll_mods; - surface.scrollCallback(x, y); + 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 { diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 619df6a9d..ad90b4362 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -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; }; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 05e43cee3..0548e33dd 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -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; }; From 9a40dc4630b6cbe5ae475e3b7349368edaf5b9f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Jun 2023 11:06:34 -0700 Subject: [PATCH 3/5] macos: build scroll mods value --- macos/Sources/Ghostty/SurfaceView.swift | 28 ++++++++++++++++++++++++- src/Surface.zig | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 1d48c1597..566aa15d4 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -291,14 +291,40 @@ 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 { + mods = 1 x *= 0.1 y *= 0.1 } - ghostty_surface_mouse_scroll(surface, x, y, 0) + // 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) { diff --git a/src/Surface.zig b/src/Surface.zig index dc40e3920..4e8a2201b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1290,7 +1290,7 @@ pub fn scrollCallback( } 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; From 32031701b83b477dae4703efda3a2786e9d3a2da Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Jun 2023 11:42:43 -0700 Subject: [PATCH 4/5] core: do high precision scrolling Y calculations --- macos/Sources/Ghostty/SurfaceView.swift | 4 +- src/Surface.zig | 67 +++++++++++++++++++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 566aa15d4..fd239ab7b 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -298,8 +298,8 @@ extension Ghostty { var y = event.scrollingDeltaY if event.hasPreciseScrollingDeltas { mods = 1 - x *= 0.1 - y *= 0.1 + + // TODO(mitchellh): do we have to scale the x/y here? } // Determine our momentum value diff --git a/src/Surface.zig b/src/Surface.zig index 4e8a2201b..7afb469ae 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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 @@ -1274,8 +1278,6 @@ pub fn scrollCallback( yoff: f64, scroll_mods: input.ScrollMods, ) !void { - _ = scroll_mods; - const tracy = trace(@src()); defer tracy.end(); @@ -1292,16 +1294,59 @@ pub fn scrollCallback( // 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, + }; + + 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), + }; + }; // 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 }); + log.info("scroll: delta_y={} delta_x={}", .{ y.delta, x_delta }); { self.renderer_state.mutex.lock(); @@ -1322,9 +1367,9 @@ pub fn scrollCallback( 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 = {} }); @@ -1350,7 +1395,7 @@ pub fn scrollCallback( // 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. From de5468e2144bf6cc05575c58e0bf0358aa463e85 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Jun 2023 11:47:28 -0700 Subject: [PATCH 5/5] scale x, speed up scrolling --- macos/Sources/Ghostty/SurfaceView.swift | 6 +++- src/Surface.zig | 37 +++++++++++++++++++------ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index fd239ab7b..48467e264 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -299,7 +299,11 @@ extension Ghostty { if event.hasPreciseScrollingDeltas { mods = 1 - // TODO(mitchellh): do we have to scale the x/y here? + // 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? } // Determine our momentum value diff --git a/src/Surface.zig b/src/Surface.zig index 7afb469ae..6cf4e28c4 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1342,11 +1342,32 @@ pub fn scrollCallback( }; }; - // 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 }); + // 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(); @@ -1376,9 +1397,9 @@ pub fn scrollCallback( } } - 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 = {} });