aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/surface_mouse.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/surface_mouse.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/surface_mouse.zig')
-rw-r--r--vendor/libghostty-vt/src/surface_mouse.zig336
1 files changed, 336 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/surface_mouse.zig b/vendor/libghostty-vt/src/surface_mouse.zig
new file mode 100644
index 00000000..8fa53d24
--- /dev/null
+++ b/vendor/libghostty-vt/src/surface_mouse.zig
@@ -0,0 +1,336 @@
+/// SurfaceMouse represents mouse helper functionality for the core surface.
+///
+/// It's currently small in scope; its purpose is to isolate mouse logic that
+/// has gotten a bit complex (e.g. pointer shape handling for key events), but
+/// the intention is to grow it later so that we can better test said logic).
+const SurfaceMouse = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const input = @import("input.zig");
+const terminal = @import("terminal/main.zig");
+const MouseShape = terminal.MouseShape;
+
+/// For processing key events; the key that was physically pressed on the
+/// keyboard.
+physical_key: input.Key,
+
+/// The mouse event tracking mode, if any.
+mouse_event: terminal.MouseEvent,
+
+/// The current terminal's mouse shape.
+mouse_shape: MouseShape,
+
+/// The last mods state when the last mouse button (whatever it was) was
+/// pressed or release.
+mods: input.Mods,
+
+/// True if the mouse position is currently over a link.
+over_link: bool,
+
+/// True if the mouse pointer is currently hidden.
+hidden: bool,
+
+/// Translates key state to mouse shape (cursor) state, based on a state
+/// machine.
+///
+/// There are 4 current states:
+///
+/// * text: starting state, displays a text bar.
+/// * default: default state when in a mouse tracking mode. (e.g. vim, etc).
+/// Displays an arrow pointer.
+/// * pointer: default state when over a link. Displays a pointing finger.
+/// * crosshair: any above state can transition to this when the rectangle
+/// select keys are pressed (ctrl/super+alt).
+///
+/// Additionally, default can transition back to text if one of the shift keys
+/// are pressed during mouse tracking mode.
+///
+/// Any secondary state transitions back to its default state when the
+/// appropriate keys are released.
+///
+/// null is returned when the mouse shape does not need changing.
+pub fn keyToMouseShape(self: SurfaceMouse) ?MouseShape {
+ // Filter for appropriate key events
+ if (!eligibleMouseShapeKeyEvent(self.physical_key)) return null;
+
+ // Exceptions: link hover or hidden state overrides any other shape
+ // processing and does not change state.
+ //
+ // TODO: As we unravel mouse state, we can fix this to be more explicit.
+ if (self.over_link or self.hidden) {
+ return null;
+ }
+
+ // Set our current default state
+ var current_shape_state: MouseShape = undefined;
+ if (self.mouse_event != .none) {
+ // In mouse tracking mode, should be default (arrow pointer)
+ current_shape_state = .default;
+ } else {
+ // Default terminal mode, should be text (text bar)
+ current_shape_state = .text;
+ }
+
+ // Transition table.
+ //
+ // TODO: This could be updated eventually to be a true transition table if
+ // we move to a full stateful mouse surface, e.g. very specific inputs
+ // transitioning state based on previous state, versus flags like "is the
+ // mouse over a link", etc.
+ switch (current_shape_state) {
+ .default => {
+ if (isMouseModeOverrideState(self.mods) and isRectangleSelectState(self.mods)) {
+ // Crosshair (rectangle select), only set if we are also
+ // overriding (e.g. shift+ctrl+alt)
+ return .crosshair;
+ } else if (isMouseModeOverrideState(self.mods)) {
+ // Normal override state
+ return .text;
+ } else {
+ return .default;
+ }
+ },
+
+ .text => {
+ if (isRectangleSelectState(self.mods)) {
+ // Crosshair (rectangle select)
+ return .crosshair;
+ } else {
+ return .text;
+ }
+ },
+
+ // Fall back on default state
+ else => unreachable,
+ }
+}
+
+fn eligibleMouseShapeKeyEvent(physical_key: input.Key) bool {
+ return physical_key.ctrlOrSuper() or
+ physical_key.leftOrRightShift() or
+ physical_key.leftOrRightAlt();
+}
+
+fn isMouseModeOverrideState(mods: input.Mods) bool {
+ return mods.shift;
+}
+
+/// Returns true if our modifiers put us in a state where dragging
+/// should cause a rectangle select.
+pub fn isRectangleSelectState(mods: input.Mods) bool {
+ return if (comptime builtin.target.os.tag.isDarwin())
+ mods.alt
+ else
+ mods.ctrlOrSuper() and mods.alt;
+}
+
+test "keyToMouseShape" {
+ const testing = std.testing;
+
+ {
+ // No specific key pressed
+ const m: SurfaceMouse = .{
+ .physical_key = .unidentified,
+ .mouse_event = .none,
+ .mouse_shape = .progress,
+ .mods = .{},
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const got = m.keyToMouseShape();
+ try testing.expect(got == null);
+ }
+
+ {
+ // Over a link. NOTE: This tests that we don't touch the inbound state,
+ // not necessarily if we're over a link.
+ const m: SurfaceMouse = .{
+ .physical_key = .shift_left,
+ .mouse_event = .none,
+ .mouse_shape = .progress,
+ .mods = .{},
+ .over_link = true,
+ .hidden = false,
+ };
+
+ const got = m.keyToMouseShape();
+ try testing.expect(got == null);
+ }
+
+ {
+ // Mouse is currently hidden
+ const m: SurfaceMouse = .{
+ .physical_key = .shift_left,
+ .mouse_event = .none,
+ .mouse_shape = .progress,
+ .mods = .{},
+ .over_link = true,
+ .hidden = true,
+ };
+
+ const got = m.keyToMouseShape();
+ try testing.expect(got == null);
+ }
+
+ {
+ // default, no mods (mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .shift_left,
+ .mouse_event = .x10,
+ .mouse_shape = .default,
+ .mods = .{},
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .default;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // default -> crosshair (mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .alt_left,
+ .mouse_event = .x10,
+ .mouse_shape = .default,
+ .mods = .{ .ctrl = true, .super = true, .alt = true, .shift = true },
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .crosshair;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // default -> text (mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .shift_left,
+ .mouse_event = .x10,
+ .mouse_shape = .default,
+ .mods = .{ .shift = true },
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .text;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // crosshair -> text (mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .alt_left,
+ .mouse_event = .x10,
+ .mouse_shape = .crosshair,
+ .mods = .{ .shift = true },
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .text;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // crosshair -> default (mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .alt_left,
+ .mouse_event = .x10,
+ .mouse_shape = .crosshair,
+ .mods = .{},
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .default;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // text -> crosshair (mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .alt_left,
+ .mouse_event = .x10,
+ .mouse_shape = .text,
+ .mods = .{ .ctrl = true, .super = true, .alt = true, .shift = true },
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .crosshair;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // text -> default (mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .shift_left,
+ .mouse_event = .x10,
+ .mouse_shape = .text,
+ .mods = .{},
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .default;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // text, no mods (no mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .shift_left,
+ .mouse_event = .none,
+ .mouse_shape = .text,
+ .mods = .{},
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .text;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // text -> crosshair (no mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .alt_left,
+ .mouse_event = .none,
+ .mouse_shape = .text,
+ .mods = .{ .ctrl = true, .super = true, .alt = true },
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .crosshair;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+
+ {
+ // crosshair -> text (no mouse tracking)
+ const m: SurfaceMouse = .{
+ .physical_key = .alt_left,
+ .mouse_event = .none,
+ .mouse_shape = .crosshair,
+ .mods = .{},
+ .over_link = false,
+ .hidden = false,
+ };
+
+ const want: MouseShape = .text;
+ const got = m.keyToMouseShape();
+ try testing.expect(want == got);
+ }
+}