mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
apprt/gtk-ng: hovered url overlay (#8010)
I was going to do more overlays but this introduced enough new complexity that I want to PR this on its own. Valgrind clean!
This commit is contained in:
@ -1316,9 +1316,7 @@ fn mouseRefreshLinks(
|
||||
break :link .{ null, false };
|
||||
};
|
||||
break :link .{
|
||||
.{
|
||||
.url = uri,
|
||||
},
|
||||
.{ .url = try alloc.dupeZ(u8, uri) },
|
||||
self.config.link_previews != .false,
|
||||
};
|
||||
},
|
||||
|
@ -507,7 +507,7 @@ pub const MouseVisibility = enum(c_int) {
|
||||
};
|
||||
|
||||
pub const MouseOverLink = struct {
|
||||
url: []const u8,
|
||||
url: [:0]const u8,
|
||||
|
||||
// Sync with: ghostty_action_mouse_over_link_s
|
||||
pub const C = extern struct {
|
||||
|
@ -16,6 +16,9 @@ pub const app_id = "com.mitchellh.ghostty";
|
||||
/// minimum adwaita version.
|
||||
pub const ui_path = "src/apprt/gtk-ng/ui";
|
||||
|
||||
/// The path to the CSS files.
|
||||
pub const css_path = "src/apprt/gtk-ng/css";
|
||||
|
||||
/// The possible icon sizes we'll embed into the gresource file.
|
||||
/// If any size doesn't exist then it will be an error. We could
|
||||
/// infer this completely from available files but we wouldn't be
|
||||
@ -36,6 +39,14 @@ pub const blueprints: []const Blueprint = &.{
|
||||
.{ .major = 1, .minor = 5, .name = "window" },
|
||||
};
|
||||
|
||||
/// CSS files in css_path
|
||||
pub const css = [_][]const u8{
|
||||
"style.css",
|
||||
// "style-dark.css",
|
||||
// "style-hc.css",
|
||||
// "style-hc-dark.css",
|
||||
};
|
||||
|
||||
pub const Blueprint = struct {
|
||||
major: u16,
|
||||
minor: u16,
|
||||
@ -45,7 +56,7 @@ pub const Blueprint = struct {
|
||||
/// The list of filepaths that we depend on. Used for the build
|
||||
/// system to have proper caching.
|
||||
pub const file_inputs = deps: {
|
||||
const total = (icon_sizes.len * 2) + blueprints.len;
|
||||
const total = (icon_sizes.len * 2) + blueprints.len + css.len;
|
||||
var deps: [total][]const u8 = undefined;
|
||||
var index: usize = 0;
|
||||
for (icon_sizes) |size| {
|
||||
@ -62,6 +73,10 @@ pub const file_inputs = deps: {
|
||||
});
|
||||
index += 1;
|
||||
}
|
||||
for (css) |name| {
|
||||
deps[index] = std.fmt.comptimePrint("{s}/{s}", .{ css_path, name });
|
||||
index += 1;
|
||||
}
|
||||
break :deps deps;
|
||||
};
|
||||
|
||||
@ -120,6 +135,7 @@ pub fn main() !void {
|
||||
\\
|
||||
);
|
||||
|
||||
try genRoot(writer);
|
||||
try genIcons(writer);
|
||||
try genUi(alloc, writer, &ui_files);
|
||||
|
||||
@ -173,6 +189,34 @@ fn genIcons(writer: anytype) !void {
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate the resources at the root prefix.
|
||||
fn genRoot(writer: anytype) !void {
|
||||
try writer.print(
|
||||
\\ <gresource prefix="{s}">
|
||||
\\
|
||||
, .{prefix});
|
||||
|
||||
const cwd = std.fs.cwd();
|
||||
inline for (css) |name| {
|
||||
const source = std.fmt.comptimePrint(
|
||||
"{s}/{s}",
|
||||
.{ css_path, name },
|
||||
);
|
||||
try cwd.access(source, .{});
|
||||
try writer.print(
|
||||
\\ <file compressed="true" alias="{s}">{s}</file>
|
||||
\\
|
||||
,
|
||||
.{ name, source },
|
||||
);
|
||||
}
|
||||
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate all the UI resources. This works by looking up all the
|
||||
/// blueprint files in `${ui_path}/{major}.{minor}/{name}.blp` and
|
||||
/// assuming these will be
|
||||
|
@ -463,6 +463,7 @@ pub const Application = extern struct {
|
||||
value.config,
|
||||
),
|
||||
|
||||
.mouse_over_link => Action.mouseOverLink(target, value),
|
||||
.mouse_shape => Action.mouseShape(target, value),
|
||||
.mouse_visibility => Action.mouseVisibility(target, value),
|
||||
|
||||
@ -484,6 +485,8 @@ pub const Application = extern struct {
|
||||
|
||||
.set_title => Action.setTitle(target, value),
|
||||
|
||||
.show_gtk_inspector => Action.showGtkInspector(),
|
||||
|
||||
// Unimplemented but todo on gtk-ng branch
|
||||
.close_window,
|
||||
.toggle_maximize,
|
||||
@ -499,12 +502,10 @@ pub const Application = extern struct {
|
||||
.open_config,
|
||||
.reload_config,
|
||||
.inspector,
|
||||
.show_gtk_inspector,
|
||||
.desktop_notification,
|
||||
.present_terminal,
|
||||
.initial_size,
|
||||
.size_limit,
|
||||
.mouse_over_link,
|
||||
.toggle_tab_overview,
|
||||
.toggle_split_zoom,
|
||||
.toggle_window_decorations,
|
||||
@ -1014,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(
|
||||
target: apprt.Target,
|
||||
shape: terminal.MouseShape,
|
||||
@ -1115,6 +1135,10 @@ const Action = struct {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn showGtkInspector() void {
|
||||
gtk.Window.setInteractiveDebugging(@intFromBool(true));
|
||||
}
|
||||
};
|
||||
|
||||
/// This sets various GTK-related environment variables as necessary
|
||||
|
@ -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 name = "pwd";
|
||||
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.
|
||||
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,
|
||||
/// usually by shell integration which then talks to libghostty
|
||||
/// which triggers this property.
|
||||
@ -170,10 +190,18 @@ pub const Surface = extern struct {
|
||||
/// The title of this surface, if any has been set.
|
||||
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
|
||||
/// to the template so it doesn't have to be unrefed manually.
|
||||
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.
|
||||
rt_surface: ApprtSurface = undefined,
|
||||
|
||||
@ -809,6 +837,13 @@ pub const Surface = extern struct {
|
||||
);
|
||||
|
||||
// Some property signals
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
self,
|
||||
?*anyopaque,
|
||||
&propMouseHoverUrl,
|
||||
null,
|
||||
.{ .detail = "mouse-hover-url" },
|
||||
);
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
self,
|
||||
?*anyopaque,
|
||||
@ -823,6 +858,41 @@ pub const Surface = extern struct {
|
||||
null,
|
||||
.{ .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 {
|
||||
@ -863,6 +933,10 @@ pub const Surface = extern struct {
|
||||
priv.core_surface = null;
|
||||
}
|
||||
|
||||
if (priv.mouse_hover_url) |v| {
|
||||
glib.free(@constCast(@ptrCast(v)));
|
||||
priv.mouse_hover_url = null;
|
||||
}
|
||||
if (priv.pwd) |v| {
|
||||
glib.free(@constCast(@ptrCast(v)));
|
||||
priv.pwd = null;
|
||||
@ -886,6 +960,16 @@ pub const Surface = extern struct {
|
||||
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(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
@ -1539,6 +1623,26 @@ pub const Surface = extern struct {
|
||||
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);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
@ -1561,13 +1665,17 @@ pub const Surface = extern struct {
|
||||
);
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("overlay", .{});
|
||||
class.bindTemplateChildPrivate("gl_area", .{});
|
||||
class.bindTemplateChildPrivate("url_left", .{});
|
||||
class.bindTemplateChildPrivate("url_right", .{});
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.config.impl,
|
||||
properties.@"mouse-shape".impl,
|
||||
properties.@"mouse-hidden".impl,
|
||||
properties.@"mouse-hover-url".impl,
|
||||
properties.pwd.impl,
|
||||
properties.title.impl,
|
||||
});
|
||||
|
24
src/apprt/gtk-ng/css/style.css
Normal file
24
src/apprt/gtk-ng/css/style.css
Normal file
@ -0,0 +1,24 @@
|
||||
/* Application CSS that applies to the entire application.
|
||||
*
|
||||
* This is automatically loaded by AdwApplication:
|
||||
* https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles
|
||||
*/
|
||||
|
||||
label.url-overlay {
|
||||
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;
|
||||
}
|
@ -2,7 +2,14 @@ using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
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;
|
||||
focus-on-click: false;
|
||||
|
||||
GLArea gl_area {
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
@ -11,3 +18,27 @@ template $GhosttySurface: Adw.Bin {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user