Merge pull request #2400 from ghostty-org/mouse-exit

Detect mouse leave events, use it to reset hover states
This commit is contained in:
Mitchell Hashimoto
2024-10-06 15:42:33 -10:00
committed by GitHub
3 changed files with 61 additions and 0 deletions

View File

@ -354,6 +354,7 @@ extension Ghostty {
addTrackingArea(NSTrackingArea( addTrackingArea(NSTrackingArea(
rect: frame, rect: frame,
options: [ options: [
.mouseEnteredAndExited,
.mouseMoved, .mouseMoved,
// Only send mouse events that happen in our visible (not obscured) rect // Only send mouse events that happen in our visible (not obscured) rect
@ -486,6 +487,14 @@ extension Ghostty {
super.rightMouseUp(with: event) super.rightMouseUp(with: event)
} }
override func mouseExited(with event: NSEvent) {
guard let surface = self.surface else { return }
// Negative values indicate cursor has left the viewport
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_surface_mouse_pos(surface, -1, -1, mods)
}
override func mouseMoved(with event: NSEvent) { override func mouseMoved(with event: NSEvent) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }

View File

@ -3032,6 +3032,11 @@ pub fn mousePressureCallback(
/// Cursor position callback. /// Cursor position callback.
/// ///
/// Send negative x or y values to indicate the cursor is outside the
/// viewport. The magnitude of the negative values are meaningless;
/// they are only used to indicate the cursor is outside the viewport.
/// It's important to do this to ensure hover states are cleared.
///
/// The mods parameter is optional because some apprts do not provide /// The mods parameter is optional because some apprts do not provide
/// modifier information on cursor position events. If mods is null then /// modifier information on cursor position events. If mods is null then
/// we'll use the last known mods. This is usually accurate since mod events /// we'll use the last known mods. This is usually accurate since mod events
@ -3047,6 +3052,36 @@ pub fn cursorPosCallback(
crash.sentry.thread_state = self.crashThreadState(); crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null; defer crash.sentry.thread_state = null;
// If the position is negative, it is outside our viewport and
// we need to clear any hover states.
if (pos.x < 0 or pos.y < 0) {
// Reset our hyperlink state
self.mouse.link_point = null;
if (self.mouse.over_link) {
self.mouse.over_link = false;
try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
self.io.terminal.mouse_shape,
);
try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
);
try self.queueRender();
}
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
// No mouse point so we don't highlight links
self.renderer_state.mouse.point = null;
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
return;
}
// Always show the mouse again if it is hidden // Always show the mouse again if it is hidden
if (self.mouse.hidden) self.showMouse(); if (self.mouse.hidden) self.showMouse();

View File

@ -526,6 +526,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
_ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(&gtkMouseDown), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(&gtkMouseDown), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gesture_click, "released", c.G_CALLBACK(&gtkMouseUp), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(gesture_click, "released", c.G_CALLBACK(&gtkMouseUp), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_motion, "motion", c.G_CALLBACK(&gtkMouseMotion), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_motion, "motion", c.G_CALLBACK(&gtkMouseMotion), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_motion, "leave", c.G_CALLBACK(&gtkMouseLeave), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_scroll, "scroll", c.G_CALLBACK(&gtkMouseScroll), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_scroll, "scroll", c.G_CALLBACK(&gtkMouseScroll), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(im_context, "preedit-start", c.G_CALLBACK(&gtkInputPreeditStart), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "preedit-start", c.G_CALLBACK(&gtkInputPreeditStart), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(&gtkInputPreeditChanged), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(&gtkInputPreeditChanged), self, null, c.G_CONNECT_DEFAULT);
@ -1344,6 +1345,22 @@ fn gtkMouseMotion(
}; };
} }
fn gtkMouseLeave(
ec: *c.GtkEventControllerMotion,
ud: ?*anyopaque,
) callconv(.C) void {
const self = userdataSelf(ud.?);
// Get our modifiers
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec));
const gtk_mods = c.gdk_event_get_modifier_state(event);
const mods = translateMods(gtk_mods);
self.core_surface.cursorPosCallback(.{ .x = -1, .y = -1 }, mods) catch |err| {
log.err("error in cursor pos callback err={}", .{err});
return;
};
}
fn gtkMouseScroll( fn gtkMouseScroll(
_: *c.GtkEventControllerScroll, _: *c.GtkEventControllerScroll,
x: c.gdouble, x: c.gdouble,