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 .{ null, false };
|
||||||
};
|
};
|
||||||
break :link .{
|
break :link .{
|
||||||
.{
|
.{ .url = try alloc.dupeZ(u8, uri) },
|
||||||
.url = uri,
|
|
||||||
},
|
|
||||||
self.config.link_previews != .false,
|
self.config.link_previews != .false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
||||||
|
@ -16,6 +16,9 @@ pub const app_id = "com.mitchellh.ghostty";
|
|||||||
/// minimum adwaita version.
|
/// minimum adwaita version.
|
||||||
pub const ui_path = "src/apprt/gtk-ng/ui";
|
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.
|
/// 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
|
/// 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
|
/// 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" },
|
.{ .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 {
|
pub const Blueprint = struct {
|
||||||
major: u16,
|
major: u16,
|
||||||
minor: u16,
|
minor: u16,
|
||||||
@ -45,7 +56,7 @@ pub const Blueprint = struct {
|
|||||||
/// The list of filepaths that we depend on. Used for the build
|
/// The list of filepaths that we depend on. Used for the build
|
||||||
/// system to have proper caching.
|
/// system to have proper caching.
|
||||||
pub const file_inputs = deps: {
|
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 deps: [total][]const u8 = undefined;
|
||||||
var index: usize = 0;
|
var index: usize = 0;
|
||||||
for (icon_sizes) |size| {
|
for (icon_sizes) |size| {
|
||||||
@ -62,6 +73,10 @@ pub const file_inputs = deps: {
|
|||||||
});
|
});
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
for (css) |name| {
|
||||||
|
deps[index] = std.fmt.comptimePrint("{s}/{s}", .{ css_path, name });
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
break :deps deps;
|
break :deps deps;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -120,6 +135,7 @@ pub fn main() !void {
|
|||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try genRoot(writer);
|
||||||
try genIcons(writer);
|
try genIcons(writer);
|
||||||
try genUi(alloc, writer, &ui_files);
|
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
|
/// Generate all the UI resources. This works by looking up all the
|
||||||
/// blueprint files in `${ui_path}/{major}.{minor}/{name}.blp` and
|
/// blueprint files in `${ui_path}/{major}.{minor}/{name}.blp` and
|
||||||
/// assuming these will be
|
/// assuming these will be
|
||||||
|
@ -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),
|
||||||
|
|
||||||
@ -484,6 +485,8 @@ pub const Application = extern struct {
|
|||||||
|
|
||||||
.set_title => Action.setTitle(target, value),
|
.set_title => Action.setTitle(target, value),
|
||||||
|
|
||||||
|
.show_gtk_inspector => Action.showGtkInspector(),
|
||||||
|
|
||||||
// Unimplemented but todo on gtk-ng branch
|
// Unimplemented but todo on gtk-ng branch
|
||||||
.close_window,
|
.close_window,
|
||||||
.toggle_maximize,
|
.toggle_maximize,
|
||||||
@ -499,12 +502,10 @@ pub const Application = extern struct {
|
|||||||
.open_config,
|
.open_config,
|
||||||
.reload_config,
|
.reload_config,
|
||||||
.inspector,
|
.inspector,
|
||||||
.show_gtk_inspector,
|
|
||||||
.desktop_notification,
|
.desktop_notification,
|
||||||
.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,
|
||||||
@ -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(
|
pub fn mouseShape(
|
||||||
target: apprt.Target,
|
target: apprt.Target,
|
||||||
shape: terminal.MouseShape,
|
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
|
/// 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 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,
|
||||||
});
|
});
|
||||||
|
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;
|
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;
|
||||||
|
focus-on-click: false;
|
||||||
|
|
||||||
GLArea gl_area {
|
GLArea gl_area {
|
||||||
hexpand: true;
|
hexpand: true;
|
||||||
vexpand: true;
|
vexpand: true;
|
||||||
@ -10,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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user