mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
Merge pull request #163 from mitchellh/precision-scroll
Precision Scroll Devices (i.e. Magic Mouse) and Momentum-Based Scrolling
This commit is contained in:
@ -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);
|
||||
|
@ -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) {
|
||||
|
109
src/Surface.zig
109
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
|
||||
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user