Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
319 lines
9.0 KiB
Rust
319 lines
9.0 KiB
Rust
//! SurfaceMouse represents mouse helper functionality for the core surface
|
|
//! (port of `surface_mouse.zig`).
|
|
//!
|
|
//! 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).
|
|
|
|
use ghostty_input::{Key, Mods};
|
|
use ghostty_terminal::terminal::MouseEvents;
|
|
use ghostty_vt::mouse_shape::MouseShape;
|
|
|
|
/// See the module docs.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct SurfaceMouse {
|
|
/// For processing key events; the key that was physically pressed on the
|
|
/// keyboard.
|
|
pub physical_key: Key,
|
|
|
|
/// The mouse event tracking mode, if any.
|
|
pub mouse_event: MouseEvents,
|
|
|
|
/// The current terminal's mouse shape.
|
|
pub mouse_shape: MouseShape,
|
|
|
|
/// The last mods state when the last mouse button (whatever it was) was
|
|
/// pressed or release.
|
|
pub mods: Mods,
|
|
|
|
/// True if the mouse position is currently over a link.
|
|
pub over_link: bool,
|
|
|
|
/// True if the mouse pointer is currently hidden.
|
|
pub hidden: bool,
|
|
}
|
|
|
|
impl SurfaceMouse {
|
|
/// 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.
|
|
///
|
|
/// `None` is returned when the mouse shape does not need changing.
|
|
#[must_use]
|
|
pub fn key_to_mouse_shape(&self) -> Option<MouseShape> {
|
|
// Filter for appropriate key events
|
|
if !eligible_mouse_shape_key_event(self.physical_key) {
|
|
return None;
|
|
}
|
|
|
|
// Exceptions: link hover or hidden state overrides any other shape
|
|
// processing and does not change state.
|
|
if self.over_link || self.hidden {
|
|
return None;
|
|
}
|
|
|
|
if self.mouse_event == MouseEvents::None {
|
|
// Default terminal mode, should be text (text bar)
|
|
if is_rectangle_select_state(self.mods) {
|
|
// Crosshair (rectangle select)
|
|
Some(MouseShape::Crosshair)
|
|
} else {
|
|
Some(MouseShape::Text)
|
|
}
|
|
} else {
|
|
// In mouse tracking mode, should be default (arrow pointer)
|
|
if is_mouse_mode_override_state(self.mods) && is_rectangle_select_state(self.mods) {
|
|
// Crosshair (rectangle select), only set if we are also
|
|
// overriding (e.g. shift+ctrl+alt)
|
|
Some(MouseShape::Crosshair)
|
|
} else if is_mouse_mode_override_state(self.mods) {
|
|
// Normal override state
|
|
Some(MouseShape::Text)
|
|
} else {
|
|
Some(MouseShape::Default)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn eligible_mouse_shape_key_event(physical_key: Key) -> bool {
|
|
physical_key.ctrl_or_super()
|
|
|| physical_key.left_or_right_shift()
|
|
|| physical_key.left_or_right_alt()
|
|
}
|
|
|
|
fn is_mouse_mode_override_state(mods: Mods) -> bool {
|
|
mods.shift
|
|
}
|
|
|
|
/// Returns true if our modifiers put us in a state where dragging should
|
|
/// cause a rectangle select.
|
|
#[must_use]
|
|
pub fn is_rectangle_select_state(mods: Mods) -> bool {
|
|
is_rectangle_select_state_for(mods, ghostty_input::platform::is_darwin())
|
|
}
|
|
|
|
/// [`is_rectangle_select_state`] for an explicit platform (Zig picks with
|
|
/// `builtin.target.os.tag.isDarwin()` at comptime): on Darwin option alone
|
|
/// selects a rectangle, elsewhere it is ctrl+alt.
|
|
#[must_use]
|
|
pub fn is_rectangle_select_state_for(mods: Mods, darwin: bool) -> bool {
|
|
if darwin {
|
|
mods.alt
|
|
} else {
|
|
mods.ctrl_or_super() && mods.alt
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn rectangle_select_state_per_platform() {
|
|
let alt = Mods {
|
|
alt: true,
|
|
..Mods::default()
|
|
};
|
|
let ctrl_alt = Mods {
|
|
alt: true,
|
|
ctrl: true,
|
|
..Mods::default()
|
|
};
|
|
// macOS: alt alone.
|
|
assert!(is_rectangle_select_state_for(alt, true));
|
|
// Elsewhere: ctrl+alt.
|
|
assert!(!is_rectangle_select_state_for(alt, false));
|
|
assert!(is_rectangle_select_state_for(ctrl_alt, false));
|
|
}
|
|
|
|
fn m(
|
|
physical_key: Key,
|
|
mouse_event: MouseEvents,
|
|
mouse_shape: MouseShape,
|
|
mods: Mods,
|
|
over_link: bool,
|
|
hidden: bool,
|
|
) -> SurfaceMouse {
|
|
SurfaceMouse {
|
|
physical_key,
|
|
mouse_event,
|
|
mouse_shape,
|
|
mods,
|
|
over_link,
|
|
hidden,
|
|
}
|
|
}
|
|
|
|
fn mods(shift: bool, ctrl: bool, alt: bool, super_: bool) -> Mods {
|
|
Mods {
|
|
shift,
|
|
ctrl,
|
|
alt,
|
|
super_,
|
|
..Mods::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn key_to_mouse_shape() {
|
|
let none = Mods::default();
|
|
|
|
// No specific key pressed
|
|
let s = m(
|
|
Key::Unidentified,
|
|
MouseEvents::None,
|
|
MouseShape::Progress,
|
|
none,
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), None);
|
|
|
|
// Over a link.
|
|
let s = m(
|
|
Key::ShiftLeft,
|
|
MouseEvents::None,
|
|
MouseShape::Progress,
|
|
none,
|
|
true,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), None);
|
|
|
|
// Mouse is currently hidden
|
|
let s = m(
|
|
Key::ShiftLeft,
|
|
MouseEvents::None,
|
|
MouseShape::Progress,
|
|
none,
|
|
true,
|
|
true,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), None);
|
|
|
|
// default, no mods (mouse tracking)
|
|
let s = m(
|
|
Key::ShiftLeft,
|
|
MouseEvents::X10,
|
|
MouseShape::Default,
|
|
none,
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Default));
|
|
|
|
// default -> crosshair (mouse tracking)
|
|
let s = m(
|
|
Key::AltLeft,
|
|
MouseEvents::X10,
|
|
MouseShape::Default,
|
|
mods(true, true, true, true),
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Crosshair));
|
|
|
|
// default -> text (mouse tracking)
|
|
let s = m(
|
|
Key::ShiftLeft,
|
|
MouseEvents::X10,
|
|
MouseShape::Default,
|
|
mods(true, false, false, false),
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Text));
|
|
|
|
// crosshair -> text (mouse tracking)
|
|
let s = m(
|
|
Key::AltLeft,
|
|
MouseEvents::X10,
|
|
MouseShape::Crosshair,
|
|
mods(true, false, false, false),
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Text));
|
|
|
|
// crosshair -> default (mouse tracking)
|
|
let s = m(
|
|
Key::AltLeft,
|
|
MouseEvents::X10,
|
|
MouseShape::Crosshair,
|
|
none,
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Default));
|
|
|
|
// text -> crosshair (mouse tracking)
|
|
let s = m(
|
|
Key::AltLeft,
|
|
MouseEvents::X10,
|
|
MouseShape::Text,
|
|
mods(true, true, true, true),
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Crosshair));
|
|
|
|
// text -> default (mouse tracking)
|
|
let s = m(
|
|
Key::ShiftLeft,
|
|
MouseEvents::X10,
|
|
MouseShape::Text,
|
|
none,
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Default));
|
|
|
|
// text, no mods (no mouse tracking)
|
|
let s = m(
|
|
Key::ShiftLeft,
|
|
MouseEvents::None,
|
|
MouseShape::Text,
|
|
none,
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Text));
|
|
|
|
// text -> crosshair (no mouse tracking)
|
|
let s = m(
|
|
Key::AltLeft,
|
|
MouseEvents::None,
|
|
MouseShape::Text,
|
|
mods(false, true, true, true),
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Crosshair));
|
|
|
|
// crosshair -> text (no mouse tracking)
|
|
let s = m(
|
|
Key::AltLeft,
|
|
MouseEvents::None,
|
|
MouseShape::Crosshair,
|
|
none,
|
|
false,
|
|
false,
|
|
);
|
|
assert_eq!(s.key_to_mouse_shape(), Some(MouseShape::Text));
|
|
}
|
|
}
|