mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #979 from vancluever/vancluever-rect-crosshair
Surface: set crosshair, change event processing logic for mouse tracking
This commit is contained in:
@ -37,6 +37,7 @@ const input = @import("input.zig");
|
||||
const App = @import("App.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
const inspector = @import("inspector/main.zig");
|
||||
const SurfaceMouse = @import("surface_mouse.zig");
|
||||
|
||||
const log = std.log.scoped(.surface);
|
||||
|
||||
@ -1285,23 +1286,16 @@ pub fn keyCallback(
|
||||
if (rehide) self.hideMouse();
|
||||
}
|
||||
|
||||
// When we are in the middle of a mouse event and we press shift,
|
||||
// we change the mouse to a text shape so that selection appears
|
||||
// possible.
|
||||
if (self.io.terminal.flags.mouse_event != .none and
|
||||
event.physical_key == .left_shift or
|
||||
event.physical_key == .right_shift)
|
||||
{
|
||||
switch (event.action) {
|
||||
.press => if (!self.mouse.over_link) {
|
||||
// If the cursor is over a link then the pointer shape takes
|
||||
// priority
|
||||
try self.rt_surface.setMouseShape(.text);
|
||||
},
|
||||
.release => try self.rt_surface.setMouseShape(self.io.terminal.mouse_shape),
|
||||
.repeat => {},
|
||||
}
|
||||
}
|
||||
// Process the cursor state logic. This will update the cursor shape if
|
||||
// needed, depending on the key state.
|
||||
if ((SurfaceMouse{
|
||||
.physical_key = event.physical_key,
|
||||
.mouse_event = self.io.terminal.flags.mouse_event,
|
||||
.mouse_shape = self.io.terminal.mouse_shape,
|
||||
.mods = self.mouse.mods,
|
||||
.over_link = self.mouse.over_link,
|
||||
}).keyToMouseShape()) |shape|
|
||||
try self.rt_surface.setMouseShape(shape);
|
||||
|
||||
// No binding, so we have to perform an encoding task. This
|
||||
// may still result in no encoding. Under different modes and
|
||||
@ -2260,17 +2254,6 @@ pub fn cursorPosCallback(
|
||||
}
|
||||
}
|
||||
|
||||
// Checks to see if super is on in mods (MacOS) or ctrl. We use this for
|
||||
// rectangle select along with alt.
|
||||
//
|
||||
// Not to be confused with ctrlOrSuper in Config.
|
||||
fn ctrlOrSuper(mods: input.Mods) bool {
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
return mods.super;
|
||||
}
|
||||
return mods.ctrl;
|
||||
}
|
||||
|
||||
/// Double-click dragging moves the selection one "word" at a time.
|
||||
fn dragLeftClickDouble(
|
||||
self: *Surface,
|
||||
@ -2385,7 +2368,7 @@ fn dragLeftClickSingle(
|
||||
self.setSelection(if (selected) .{
|
||||
.start = screen_point,
|
||||
.end = screen_point,
|
||||
.rectangle = ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt,
|
||||
.rectangle = self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
|
||||
} else null);
|
||||
|
||||
return;
|
||||
@ -2432,7 +2415,7 @@ fn dragLeftClickSingle(
|
||||
self.setSelection(.{
|
||||
.start = start,
|
||||
.end = screen_point,
|
||||
.rectangle = ctrlOrSuper(self.mouse.mods) and self.mouse.mods.alt,
|
||||
.rectangle = self.mouse.mods.ctrlOrSuper() and self.mouse.mods.alt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -2491,7 +2474,7 @@ fn dragLeftClickBefore(
|
||||
click_point: terminal.point.ScreenPoint,
|
||||
mods: input.Mods,
|
||||
) bool {
|
||||
if (ctrlOrSuper(mods) and mods.alt) {
|
||||
if (mods.ctrlOrSuper() and mods.alt) {
|
||||
return screen_point.x < click_point.x;
|
||||
}
|
||||
|
||||
|
@ -881,23 +881,23 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
// Fonts
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .equal, .mods = ctrlOrSuper(.{}) },
|
||||
.{ .key = .equal, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||
.{ .increase_font_size = 1 },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .minus, .mods = ctrlOrSuper(.{}) },
|
||||
.{ .key = .minus, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||
.{ .decrease_font_size = 1 },
|
||||
);
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .zero, .mods = ctrlOrSuper(.{}) },
|
||||
.{ .key = .zero, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||
.{ .reset_font_size = {} },
|
||||
);
|
||||
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .j, .mods = ctrlOrSuper(.{ .shift = true }) },
|
||||
.{ .key = .j, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
||||
.{ .write_scrollback_file = {} },
|
||||
);
|
||||
|
||||
@ -1098,14 +1098,14 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
// Toggle fullscreen
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .enter, .mods = ctrlOrSuper(.{}) },
|
||||
.{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||
.{ .toggle_fullscreen = {} },
|
||||
);
|
||||
|
||||
// Toggle zoom a split
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .enter, .mods = ctrlOrSuper(.{ .shift = true }) },
|
||||
.{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
||||
.{ .toggle_split_zoom = {} },
|
||||
);
|
||||
|
||||
@ -1289,21 +1289,6 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// This sets either "ctrl" or "super" to true (but not both)
|
||||
/// on mods depending on if the build target is Mac or not. On
|
||||
/// Mac, we default to super (i.e. super+c for copy) and on
|
||||
/// non-Mac we default to ctrl (i.e. ctrl+c for copy).
|
||||
fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods {
|
||||
var copy = mods;
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
copy.super = true;
|
||||
} else {
|
||||
copy.ctrl = true;
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Load configuration from an iterator that yields values that look like
|
||||
/// command-line arguments, i.e. `--key=value`.
|
||||
pub fn loadIter(
|
||||
|
@ -145,6 +145,14 @@ pub const Mods = packed struct(Mods.Backing) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Checks to see if super is on (MacOS) or ctrl.
|
||||
pub fn ctrlOrSuper(self: Mods) bool {
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
return self.super;
|
||||
}
|
||||
return self.ctrl;
|
||||
}
|
||||
|
||||
// For our own understanding
|
||||
test {
|
||||
const testing = std.testing;
|
||||
@ -607,6 +615,26 @@ pub const Key = enum(c_int) {
|
||||
=> null,
|
||||
};
|
||||
}
|
||||
|
||||
/// true if this key is one of the left or right versions of super (MacOS)
|
||||
/// or ctrl.
|
||||
pub fn ctrlOrSuper(self: Key) bool {
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
return self == .left_super or self == .right_super;
|
||||
}
|
||||
return self == .left_control or self == .right_control;
|
||||
}
|
||||
|
||||
/// true if this key is either left or right shift.
|
||||
pub fn leftOrRightShift(self: Key) bool {
|
||||
return self == .left_shift or self == .right_shift;
|
||||
}
|
||||
|
||||
/// true if this key is either left or right alt.
|
||||
pub fn leftOrRightAlt(self: Key) bool {
|
||||
return self == .left_alt or self == .right_alt;
|
||||
}
|
||||
|
||||
test "fromASCII should not return keypad keys" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(Key.fromASCII('0').? == .zero);
|
||||
@ -689,3 +717,25 @@ pub const Key = enum(c_int) {
|
||||
.{ '=', .kp_equal },
|
||||
};
|
||||
};
|
||||
|
||||
/// This sets either "ctrl" or "super" to true (but not both)
|
||||
/// on mods depending on if the build target is Mac or not. On
|
||||
/// Mac, we default to super (i.e. super+c for copy) and on
|
||||
/// non-Mac we default to ctrl (i.e. ctrl+c for copy).
|
||||
pub fn ctrlOrSuper(mods: Mods) Mods {
|
||||
var copy = mods;
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
copy.super = true;
|
||||
} else {
|
||||
copy.ctrl = true;
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
test "ctrlOrSuper" {
|
||||
const testing = std.testing;
|
||||
var m: Mods = ctrlOrSuper(.{});
|
||||
|
||||
try testing.expect(m.ctrlOrSuper());
|
||||
}
|
||||
|
@ -313,6 +313,7 @@ test {
|
||||
_ = @import("termio.zig");
|
||||
_ = @import("input.zig");
|
||||
_ = @import("cli.zig");
|
||||
_ = @import("surface_mouse.zig");
|
||||
|
||||
// Libraries
|
||||
_ = @import("segmented_pool.zig");
|
||||
|
302
src/surface_mouse.zig
Normal file
302
src/surface_mouse.zig
Normal file
@ -0,0 +1,302 @@
|
||||
/// 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 apprt = @import("apprt.zig");
|
||||
const terminal = @import("terminal/main.zig");
|
||||
const MouseShape = @import("terminal/mouse_shape.zig").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.Terminal.MouseEvents,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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;
|
||||
|
||||
// Exception: link hover overrides any other shape processing currently and
|
||||
// does not change state.
|
||||
//
|
||||
// TODO: As we unravel mouse state, we can fix this to be more explicit.
|
||||
if (self.over_link) {
|
||||
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 isRectangleSelectState(mods: input.Mods) bool {
|
||||
return mods.ctrlOrSuper() and mods.alt;
|
||||
}
|
||||
|
||||
fn isMouseModeOverrideState(mods: input.Mods) bool {
|
||||
return mods.shift;
|
||||
}
|
||||
|
||||
test "keyToMouseShape" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
// No specific key pressed
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .invalid,
|
||||
.mouse_event = .none,
|
||||
.mouse_shape = .progress,
|
||||
.mods = .{},
|
||||
.over_link = 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 = .left_shift,
|
||||
.mouse_event = .none,
|
||||
.mouse_shape = .progress,
|
||||
.mods = .{},
|
||||
.over_link = true,
|
||||
};
|
||||
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(got == null);
|
||||
}
|
||||
|
||||
{
|
||||
// default, no mods (mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_shift,
|
||||
.mouse_event = .x10,
|
||||
.mouse_shape = .default,
|
||||
.mods = .{},
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .default;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
|
||||
{
|
||||
// default -> crosshair (mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_alt,
|
||||
.mouse_event = .x10,
|
||||
.mouse_shape = .default,
|
||||
.mods = .{ .ctrl = true, .super = true, .alt = true, .shift = true },
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .crosshair;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
|
||||
{
|
||||
// default -> text (mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_shift,
|
||||
.mouse_event = .x10,
|
||||
.mouse_shape = .default,
|
||||
.mods = .{ .shift = true },
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .text;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
|
||||
{
|
||||
// crosshair -> text (mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_alt,
|
||||
.mouse_event = .x10,
|
||||
.mouse_shape = .crosshair,
|
||||
.mods = .{ .shift = true },
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .text;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
|
||||
{
|
||||
// crosshair -> default (mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_alt,
|
||||
.mouse_event = .x10,
|
||||
.mouse_shape = .crosshair,
|
||||
.mods = .{},
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .default;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
|
||||
{
|
||||
// text -> crosshair (mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_alt,
|
||||
.mouse_event = .x10,
|
||||
.mouse_shape = .text,
|
||||
.mods = .{ .ctrl = true, .super = true, .alt = true, .shift = true },
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .crosshair;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
|
||||
{
|
||||
// text -> default (mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_shift,
|
||||
.mouse_event = .x10,
|
||||
.mouse_shape = .text,
|
||||
.mods = .{},
|
||||
.over_link = 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 = .left_shift,
|
||||
.mouse_event = .none,
|
||||
.mouse_shape = .text,
|
||||
.mods = .{},
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .text;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
|
||||
{
|
||||
// text -> crosshair (no mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_alt,
|
||||
.mouse_event = .none,
|
||||
.mouse_shape = .text,
|
||||
.mods = .{ .ctrl = true, .super = true, .alt = true },
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .crosshair;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
|
||||
{
|
||||
// crosshair -> text (no mouse tracking)
|
||||
const m: SurfaceMouse = .{
|
||||
.physical_key = .left_alt,
|
||||
.mouse_event = .none,
|
||||
.mouse_shape = .crosshair,
|
||||
.mods = .{},
|
||||
.over_link = false,
|
||||
};
|
||||
|
||||
const want: MouseShape = .text;
|
||||
const got = m.keyToMouseShape();
|
||||
try testing.expect(want == got);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user