diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/apprt/gtk/class/tab.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/apprt/gtk/class/tab.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/apprt/gtk/class/tab.zig | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/tab.zig b/vendor/libghostty-vt/src/apprt/gtk/class/tab.zig new file mode 100644 index 00000000..0c60c8cc --- /dev/null +++ b/vendor/libghostty-vt/src/apprt/gtk/class/tab.zig @@ -0,0 +1,592 @@ +const std = @import("std"); +const adw = @import("adw"); +const gio = @import("gio"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const configpkg = @import("../../../config.zig"); +const apprt = @import("../../../apprt.zig"); +const CoreSurface = @import("../../../Surface.zig"); +const ext = @import("../ext.zig"); +const gresource = @import("../build/gresource.zig"); +const Common = @import("../class.zig").Common; +const Config = @import("config.zig").Config; +const Application = @import("application.zig").Application; +const SplitTree = @import("split_tree.zig").SplitTree; +const Surface = @import("surface.zig").Surface; +const TitleDialog = @import("title_dialog.zig").TitleDialog; + +const log = std.log.scoped(.gtk_ghostty_window); + +pub const Tab = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = gtk.Box; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyTab", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + /// The active surface is the surface that should be receiving all + /// surface-targeted actions. This is usually the focused surface, + /// but may also not be focused if the user has selected a non-surface + /// widget. + pub const @"active-surface" = struct { + pub const name = "active-surface"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Surface, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*Surface, + .{ + .getter = Self.getActiveSurface, + }, + ), + }, + ); + }; + + pub const config = struct { + pub const name = "config"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Config, + .{ + .accessor = C.privateObjFieldAccessor("config"), + }, + ); + }; + + 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( + name, + Self, + ?*Surface.Tree, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*Surface.Tree, + .{ + .getter = getSurfaceTree, + }, + ), + }, + ); + }; + + 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"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .default = null, + .accessor = C.privateStringFieldAccessor("title"), + }, + ); + }; + pub const @"title-override" = struct { + pub const name = "title-override"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .default = null, + .accessor = C.privateStringFieldAccessor("title_override"), + }, + ); + }; + }; + + pub const signals = struct { + /// Emitted whenever the tab would like to be closed. + pub const @"close-request" = struct { + pub const name = "close-request"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + }; + + const Private = struct { + /// The configuration that this surface is using. + config: ?*Config = null, + + /// The title of this tab. This is usually bound to the active surface. + title: ?[:0]const u8 = null, + + /// The manually overridden title from `promptTabTitle`. + title_override: ?[:0]const u8 = null, + + /// The tooltip of this tab. This is usually bound to the active surface. + tooltip: ?[:0]const u8 = null, + + // Template bindings + split_tree: *SplitTree, + + pub var offset: c_int = 0; + }; + + /// Set the parent of this tab page. This only affects the first surface + /// ever created for a tab. If a surface was already created this does + /// nothing. + pub fn setParent(self: *Self, parent: *CoreSurface) void { + self.setParentWithContext(parent, .tab); + } + + pub fn setParentWithContext(self: *Self, parent: *CoreSurface, context: apprt.surface.NewSurfaceContext) void { + if (self.getActiveSurface()) |surface| { + surface.setParent(parent, context); + } + } + + pub fn new(config: ?*Config, overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }) *Self { + const tab = gobject.ext.newInstance(Tab, .{}); + + const priv: *Private = tab.private(); + + if (config) |c| priv.config = c.ref(); + + // If our configuration is null then we get the configuration + // from the application. + if (priv.config == null) { + const app = Application.default(); + priv.config = app.getConfig(); + } + + tab.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); + + // Create our initial surface in the split tree. + priv.split_tree.newSplit(.right, null, .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }) catch |err| switch (err) { + error.OutOfMemory => { + // TODO: We should make our "no surfaces" state more aesthetically + // pleasing and show something like an "Oops, something went wrong" + // message. For now, this is incredibly unlikely. + @panic("oom"); + }, + }; + + return tab; + } + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // Init our actions + self.initActionMap(); + } + + fn initActionMap(self: *Self) void { + const s_param_type = glib.ext.VariantType.newFor([:0]const u8); + defer s_param_type.free(); + + const actions = [_]ext.actions.Action(Self){ + .init("close", actionClose, s_param_type), + .init("ring-bell", actionRingBell, null), + .init("next-page", actionNextPage, null), + .init("previous-page", actionPreviousPage, null), + .init("prompt-tab-title", actionPromptTabTitle, null), + }; + + _ = ext.actions.addAsGroup(Self, self, "tab", &actions); + } + + //--------------------------------------------------------------- + // Properties + + /// Overridden title. This will be generally be shown over the title + /// unless this is unset (null). + pub fn setTitleOverride(self: *Self, title: ?[:0]const u8) void { + const priv = self.private(); + if (priv.title_override) |v| glib.free(@ptrCast(@constCast(v))); + priv.title_override = null; + if (title) |v| priv.title_override = glib.ext.dupeZ(u8, v); + self.as(gobject.Object).notifyByPspec(properties.@"title-override".impl.param_spec); + } + fn titleDialogSet( + _: *TitleDialog, + title_ptr: [*:0]const u8, + self: *Self, + ) callconv(.c) void { + const title = std.mem.span(title_ptr); + self.setTitleOverride(if (title.len == 0) null else title); + } + pub fn promptTabTitle(self: *Self) void { + const priv = self.private(); + const dialog = TitleDialog.new(.tab, priv.title_override orelse priv.title); + _ = TitleDialog.signals.set.connect( + dialog, + *Self, + titleDialogSet, + self, + .{}, + ); + + dialog.present(self.as(gtk.Widget)); + } + + /// Get the currently active surface. See the "active-surface" property. + /// This does not ref the value. + pub fn getActiveSurface(self: *Self) ?*Surface { + return self.getSplitTree().getActiveSurface(); + } + + /// Get the surface tree of this tab. + pub fn getSurfaceTree(self: *Self) ?*Surface.Tree { + const priv = self.private(); + return priv.split_tree.getTree(); + } + + /// Get the split tree widget that is in this tab. + pub fn getSplitTree(self: *Self) *SplitTree { + const priv = self.private(); + return priv.split_tree; + } + + /// Returns true if this tab needs confirmation before quitting based + /// on the various Ghostty configurations. + pub fn getNeedsConfirmQuit(self: *Self) bool { + const tree = self.getSplitTree(); + return tree.getNeedsConfirmQuit(); + } + + /// Get the tab view holding this tab, if any. + fn getTabView(self: *Self) ?*adw.TabView { + return ext.getAncestor( + adw.TabView, + self.as(gtk.Widget), + ); + } + + /// Get the tab page holding this tab, if any. + fn getTabPage(self: *Self) ?*adw.TabPage { + const tab_view = self.getTabView() orelse return null; + return tab_view.getPage(self.as(gtk.Widget)); + } + + //--------------------------------------------------------------- + // Virtual methods + + fn dispose(self: *Self) callconv(.c) void { + const priv = self.private(); + if (priv.config) |v| { + v.unref(); + priv.config = null; + } + + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + fn finalize(self: *Self) callconv(.c) void { + const priv = self.private(); + if (priv.tooltip) |v| { + glib.free(@ptrCast(@constCast(v))); + priv.tooltip = null; + } + if (priv.title) |v| { + glib.free(@ptrCast(@constCast(v))); + priv.title = null; + } + if (priv.title_override) |v| { + glib.free(@ptrCast(@constCast(v))); + priv.title_override = null; + } + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + //--------------------------------------------------------------- + // Signal handlers + + fn propSplitTree( + _: *SplitTree, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + self.as(gobject.Object).notifyByPspec(properties.@"surface-tree".impl.param_spec); + + // If our tree is empty we close the tab. + const tree: *const Surface.Tree = self.getSurfaceTree() orelse &.empty; + if (tree.isEmpty()) { + signals.@"close-request".impl.emit( + self, + null, + .{}, + null, + ); + return; + } + } + + fn propActiveSurface( + _: *SplitTree, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec); + } + + fn actionClose( + _: *gio.SimpleAction, + param_: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + const param = param_ orelse { + log.warn("tab.close-tab called without a parameter", .{}); + return; + }; + + var str: ?[*:0]const u8 = null; + param.get("&s", &str); + + const tab_view = self.getTabView() orelse return; + const page = tab_view.getPage(self.as(gtk.Widget)); + + const mode = std.meta.stringToEnum( + apprt.action.CloseTabMode, + std.mem.span( + str orelse { + log.warn("invalid mode provided to tab.close-tab", .{}); + return; + }, + ), + ) orelse { + // Need to be defensive here since actions can be triggered externally. + log.warn("invalid mode provided to tab.close-tab: {s}", .{str.?}); + return; + }; + + // Delegate to our parent to handle this, since this will emit + // a close-page signal that the parent can intercept. + switch (mode) { + .this => tab_view.closePage(page), + .other => tab_view.closeOtherPages(page), + .right => tab_view.closePagesAfter(page), + } + } + + fn actionPromptTabTitle( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + self.promptTabTitle(); + } + + fn actionRingBell( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + // Future note: I actually don't like this logic living here at all. + // I think a better approach will be for the ring bell action to + // specify its sending surface and then do all this in the window. + + // If the page is selected already we don't mark it as needing + // attention. We only want to mark unfocused pages. This will then + // clear when the page is selected. + const page = self.getTabPage() orelse return; + if (page.getSelected() != 0) return; + page.setNeedsAttention(@intFromBool(true)); + } + + /// Select the next tab page. + fn actionNextPage( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + const tab_view = self.getTabView() orelse return; + _ = tab_view.selectNextPage(); + } + + /// Select the previous tab page. + fn actionPreviousPage( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + const tab_view = self.getTabView() orelse return; + _ = tab_view.selectPreviousPage(); + } + + fn closureComputedTitle( + _: *Self, + config_: ?*Config, + terminal_: ?[*:0]const u8, + surface_override_: ?[*:0]const u8, + tab_override_: ?[*:0]const u8, + zoomed_: c_int, + bell_ringing_: c_int, + _: *gobject.ParamSpec, + ) callconv(.c) ?[*:0]const u8 { + const zoomed = zoomed_ != 0; + const bell_ringing = bell_ringing_ != 0; + + // Our plain title is the manually tab overridden title if it exists, + // otherwise the overridden title if it exists, otherwise + // the terminal title if it exists, otherwise a default string. + const plain = plain: { + const default = "Ghostty"; + const config_title: ?[*:0]const u8 = title: { + const config = config_ orelse break :title null; + break :title config.get().title orelse null; + }; + + const plain = tab_override_ orelse + surface_override_ orelse + terminal_ orelse + config_title orelse + break :plain default; + break :plain std.mem.span(plain); + }; + + // We don't need a config in every case, but if we don't have a config + // let's just assume something went terribly wrong and use our + // default title. Its easier then guarding on the config existing + // in every case for something so unlikely. + const config = if (config_) |v| v.get() else { + log.warn("config unavailable for computed title, likely bug", .{}); + return glib.ext.dupeZ(u8, plain); + }; + + // Use an allocator to build up our string as we write it. + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); + defer buf.deinit(); + + // If our bell is ringing, then we prefix the bell icon to the title. + if (bell_ringing and config.@"bell-features".title) { + buf.writer.writeAll("🔔 ") catch {}; + } + + // If we're zoomed, prefix with the magnifying glass emoji. + if (zoomed) { + buf.writer.writeAll("🔍 ") catch {}; + } + + buf.writer.writeAll(plain) catch return glib.ext.dupeZ(u8, plain); + return glib.ext.dupeZ(u8, buf.written()); + } + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.c) void { + gobject.ext.ensureType(SplitTree); + gobject.ext.ensureType(Surface); + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + comptime gresource.blueprint(.{ + .major = 1, + .minor = 5, + .name = "tab", + }), + ); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.@"active-surface".impl, + properties.config.impl, + properties.@"split-tree".impl, + properties.@"surface-tree".impl, + properties.title.impl, + properties.@"title-override".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); + + // Signals + signals.@"close-request".impl.register(.{}); + + // Virtual methods + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); + } + + pub const as = C.Class.as; + pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; + }; +}; |
