diff --git a/src/Surface.zig b/src/Surface.zig index a9a7e239f..1ab0fc59e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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, }; }, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index da97fc04b..6f745c080 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -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 { diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index dd9bf7de9..4bd42a742 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -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( + \\ + \\ + , .{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( + \\ {s} + \\ + , + .{ name, source }, + ); + } + + try writer.writeAll( + \\ + \\ + ); +} + /// 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 diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 22a27c57d..f62f346cf 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -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 diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 535087d9a..6231241bb 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -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, }); diff --git a/src/apprt/gtk-ng/css/style.css b/src/apprt/gtk-ng/css/style.css new file mode 100644 index 000000000..e26b4419b --- /dev/null +++ b/src/apprt/gtk-ng/css/style.css @@ -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; +} diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index a91206803..d08c21901 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -2,12 +2,43 @@ using Gtk 4.0; using Adw 1; template $GhosttySurface: Adw.Bin { - Overlay { - GLArea gl_area { - hexpand: true; - vexpand: true; - focusable: true; - focus-on-click: true; + // 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; + focusable: 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; +}