mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: implement setCursorVisibility
This commit is contained in:
@ -302,6 +302,7 @@ typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
||||
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *);
|
||||
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
||||
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e);
|
||||
typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void *, bool);
|
||||
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e);
|
||||
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s);
|
||||
@ -320,6 +321,7 @@ typedef struct {
|
||||
ghostty_runtime_reload_config_cb reload_config_cb;
|
||||
ghostty_runtime_set_title_cb set_title_cb;
|
||||
ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb;
|
||||
ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb;
|
||||
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
|
||||
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
|
||||
ghostty_runtime_new_split_cb new_split_cb;
|
||||
|
@ -73,6 +73,7 @@ extension Ghostty {
|
||||
reload_config_cb: { userdata in AppState.reloadConfig(userdata) },
|
||||
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
|
||||
set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
|
||||
set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) },
|
||||
read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) },
|
||||
write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) },
|
||||
new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
|
||||
@ -338,6 +339,11 @@ extension Ghostty {
|
||||
let surfaceView = Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
surfaceView.setCursorShape(shape)
|
||||
}
|
||||
|
||||
static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) {
|
||||
let surfaceView = Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
surfaceView.setCursorVisibility(visible)
|
||||
}
|
||||
|
||||
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
|
||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||
|
@ -197,7 +197,9 @@ extension Ghostty {
|
||||
|
||||
private var markedText: NSMutableAttributedString
|
||||
private var mouseEntered: Bool = false
|
||||
private var cursor: NSCursor = .arrow
|
||||
private var focused: Bool = true
|
||||
private var cursor: NSCursor = .iBeam
|
||||
private var cursorVisible: CursorVisibility = .visible
|
||||
|
||||
// We need to support being a first responder so that we can get input events
|
||||
override var acceptsFirstResponder: Bool { return true }
|
||||
@ -206,6 +208,15 @@ extension Ghostty {
|
||||
// so we'll use that to tell ghostty to refresh.
|
||||
override var wantsUpdateLayer: Bool { return true }
|
||||
|
||||
// State machine for mouse cursor visibility because every call to
|
||||
// NSCursor.hide/unhide must be balanced.
|
||||
enum CursorVisibility {
|
||||
case visible
|
||||
case hidden
|
||||
case pendingVisible
|
||||
case pendingHidden
|
||||
}
|
||||
|
||||
init(_ app: ghostty_app_t, _ baseConfig: ghostty_surface_config_s?) {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
|
||||
@ -236,7 +247,14 @@ extension Ghostty {
|
||||
|
||||
deinit {
|
||||
trackingAreas.forEach { removeTrackingArea($0) }
|
||||
|
||||
|
||||
// mouseExited is not called by AppKit one last time when the view
|
||||
// closes so we do it manually to ensure our NSCursor state remains
|
||||
// accurate.
|
||||
if (mouseEntered) {
|
||||
mouseExited(with: NSEvent())
|
||||
}
|
||||
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_free(surface)
|
||||
}
|
||||
@ -320,8 +338,41 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
// Set our cursor immediately if our mouse is over our window
|
||||
if (mouseEntered) { cursorUpdate(with: NSEvent()) }
|
||||
if let window = self.window {
|
||||
window.invalidateCursorRects(for: self)
|
||||
}
|
||||
}
|
||||
|
||||
func setCursorVisibility(_ visible: Bool) {
|
||||
switch (cursorVisible) {
|
||||
case .visible:
|
||||
// If we want to be visible, do nothing. If we want to be hidden
|
||||
// enter the pending state.
|
||||
if (visible) { return }
|
||||
cursorVisible = .pendingHidden
|
||||
|
||||
case .hidden:
|
||||
// If we want to be hidden, do nothing. If we want to be visible
|
||||
// enter the pending state.
|
||||
if (!visible) { return }
|
||||
cursorVisible = .pendingVisible
|
||||
|
||||
case .pendingVisible:
|
||||
// If we want to be visible, do nothing because we're already pending.
|
||||
// If we want to be hidden, we're already hidden so reset state.
|
||||
if (visible) { return }
|
||||
cursorVisible = .hidden
|
||||
|
||||
case .pendingHidden:
|
||||
// If we want to be hidden, do nothing because we're pending that switch.
|
||||
// If we want to be visible, we're already visible so reset state.
|
||||
if (!visible) { return }
|
||||
cursorVisible = .visible
|
||||
}
|
||||
|
||||
if (mouseEntered) {
|
||||
cursor.set()
|
||||
cursorUpdate(with: NSEvent())
|
||||
}
|
||||
}
|
||||
|
||||
@ -338,13 +389,22 @@ extension Ghostty {
|
||||
// If we have a blur, set the blur
|
||||
ghostty_set_window_background_blur(surface, Unmanaged.passUnretained(window).toOpaque())
|
||||
}
|
||||
|
||||
override func becomeFirstResponder() -> Bool {
|
||||
let result = super.becomeFirstResponder()
|
||||
if (result) { focused = true }
|
||||
return result
|
||||
}
|
||||
|
||||
override func resignFirstResponder() -> Bool {
|
||||
let result = super.resignFirstResponder()
|
||||
|
||||
// We sometimes call this manually (see SplitView) as a way to force us to
|
||||
// yield our focus state.
|
||||
if (result) { focusDidChange(false) }
|
||||
if (result) {
|
||||
focusDidChange(false)
|
||||
focused = false
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@ -372,7 +432,7 @@ extension Ghostty {
|
||||
|
||||
override func resetCursorRects() {
|
||||
discardCursorRects()
|
||||
addCursorRect(frame, cursor: .iBeam)
|
||||
addCursorRect(frame, cursor: self.cursor)
|
||||
}
|
||||
|
||||
override func viewDidChangeBackingProperties() {
|
||||
@ -419,7 +479,7 @@ extension Ghostty {
|
||||
|
||||
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)
|
||||
@ -432,10 +492,20 @@ extension Ghostty {
|
||||
|
||||
override func mouseEntered(with event: NSEvent) {
|
||||
mouseEntered = true
|
||||
|
||||
// If our cursor is hidden, we hide it on upon entry and we unhide
|
||||
// it on exit (mouseExited)
|
||||
if (cursorVisible == .hidden) {
|
||||
NSCursor.hide()
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
mouseEntered = false
|
||||
|
||||
if (cursorVisible == .hidden) {
|
||||
NSCursor.unhide()
|
||||
}
|
||||
}
|
||||
|
||||
override func scrollWheel(with event: NSEvent) {
|
||||
@ -482,6 +552,22 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
override func cursorUpdate(with event: NSEvent) {
|
||||
if (focused) {
|
||||
switch (cursorVisible) {
|
||||
case .visible, .hidden:
|
||||
// Do nothing, stable state
|
||||
break
|
||||
|
||||
case .pendingHidden:
|
||||
NSCursor.hide()
|
||||
cursorVisible = .hidden
|
||||
|
||||
case .pendingVisible:
|
||||
NSCursor.unhide()
|
||||
cursorVisible = .visible
|
||||
}
|
||||
}
|
||||
|
||||
cursor.set()
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,9 @@ pub const App = struct {
|
||||
/// Called to set the cursor shape.
|
||||
set_mouse_shape: *const fn (SurfaceUD, terminal.MouseShape) callconv(.C) void,
|
||||
|
||||
/// Called to set the mouse visibility.
|
||||
set_mouse_visibility: *const fn (SurfaceUD, bool) callconv(.C) void,
|
||||
|
||||
/// Read the clipboard value. The return value must be preserved
|
||||
/// by the host until the next call. If there is no valid clipboard
|
||||
/// value then this should return null.
|
||||
@ -321,6 +324,14 @@ pub const Surface = struct {
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the visibility of the mouse cursor.
|
||||
pub fn setMouseVisibility(self: *Surface, visible: bool) void {
|
||||
self.app.opts.set_mouse_visibility(
|
||||
self.opts.userdata,
|
||||
visible,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn supportsClipboard(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
|
Reference in New Issue
Block a user