mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
apprt/gtk-ng: tab tooltips, window-subtitle, split zoom title prefix (#8218)
This brings together all our title-related functionality (so far). To make this all work, I heavily use (abuse?) blueprint bindings with closures. Blueprint sets up property subscription for all closure parameters and all properties in a chain (`a.b.c`). This makes for a really long, deeply nested property access but it saves us literally hundreds (at least 100) lines of `notify` signal subscription boilerplate. This also lets some of these properties be truly dynamic and avoid simply copying around intermediate values up the widget tree. Unfortunately Blueprint's auto-formatter won't let us split property access or function parameters onto separate lines so we're going to have some very, very long lines.
This commit is contained in:
@@ -70,6 +70,24 @@ pub const Tab = extern struct {
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"split-tree" = struct {
|
||||
pub const name = "split-tree";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?*SplitTree,
|
||||
.{
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Self,
|
||||
?*SplitTree,
|
||||
.{
|
||||
.getter = getSplitTree,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"surface-tree" = struct {
|
||||
pub const name = "surface-tree";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
@@ -88,10 +106,21 @@ pub const Tab = extern struct {
|
||||
);
|
||||
};
|
||||
|
||||
pub const tooltip = struct {
|
||||
pub const name = "tooltip";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.default = null,
|
||||
.accessor = C.privateStringFieldAccessor("tooltip"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const title = struct {
|
||||
pub const name = "title";
|
||||
pub const get = impl.get;
|
||||
pub const set = impl.set;
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
@@ -122,12 +151,11 @@ pub const Tab = extern struct {
|
||||
/// The configuration that this surface is using.
|
||||
config: ?*Config = null,
|
||||
|
||||
/// The title to show for this tab. This is usually set to a binding
|
||||
/// with the active surface but can be manually set to anything.
|
||||
/// The title of this tab. This is usually bound to the active surface.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
/// The binding groups for the current active surface.
|
||||
surface_bindings: *gobject.BindingGroup,
|
||||
/// The tooltip of this tab. This is usually bound to the active surface.
|
||||
tooltip: ?[:0]const u8 = null,
|
||||
|
||||
// Template bindings
|
||||
split_tree: *SplitTree,
|
||||
@@ -155,15 +183,6 @@ pub const Tab = extern struct {
|
||||
priv.config = app.getConfig();
|
||||
}
|
||||
|
||||
// Setup binding groups for surface properties
|
||||
priv.surface_bindings = gobject.BindingGroup.new();
|
||||
priv.surface_bindings.bind(
|
||||
"title",
|
||||
self.as(gobject.Object),
|
||||
"title",
|
||||
.{},
|
||||
);
|
||||
|
||||
// Create our initial surface in the split tree.
|
||||
priv.split_tree.newSplit(.right, null) catch |err| switch (err) {
|
||||
error.OutOfMemory => {
|
||||
@@ -213,7 +232,6 @@ pub const Tab = extern struct {
|
||||
v.unref();
|
||||
priv.config = null;
|
||||
}
|
||||
priv.surface_bindings.setSource(null);
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
@@ -228,11 +246,14 @@ pub const Tab = extern struct {
|
||||
|
||||
fn finalize(self: *Self) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
if (priv.tooltip) |v| {
|
||||
glib.free(@constCast(@ptrCast(v)));
|
||||
priv.tooltip = null;
|
||||
}
|
||||
if (priv.title) |v| {
|
||||
glib.free(@constCast(@ptrCast(v)));
|
||||
priv.title = null;
|
||||
}
|
||||
priv.surface_bindings.unref();
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
@@ -267,13 +288,36 @@ pub const Tab = extern struct {
|
||||
_: *gobject.ParamSpec,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
priv.surface_bindings.setSource(null);
|
||||
if (self.getActiveSurface()) |surface| {
|
||||
priv.surface_bindings.setSource(surface.as(gobject.Object));
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
|
||||
}
|
||||
|
||||
fn closureComputedTitle(
|
||||
_: *Self,
|
||||
plain_: ?[*:0]const u8,
|
||||
zoomed_: c_int,
|
||||
) callconv(.c) ?[*:0]const u8 {
|
||||
const zoomed = zoomed_ != 0;
|
||||
const plain = plain: {
|
||||
const default = "Ghostty";
|
||||
const plain = plain_ orelse break :plain default;
|
||||
break :plain std.mem.span(plain);
|
||||
};
|
||||
|
||||
// If we're zoomed, prefix with the magnifying glass emoji.
|
||||
if (zoomed) zoomed: {
|
||||
// This results in an extra allocation (that we free), but I
|
||||
// prefer using the Zig APIs so much more than the libc ones.
|
||||
const alloc = Application.default().allocator();
|
||||
const slice = std.fmt.allocPrint(
|
||||
alloc,
|
||||
"🔍 {s}",
|
||||
.{plain},
|
||||
) catch break :zoomed;
|
||||
defer alloc.free(slice);
|
||||
return glib.ext.dupeZ(u8, slice);
|
||||
}
|
||||
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
|
||||
return glib.ext.dupeZ(u8, plain);
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
@@ -303,14 +347,17 @@ pub const Tab = extern struct {
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.@"active-surface".impl,
|
||||
properties.config.impl,
|
||||
properties.@"split-tree".impl,
|
||||
properties.@"surface-tree".impl,
|
||||
properties.title.impl,
|
||||
properties.tooltip.impl,
|
||||
});
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("split_tree", .{});
|
||||
|
||||
// Template Callbacks
|
||||
class.bindTemplateCallback("computed_title", &closureComputedTitle);
|
||||
class.bindTemplateCallback("notify_active_surface", &propActiveSurface);
|
||||
class.bindTemplateCallback("notify_tree", &propSplitTree);
|
||||
|
||||
|
@@ -416,6 +416,12 @@ pub const Window = extern struct {
|
||||
"title",
|
||||
.{ .sync_create = true },
|
||||
);
|
||||
_ = tab.as(gobject.Object).bindProperty(
|
||||
"tooltip",
|
||||
page.as(gobject.Object),
|
||||
"tooltip",
|
||||
.{ .sync_create = true },
|
||||
);
|
||||
|
||||
// Bind signals
|
||||
const split_tree = tab.getSplitTree();
|
||||
@@ -1060,6 +1066,21 @@ pub const Window = extern struct {
|
||||
});
|
||||
}
|
||||
|
||||
fn closureSubtitle(
|
||||
_: *Self,
|
||||
config_: ?*Config,
|
||||
pwd_: ?[*:0]const u8,
|
||||
) callconv(.c) ?[*:0]const u8 {
|
||||
const config = if (config_) |v| v.get() else return null;
|
||||
return switch (config.@"window-subtitle") {
|
||||
.false => null,
|
||||
.@"working-directory" => pwd: {
|
||||
const pwd = pwd_ orelse return null;
|
||||
break :pwd glib.ext.dupeZ(u8, std.mem.span(pwd));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual methods
|
||||
|
||||
@@ -1783,6 +1804,9 @@ pub const Window = extern struct {
|
||||
|
||||
fn init(class: *Class) callconv(.c) void {
|
||||
gobject.ext.ensureType(DebugWarning);
|
||||
gobject.ext.ensureType(SplitTree);
|
||||
gobject.ext.ensureType(Surface);
|
||||
gobject.ext.ensureType(Tab);
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
@@ -1832,6 +1856,7 @@ pub const Window = extern struct {
|
||||
class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal);
|
||||
class.bindTemplateCallback("notify_scale_factor", &propScaleFactor);
|
||||
class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab);
|
||||
class.bindTemplateCallback("computed_subtitle", &closureSubtitle);
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
|
@@ -8,6 +8,8 @@ template $GhosttyTab: Box {
|
||||
orientation: vertical;
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
title: bind $computed_title(split_tree.active-surface as <$GhosttySurface>.title, split_tree.is-zoomed) as <string>;
|
||||
tooltip: bind split_tree.active-surface as <$GhosttySurface>.pwd;
|
||||
|
||||
$GhosttySplitTree split_tree {
|
||||
notify::active-surface => $notify_active_surface();
|
||||
|
@@ -40,6 +40,13 @@ template $GhosttyWindow: Adw.ApplicationWindow {
|
||||
|
||||
title-widget: Adw.WindowTitle {
|
||||
title: bind template.title;
|
||||
// Blueprint auto-formatter won't let me split this into multiple
|
||||
// lines. Let me explain myself. All parameters to a closure are used
|
||||
// as notifications to recompute the value of the closure. All
|
||||
// elements of a property chain are also subscribed to for changes.
|
||||
// This one long, ugly line saves us from manually building up this
|
||||
// massive notify chain in code.
|
||||
subtitle: bind $computed_subtitle(template.config, tab_view.selected-page.child as <$GhosttyTab>.active-surface as <$GhosttySurface>.pwd) as <string>;
|
||||
};
|
||||
|
||||
[start]
|
||||
|
Reference in New Issue
Block a user