macos: move mousePos and mousScroll to Ghostty.Surface

pull/7634/head
Mitchell Hashimoto 2025-06-19 14:07:09 -07:00
parent 4445a9c637
commit bc134016f7
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 183 additions and 39 deletions

View File

@ -269,6 +269,40 @@ extension Ghostty.Input {
self.mods = Mods(cMods: mods) self.mods = Mods(cMods: mods)
} }
} }
/// Represents a mouse position/movement event with coordinates and modifier keys.
struct MousePosEvent {
let x: Double
let y: Double
let mods: Mods
init(
x: Double,
y: Double,
mods: Mods = []
) {
self.x = x
self.y = y
self.mods = mods
}
}
/// Represents a mouse scroll event with scroll deltas and modifier keys.
struct MouseScrollEvent {
let x: Double
let y: Double
let mods: ScrollMods
init(
x: Double,
y: Double,
mods: ScrollMods = .init(rawValue: 0)
) {
self.x = x
self.y = y
self.mods = mods
}
}
} }
// MARK: Ghostty.Input.MouseState // MARK: Ghostty.Input.MouseState
@ -335,6 +369,92 @@ extension Ghostty.Input.MouseButton: AppEnum {
] ]
} }
// MARK: Ghostty.Input.ScrollMods
extension Ghostty.Input {
/// `ghostty_input_scroll_mods_t` - Scroll event modifiers
///
/// This is a packed bitmask that contains precision and momentum information
/// for scroll events, matching the Zig `ScrollMods` packed struct.
struct ScrollMods {
let rawValue: Int32
/// True if this is a high-precision scroll event (e.g., trackpad, Magic Mouse)
var precision: Bool {
rawValue & 0b0000_0001 != 0
}
/// The momentum phase of the scroll event for inertial scrolling
var momentum: Momentum {
let momentumBits = (rawValue >> 1) & 0b0000_0111
return Momentum(rawValue: UInt8(momentumBits)) ?? .none
}
init(precision: Bool = false, momentum: Momentum = .none) {
var value: Int32 = 0
if precision {
value |= 0b0000_0001
}
value |= Int32(momentum.rawValue) << 1
self.rawValue = value
}
init(rawValue: Int32) {
self.rawValue = rawValue
}
var cScrollMods: ghostty_input_scroll_mods_t {
rawValue
}
}
}
// MARK: Ghostty.Input.Momentum
extension Ghostty.Input {
/// `ghostty_input_mouse_momentum_e` - Momentum phase for scroll events
enum Momentum: UInt8, CaseIterable {
case none = 0
case began = 1
case stationary = 2
case changed = 3
case ended = 4
case cancelled = 5
case mayBegin = 6
var cMomentum: ghostty_input_mouse_momentum_e {
switch self {
case .none: GHOSTTY_MOUSE_MOMENTUM_NONE
case .began: GHOSTTY_MOUSE_MOMENTUM_BEGAN
case .stationary: GHOSTTY_MOUSE_MOMENTUM_STATIONARY
case .changed: GHOSTTY_MOUSE_MOMENTUM_CHANGED
case .ended: GHOSTTY_MOUSE_MOMENTUM_ENDED
case .cancelled: GHOSTTY_MOUSE_MOMENTUM_CANCELLED
case .mayBegin: GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
}
}
}
}
#if canImport(AppKit)
import AppKit
extension Ghostty.Input.Momentum {
/// Create a Momentum from an NSEvent.Phase
init(_ phase: NSEvent.Phase) {
switch phase {
case .began: self = .began
case .stationary: self = .stationary
case .changed: self = .changed
case .ended: self = .ended
case .cancelled: self = .cancelled
case .mayBegin: self = .mayBegin
default: self = .none
}
}
}
#endif
// MARK: Ghostty.Input.Mods // MARK: Ghostty.Input.Mods
extension Ghostty.Input { extension Ghostty.Input {

View File

@ -88,6 +88,38 @@ extension Ghostty {
event.mods.cMods) event.mods.cMods)
} }
/// Send a mouse position event to the terminal.
///
/// This reports the current mouse position to the terminal, which may be used
/// for mouse tracking, hover effects, or other position-dependent features.
/// The terminal will only receive these events if mouse reporting is enabled.
///
/// - Parameter event: The mouse position event to send to the terminal
@MainActor
func sendMousePos(_ event: Input.MousePosEvent) {
ghostty_surface_mouse_pos(
surface,
event.x,
event.y,
event.mods.cMods)
}
/// Send a mouse scroll event to the terminal.
///
/// This sends scroll wheel input to the terminal with delta values for both
/// horizontal and vertical scrolling, along with precision and momentum information.
/// The terminal processes this according to its scroll handling configuration.
///
/// - Parameter event: The mouse scroll event to send to the terminal
@MainActor
func sendMouseScroll(_ event: Input.MouseScrollEvent) {
ghostty_surface_mouse_scroll(
surface,
event.x,
event.y,
event.mods.cScrollMods)
}
/// Perform a keybinding action. /// Perform a keybinding action.
/// ///
/// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4`

