Files

298 lines
8.9 KiB
Zig

/// 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, called during key events. This mainly
/// handles overrides on key presses depending on whether or not we are in
/// mouse tracking mode, however it is also responsible for resetting cursor
/// state on any particular key releases.
///
/// 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;
}
// Handle possible overrides depending on mouse tracking state.
switch (self.mouse_event != .none) {
true => {
// In mouse tracking mode
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;
}
},
false => {
// Default terminal mode
if (isRectangleSelectState(self.mods)) {
// Crosshair (rectangle select)
return .crosshair;
} else if (isMouseModeOverrideState(self.mods)) {
// Shift shows an I-beam so selection is obvious even when
// the application cursor is not text (OSC 22). Release
// restores mouse_shape below.
return .text;
}
},
}
// No overrides means we just revert back to the stored terminal mouse
// shape. Note that this may be different than what has been currently sent
// to the apprt, so this will force the reset.
return self.mouse_shape;
}
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);
}
{
// no override restores the application shape (mouse tracking)
const m: SurfaceMouse = .{
.physical_key = .alt_left,
.mouse_event = .x10,
.mouse_shape = .crosshair,
.mods = .{},
.over_link = false,
.hidden = false,
};
const want: MouseShape = .crosshair;
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, 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);
}
{
// no override restores the application shape (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 = .crosshair;
const got = m.keyToMouseShape();
try testing.expect(want == got);
}
}