diff --git a/include/ghostty.h b/include/ghostty.h index 57eb2d716..3fb7b3616 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -40,6 +40,17 @@ typedef struct { double scale_factor; } ghostty_surface_config_s; +typedef enum { + GHOSTTY_MOUSE_RELEASE, + GHOSTTY_MOUSE_PRESS, +} ghostty_input_mouse_state_e; + +typedef enum { + GHOSTTY_MOUSE_LEFT = 1, + GHOSTTY_MOUSE_RIGHT, + GHOSTTY_MOUSE_MIDDLE, +} ghostty_input_mouse_button_e; + typedef enum { GHOSTTY_MODS_NONE = 0, GHOSTTY_MODS_SHIFT = 1 << 0, @@ -218,6 +229,8 @@ void ghostty_surface_set_focus(ghostty_surface_t, bool); void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_mods_e); void ghostty_surface_char(ghostty_surface_t, uint32_t); +void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e); +void ghostty_surface_mouse_pos(ghostty_surface_t, double, double); #ifdef __cplusplus } diff --git a/macos/Sources/TerminalSurfaceView.swift b/macos/Sources/TerminalSurfaceView.swift index 644ec0ac2..6b75d8125 100644 --- a/macos/Sources/TerminalSurfaceView.swift +++ b/macos/Sources/TerminalSurfaceView.swift @@ -221,8 +221,10 @@ class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject { self.error = AppError.surfaceCreateError return } - self.surface = surface; + + // Setup our tracking area so we get mouse moved events + updateTrackingAreas() } required init?(coder: NSCoder) { @@ -230,6 +232,8 @@ class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject { } deinit { + trackingAreas.forEach { removeTrackingArea($0) } + guard let surface = self.surface else { return } ghostty_surface_free(surface) } @@ -249,6 +253,27 @@ class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject { } } + override func updateTrackingAreas() { + // To update our tracking area we just recreate it all. + trackingAreas.forEach { removeTrackingArea($0) } + + // This tracking area is across the entire frame to notify us of mouse movements. + addTrackingArea(NSTrackingArea( + rect: frame, + options: [ + .mouseEnteredAndExited, + .mouseMoved, + .inVisibleRect, + + // It is possible this is incorrect when we have splits. This will make + // mouse events only happen while the terminal is focused. Is that what + // we want? + .activeWhenFirstResponder, + ], + owner: self, + userInfo: nil)) + } + override func viewDidChangeBackingProperties() { guard let surface = self.surface else { return } @@ -268,7 +293,40 @@ class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject { } override func mouseDown(with event: NSEvent) { - print("Mouse down: \(event)") + guard let surface = self.surface else { return } + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods) + } + + override func mouseUp(with event: NSEvent) { + guard let surface = self.surface else { return } + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods) + } + + override func rightMouseDown(with event: NSEvent) { + guard let surface = self.surface else { return } + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods) + } + + override func rightMouseUp(with event: NSEvent) { + guard let surface = self.surface else { return } + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods) + } + + override func mouseMoved(with event: NSEvent) { + guard let surface = self.surface else { return } + + // Convert window position to view position. Note (0, 0) is bottom left. + let pos = self.convert(event.locationInWindow, from: nil) + ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y) + + } + + override func mouseDragged(with event: NSEvent) { + self.mouseMoved(with: event) } override func keyDown(with event: NSEvent) { diff --git a/src/App.zig b/src/App.zig index a29f87330..c7fc751d8 100644 --- a/src/App.zig +++ b/src/App.zig @@ -439,4 +439,23 @@ pub const CAPI = struct { export fn ghostty_surface_char(win: *Window, codepoint: u32) void { win.window.charCallback(codepoint); } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_mouse_button( + win: *Window, + action: input.MouseButtonState, + button: input.MouseButton, + mods: c_int, + ) void { + win.window.mouseButtonCallback( + action, + button, + @bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))), + ); + } + + /// Update the mouse position within the view. + export fn ghostty_surface_mouse_pos(win: *Window, x: f64, y: f64) void { + win.window.cursorPosCallback(x, y); + } }; diff --git a/src/Window.zig b/src/Window.zig index 423432409..a104b1ee6 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -1292,7 +1292,7 @@ pub fn mouseButtonCallback( } // Always record our latest mouse state - self.mouse.click_state[@enumToInt(button)] = action; + self.mouse.click_state[@intCast(usize, @enumToInt(button))] = action; self.mouse.mods = @bitCast(input.Mods, mods); self.renderer_state.mutex.lock(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 59b52878c..0e88a032a 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -58,6 +58,7 @@ pub const Window = struct { core_win: *CoreWindow, content_scale: apprt.ContentScale, size: apprt.WindowSize, + cursor_pos: apprt.CursorPos, opts: Options, pub const Options = extern struct { @@ -82,6 +83,7 @@ pub const Window = struct { .y = @floatCast(f32, opts.scale_factor), }, .size = .{ .width = 800, .height = 600 }, + .cursor_pos = .{ .x = 0, .y = 0 }, .opts = opts, }; } @@ -130,6 +132,10 @@ pub const Window = struct { return false; } + pub fn getCursorPos(self: *const Window) !apprt.CursorPos { + return self.cursor_pos; + } + pub fn refresh(self: *Window) void { self.core_win.refreshCallback() catch |err| { log.err("error in refresh callback err={}", .{err}); @@ -157,6 +163,37 @@ pub const Window = struct { }; } + pub fn mouseButtonCallback( + self: *const Window, + action: input.MouseButtonState, + button: input.MouseButton, + mods: input.Mods, + ) void { + self.core_win.mouseButtonCallback(action, button, mods) catch |err| { + log.err("error in mouse button callback err={}", .{err}); + return; + }; + } + + pub fn cursorPosCallback(self: *Window, x: f64, y: f64) void { + // Convert our unscaled x/y to scaled. + self.cursor_pos = self.core_win.window.cursorPosToPixels(.{ + .x = @floatCast(f32, x), + .y = @floatCast(f32, y), + }) catch |err| { + log.err( + "error converting cursor pos to scaled pixels in cursor pos callback err={}", + .{err}, + ); + return; + }; + + self.core_win.cursorPosCallback(self.cursor_pos) catch |err| { + log.err("error in cursor pos callback err={}", .{err}); + return; + }; + } + pub fn keyCallback( self: *const Window, action: input.Action, @@ -184,4 +221,11 @@ pub const Window = struct { return; }; } + + /// The cursor position from the host directly is in screen coordinates but + /// all our interface works in pixels. + fn cursorPosToPixels(self: *const Window, pos: apprt.CursorPos) !apprt.CursorPos { + const scale = try self.getContentScale(); + return .{ .x = pos.x * scale.x, .y = pos.y * scale.y }; + } }; diff --git a/src/input/mouse.zig b/src/input/mouse.zig index 113521813..e1059e4c0 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -1,7 +1,11 @@ /// The state of a mouse button. -pub const MouseButtonState = enum(u1) { - release = 0, - press = 1, +/// +/// This is backed by a c_int so we can use this as-is for our embedding API. +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const MouseButtonState = enum(c_int) { + release, + press, }; /// Possible mouse buttons. We only track up to 11 because thats the maximum @@ -10,7 +14,11 @@ pub const MouseButtonState = enum(u1) { /// /// Its a bit silly to name numbers like this but given its a restricted /// set, it feels better than passing around raw numeric literals. -pub const MouseButton = enum(u4) { +/// +/// This is backed by a c_int so we can use this as-is for our embedding API. +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const MouseButton = enum(c_int) { const Self = @This(); /// The maximum value in this enum. This can be used to create a densely