View File

@ -808,19 +808,23 @@ extension Ghostty {
override func mouseEntered(with event: NSEvent) { override func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event) super.mouseEntered(with: event)
guard let surface = self.surface else { return } guard let surfaceModel else { return }
// On mouse enter we need to reset our cursor position. This is // On mouse enter we need to reset our cursor position. This is
// super important because we set it to -1/-1 on mouseExit and // super important because we set it to -1/-1 on mouseExit and
// lots of mouse logic (i.e. whether to send mouse reports) depend // lots of mouse logic (i.e. whether to send mouse reports) depend
// on the position being in the viewport if it is. // on the position being in the viewport if it is.
let pos = self.convert(event.locationInWindow, from: nil) let pos = self.convert(event.locationInWindow, from: nil)
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mouseEvent = Ghostty.Input.MousePosEvent(
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods) x: pos.x,
y: frame.height - pos.y,
mods: .init(nsFlags: event.modifierFlags)
)
surfaceModel.sendMousePos(mouseEvent)
} }
override func mouseExited(with event: NSEvent) { override func mouseExited(with event: NSEvent) {
guard let surface = self.surface else { return } guard let surfaceModel else { return }
// If the mouse is being dragged then we don't have to emit // If the mouse is being dragged then we don't have to emit
// this because we get mouse drag events even if we've already // this because we get mouse drag events even if we've already
@ -830,17 +834,25 @@ extension Ghostty {
} }
// Negative values indicate cursor has left the viewport // Negative values indicate cursor has left the viewport
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mouseEvent = Ghostty.Input.MousePosEvent(
ghostty_surface_mouse_pos(surface, -1, -1, mods) x: -1,
y: -1,
mods: .init(nsFlags: event.modifierFlags)
)
surfaceModel.sendMousePos(mouseEvent)
} }
override func mouseMoved(with event: NSEvent) { override func mouseMoved(with event: NSEvent) {
guard let surface = self.surface else { return } guard let surfaceModel else { return }
// Convert window position to view position. Note (0, 0) is bottom left. // Convert window position to view position. Note (0, 0) is bottom left.
let pos = self.convert(event.locationInWindow, from: nil) let pos = self.convert(event.locationInWindow, from: nil)
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mouseEvent = Ghostty.Input.MousePosEvent(
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods) x: pos.x,
y: frame.height - pos.y,
mods: .init(nsFlags: event.modifierFlags)
)
surfaceModel.sendMousePos(mouseEvent)
// Handle focus-follows-mouse // Handle focus-follows-mouse
if let window, if let window,
@ -866,16 +878,13 @@ extension Ghostty {
} }
override func scrollWheel(with event: NSEvent) { override func scrollWheel(with event: NSEvent) {
guard let surface = self.surface else { return } guard let surfaceModel else { return }
// Builds up the "input.ScrollMods" bitmask
var mods: Int32 = 0
var x = event.scrollingDeltaX var x = event.scrollingDeltaX
var y = event.scrollingDeltaY var y = event.scrollingDeltaY
if event.hasPreciseScrollingDeltas { let precision = event.hasPreciseScrollingDeltas
mods = 1
if precision {
// We do a 2x speed multiplier. This is subjective, it "feels" better to me. // We do a 2x speed multiplier. This is subjective, it "feels" better to me.
x *= 2; x *= 2;
y *= 2; y *= 2;
@ -883,29 +892,12 @@ extension Ghostty {
// TODO(mitchellh): do we have to scale the x/y here by window scale factor? // TODO(mitchellh): do we have to scale the x/y here by window scale factor?
} }
// Determine our momentum value let scrollEvent = Ghostty.Input.MouseScrollEvent(
var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE x: x,
switch (event.momentumPhase) { y: y,
case .began: mods: .init(precision: precision, momentum: .init(event.momentumPhase))
momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN )
case .stationary: surfaceModel.sendMouseScroll(scrollEvent)
momentum = GHOSTTY_MOUSE_MOMENTUM_STATIONARY
case .changed:
momentum = GHOSTTY_MOUSE_MOMENTUM_CHANGED
case .ended:
momentum = GHOSTTY_MOUSE_MOMENTUM_ENDED
case .cancelled:
momentum = GHOSTTY_MOUSE_MOMENTUM_CANCELLED
case .mayBegin:
momentum = GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
default:
break
}
// Pack our momentum value into the mods bitmask
mods |= Int32(momentum.rawValue) << 1
ghostty_surface_mouse_scroll(surface, x, y, mods)
} }
override func pressureChange(with event: NSEvent) { override func pressureChange(with event: NSEvent) {