apprt/gtk-ng: mouse over link to show tooltip

This commit is contained in:
Mitchell Hashimoto
2025-07-21 11:00:49 -07:00
parent 7711f6b5b6
commit 58ccfb558a
6 changed files with 183 additions and 15 deletions

View File

@ -1316,9 +1316,7 @@ fn mouseRefreshLinks(
break :link .{ null, false }; break :link .{ null, false };
}; };
break :link .{ break :link .{
.{ .{ .url = try alloc.dupeZ(u8, uri) },
.url = uri,
},
self.config.link_previews != .false, self.config.link_previews != .false,
}; };
}, },

View File

@ -507,7 +507,7 @@ pub const MouseVisibility = enum(c_int) {
}; };
pub const MouseOverLink = struct { pub const MouseOverLink = struct {
url: []const u8, url: [:0]const u8,
// Sync with: ghostty_action_mouse_over_link_s // Sync with: ghostty_action_mouse_over_link_s
pub const C = extern struct { pub const C = extern struct {

View File

@ -463,6 +463,7 @@ pub const Application = extern struct {
value.config, value.config,
), ),
.mouse_over_link => Action.mouseOverLink(target, value),
.mouse_shape => Action.mouseShape(target, value), .mouse_shape => Action.mouseShape(target, value),
.mouse_visibility => Action.mouseVisibility(target, value), .mouse_visibility => Action.mouseVisibility(target, value),
@ -505,7 +506,6 @@ pub const Application = extern struct {
.present_terminal, .present_terminal,
.initial_size, .initial_size,
.size_limit, .size_limit,
.mouse_over_link,
.toggle_tab_overview, .toggle_tab_overview,
.toggle_split_zoom, .toggle_split_zoom,
.toggle_window_decorations, .toggle_window_decorations,
@ -1015,6 +1015,25 @@ const Action = struct {
} }
} }
pub fn mouseOverLink(
target: apprt.Target,
value: apprt.action.MouseOverLink,
) void {
switch (target) {
.app => log.warn("mouse over link to app is unexpected", .{}),
.surface => |surface| {
var v = gobject.ext.Value.new([:0]const u8);
if (value.url.len > 0) gobject.ext.Value.set(&v, value.url);
defer v.unset();
gobject.Object.setProperty(
surface.rt_surface.gobj().as(gobject.Object),
"mouse-hover-url",
&v,
);
},
}
}
pub fn mouseShape( pub fn mouseShape(
target: apprt.Target, target: apprt.Target,
shape: terminal.MouseShape, shape: terminal.MouseShape,

View File

@ -94,6 +94,23 @@ pub const Surface = extern struct {
); );
}; };
pub const @"mouse-hover-url" = struct {
pub const name = "mouse-hover-url";
pub const get = impl.get;
pub const set = impl.set;
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.nick = "Mouse Hover URL",
.blurb = "The URL the mouse is currently hovering over (if any).",
.default = null,
.accessor = C.privateStringFieldAccessor("mouse_hover_url"),
},
);
};
pub const pwd = struct { pub const pwd = struct {
pub const name = "pwd"; pub const name = "pwd";
pub const get = impl.get; pub const get = impl.get;
@ -162,6 +179,9 @@ pub const Surface = extern struct {
/// Whether the mouse should be hidden or not as requested externally. /// Whether the mouse should be hidden or not as requested externally.
mouse_hidden: bool = false, mouse_hidden: bool = false,
/// The URL that the mouse is currently hovering over.
mouse_hover_url: ?[:0]const u8 = null,
/// The current working directory. This has to be reported externally, /// The current working directory. This has to be reported externally,
/// usually by shell integration which then talks to libghostty /// usually by shell integration which then talks to libghostty
/// which triggers this property. /// which triggers this property.
@ -170,10 +190,18 @@ pub const Surface = extern struct {
/// The title of this surface, if any has been set. /// The title of this surface, if any has been set.
title: ?[:0]const u8 = null, title: ?[:0]const u8 = null,
/// The overlay we use for things such as the URL hover label
/// or resize box. Bound from the template.
overlay: *gtk.Overlay = undefined,
/// The GLAarea that renders the actual surface. This is a binding /// The GLAarea that renders the actual surface. This is a binding
/// to the template so it doesn't have to be unrefed manually. /// to the template so it doesn't have to be unrefed manually.
gl_area: *gtk.GLArea = undefined, gl_area: *gtk.GLArea = undefined,
/// The labels for the left/right sides of the URL hover tooltip.
url_left: *gtk.Label = undefined,
url_right: *gtk.Label = undefined,
/// The apprt Surface. /// The apprt Surface.
rt_surface: ApprtSurface = undefined, rt_surface: ApprtSurface = undefined,
@ -809,6 +837,13 @@ pub const Surface = extern struct {
); );
// Some property signals // Some property signals
_ = gobject.Object.signals.notify.connect(
self,
?*anyopaque,
&propMouseHoverUrl,
null,
.{ .detail = "mouse-hover-url" },
);
_ = gobject.Object.signals.notify.connect( _ = gobject.Object.signals.notify.connect(
self, self,
?*anyopaque, ?*anyopaque,
@ -823,6 +858,41 @@ pub const Surface = extern struct {
null, null,
.{ .detail = "mouse-shape" }, .{ .detail = "mouse-shape" },
); );
// Some other initialization steps
self.initUrlOverlay();
}
fn initUrlOverlay(self: *Self) void {
const priv = self.private();
const overlay = priv.overlay;
const url_left = priv.url_left.as(gtk.Widget);
const url_right = priv.url_right.as(gtk.Widget);
// Add the url label to the overlay
overlay.addOverlay(url_left);
overlay.addOverlay(url_right);
// Setup a motion controller to handle moving the label
// to avoid the mouse.
const ec_motion = gtk.EventControllerMotion.new();
errdefer ec_motion.unref();
url_left.addController(ec_motion.as(gtk.EventController));
errdefer url_left.removeController(ec_motion.as(gtk.EventController));
_ = gtk.EventControllerMotion.signals.enter.connect(
ec_motion,
*Self,
ecUrlMouseEnter,
self,
.{},
);
_ = gtk.EventControllerMotion.signals.leave.connect(
ec_motion,
*Self,
ecUrlMouseLeave,
self,
.{},
);
} }
fn dispose(self: *Self) callconv(.C) void { fn dispose(self: *Self) callconv(.C) void {
@ -863,6 +933,10 @@ pub const Surface = extern struct {
priv.core_surface = null; priv.core_surface = null;
} }
if (priv.mouse_hover_url) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.mouse_hover_url = null;
}
if (priv.pwd) |v| { if (priv.pwd) |v| {
glib.free(@constCast(@ptrCast(v))); glib.free(@constCast(@ptrCast(v)));
priv.pwd = null; priv.pwd = null;
@ -886,6 +960,16 @@ pub const Surface = extern struct {
return self.private().title; return self.private().title;
} }
fn propMouseHoverUrl(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
const visible = if (priv.mouse_hover_url) |v| v.len > 0 else false;
priv.url_left.as(gtk.Widget).setVisible(if (visible) 1 else 0);
}
fn propMouseHidden( fn propMouseHidden(
self: *Self, self: *Self,
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
@ -1539,6 +1623,26 @@ pub const Surface = extern struct {
priv.core_surface = surface; priv.core_surface = surface;
} }
fn ecUrlMouseEnter(
_: *gtk.EventControllerMotion,
_: f64,
_: f64,
self: *Self,
) callconv(.c) void {
const priv = self.private();
const right = priv.url_right.as(gtk.Widget);
right.setVisible(1);
}
fn ecUrlMouseLeave(
_: *gtk.EventControllerMotion,
self: *Self,
) callconv(.c) void {
const priv = self.private();
const right = priv.url_right.as(gtk.Widget);
right.setVisible(0);
}
const C = Common(Self, Private); const C = Common(Self, Private);
pub const as = C.as; pub const as = C.as;
pub const ref = C.ref; pub const ref = C.ref;
@ -1561,13 +1665,17 @@ pub const Surface = extern struct {
); );
// Bindings // Bindings
class.bindTemplateChildPrivate("overlay", .{});
class.bindTemplateChildPrivate("gl_area", .{}); class.bindTemplateChildPrivate("gl_area", .{});
class.bindTemplateChildPrivate("url_left", .{});
class.bindTemplateChildPrivate("url_right", .{});
// Properties // Properties
gobject.ext.registerProperties(class, &.{ gobject.ext.registerProperties(class, &.{
properties.config.impl, properties.config.impl,
properties.@"mouse-shape".impl, properties.@"mouse-shape".impl,
properties.@"mouse-hidden".impl, properties.@"mouse-hidden".impl,
properties.@"mouse-hover-url".impl,
properties.pwd.impl, properties.pwd.impl,
properties.title.impl, properties.title.impl,
}); });

View File

@ -4,6 +4,21 @@
* https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles * https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles
*/ */
label { label.url-overlay {
color: red; padding: 4px 8px 4px 8px;
outline-style: solid;
outline-color: #555555;
outline-width: 1px;
}
label.url-overlay:hover {
opacity: 0;
}
label.url-overlay.left {
border-radius: 0px 6px 0px 0px;
}
label.url-overlay.right {
border-radius: 6px 0px 0px 0px;
} }

View File

@ -2,7 +2,11 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $GhosttySurface: Adw.Bin { template $GhosttySurface: Adw.Bin {
Overlay { // We need to wrap our Overlay one more time because if you bind a
// direct child of your widget to a property, it will double free:
// https://gitlab.gnome.org/GNOME/gtk/-/blob/847571a1e314aba79260e4ef282e2ed9ba91a0d9/gtk/gtkwidget.c#L11423-11425
Adw.Bin {
Overlay overlay {
focusable: false; focusable: false;
focus-on-click: false; focus-on-click: false;
@ -13,4 +17,28 @@ template $GhosttySurface: Adw.Bin {
focus-on-click: true; focus-on-click: true;
} }
} }
}
}
// The label that shows the currently hovered URL.
Label url_left {
styles [
"url-overlay",
]
visible: false;
halign: start;
valign: end;
label: bind template.mouse-hover-url;
}
Label url_right {
styles [
"url-overlay",
]
visible: false;
halign: end;
valign: end;
label: bind template.mouse-hover-url;
} }