Merge pull request #979 from vancluever/vancluever-rect-crosshair

Surface: set crosshair, change event processing logic for mouse tracking
This commit is contained in:
Mitchell Hashimoto
2023-12-16 08:08:11 -08:00
committed by GitHub
5 changed files with 373 additions and 52 deletions

View File

@ -37,6 +37,7 @@ const input = @import("input.zig");
const App = @import("App.zig"); const App = @import("App.zig");
const internal_os = @import("os/main.zig"); const internal_os = @import("os/main.zig");
const inspector = @import("inspector/main.zig"); const inspector = @import("inspector/main.zig");
const SurfaceMouse = @import("surface_mouse.zig");
const log = std.log.scoped(.surface); const log = std.log.scoped(.surface);
@ -1285,23 +1286,16 @@ pub fn keyCallback(
if (rehide) self.hideMouse(); if (rehide) self.hideMouse();
} }
// When we are in the middle of a mouse event and we press shift, // Process the cursor state logic. This will update the cursor shape if
// we change the mouse to a text shape so that selection appears // needed, depending on the key state.
// possible. if ((SurfaceMouse{
if (self.io.terminal.flags.mouse_event != .none and .physical_key = event.physical_key,
event.physical_key == .left_shift or .mouse_event = self.io.terminal.flags.mouse_event,
event.physical_key == .right_shift) .mouse_shape = self.io.terminal.mouse_shape,
{ .mods = self.mouse.mods,
switch (event.action) { .over_link = self.mouse.over_link,
.press => if (!self.mouse.over_link) { }).keyToMouseShape()) |shape|
// If the cursor is over a link then the pointer shape takes try self.rt_surface.setMouseShape(shape);
// priority
try self.rt_surface.setMouseShape(.text);
},
.release => try self.rt_surface.setMouseShape(self.io.terminal.mouse_shape),
.repeat => {},
}
}
// No binding, so we have to perform an encoding task. This // No binding, so we have to perform an encoding task. This
// may still result in no encoding. Under different modes and // 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. /// Double-click dragging moves the selection one "word" at a time.
fn dragLeftClickDouble( fn dragLeftClickDouble(
self: *Surface, self: *Surface,
@ -2385,7 +2368,7 @@ fn dragLeftClickSingle(
self.setSelection(if (selected) .{ self.setSelection(if (selected) .{
.start = screen_point, .start = screen_point,
.end = 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); } else null);
return; return;
@ -2432,7 +2415,7 @@ fn dragLeftClickSingle(
self.setSelection(.{ self.setSelection(.{
.start = start, .start = start,
.end = 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,
}); });
return; return;
} }
@ -2491,7 +2474,7 @@ fn dragLeftClickBefore(
click_point: terminal.point.ScreenPoint, click_point: terminal.point.ScreenPoint,
mods: input.Mods, mods: input.Mods,
) bool { ) bool {
if (ctrlOrSuper(mods) and mods.alt) { if (mods.ctrlOrSuper() and mods.alt) {
return screen_point.x < click_point.x; return screen_point.x < click_point.x;
} }

View File

@ -881,23 +881,23 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
// Fonts // Fonts
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .equal, .mods = ctrlOrSuper(.{}) }, .{ .key = .equal, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .increase_font_size = 1 }, .{ .increase_font_size = 1 },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .minus, .mods = ctrlOrSuper(.{}) }, .{ .key = .minus, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .decrease_font_size = 1 }, .{ .decrease_font_size = 1 },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .zero, .mods = ctrlOrSuper(.{}) }, .{ .key = .zero, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .reset_font_size = {} }, .{ .reset_font_size = {} },
); );
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .j, .mods = ctrlOrSuper(.{ .shift = true }) }, .{ .key = .j, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
.{ .write_scrollback_file = {} }, .{ .write_scrollback_file = {} },
); );
@ -1098,14 +1098,14 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
// Toggle fullscreen // Toggle fullscreen
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .enter, .mods = ctrlOrSuper(.{}) }, .{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .toggle_fullscreen = {} }, .{ .toggle_fullscreen = {} },
); );
// Toggle zoom a split // Toggle zoom a split
try result.keybind.set.put( try result.keybind.set.put(
alloc, alloc,
.{ .key = .enter, .mods = ctrlOrSuper(.{ .shift = true }) }, .{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
.{ .toggle_split_zoom = {} }, .{ .toggle_split_zoom = {} },
); );
@ -1289,21 +1289,6 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
return result; 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 /// Load configuration from an iterator that yields values that look like
/// command-line arguments, i.e. `--key=value`. /// command-line arguments, i.e. `--key=value`.
pub fn loadIter( pub fn loadIter(

View File

@ -145,6 +145,14 @@ pub const Mods = packed struct(Mods.Backing) {
return result; 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 // For our own understanding
test { test {
const testing = std.testing; const testing = std.testing;
@ -607,6 +615,26 @@ pub const Key = enum(c_int) {
=> null, => 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" { test "fromASCII should not return keypad keys" {
const testing = std.testing; const testing = std.testing;
try testing.expect(Key.fromASCII('0').? == .zero); try testing.expect(Key.fromASCII('0').? == .zero);
@ -689,3 +717,25 @@ pub const Key = enum(c_int) {
.{ '=', .kp_equal }, .{ '=', .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());
}

View File

@ -313,6 +313,7 @@ test {
_ = @import("termio.zig"); _ = @import("termio.zig");
_ = @import("input.zig"); _ = @import("input.zig");
_ = @import("cli.zig"); _ = @import("cli.zig");
_ = @import("surface_mouse.zig");
// Libraries // Libraries
_ = @import("segmented_pool.zig"); _ = @import("segmented_pool.zig");

302
src/surface_mouse.zig Normal file
View 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);
}
}