From 288461fbeea2a8f31589684f62e14ce269a1e3cd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 27 Jul 2025 21:28:42 -0700 Subject: [PATCH 01/28] apprt/gtk-ng: setup tab views in blueprint --- src/apprt/gtk-ng/ui/1.5/window.blp | 79 ++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index a7f5d82e0..59e722d76 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -1,6 +1,7 @@ using Gtk 4.0; using Adw 1; - +// TODO:gtk-tabs-location +// TODO:gtk-toolbar-style template $GhosttyWindow: Adw.ApplicationWindow { styles [ "window", @@ -18,38 +19,64 @@ template $GhosttyWindow: Adw.ApplicationWindow { handle-menubar-accel: false; title: bind (template.active-surface as <$GhosttySurface>).title; - content: Box { - orientation: vertical; + content: Adw.TabOverview tab_overview { + enable-new-tab: true; + view: tab_view; - Adw.HeaderBar { - visible: bind template.headerbar-visible; + Adw.ToolbarView { + top-bar-style: raised; + bottom-bar-style: raised; - title-widget: Adw.WindowTitle { - title: bind (template.active-surface as <$GhosttySurface>).title; - }; + [top] + Adw.HeaderBar { + visible: bind template.headerbar-visible; - [end] - Gtk.Box { - Gtk.MenuButton { - notify::active => $notify_menu_active(); - icon-name: "open-menu-symbolic"; - menu-model: main_menu; - tooltip-text: _("Main Menu"); - can-focus: false; + title-widget: Adw.WindowTitle { + title: bind (template.active-surface as <$GhosttySurface>).title; + }; + + [end] + Gtk.Box { + Gtk.ToggleButton { + icon-name: "view-grid-symbolic"; + tooltip-text: _("View Open Tabs"); + active: bind tab_overview.open bidirectional; + can-focus: false; + focus-on-click: false; + } + + Gtk.MenuButton { + notify::active => $notify_menu_active(); + icon-name: "open-menu-symbolic"; + menu-model: main_menu; + tooltip-text: _("Main Menu"); + can-focus: false; + } } } - } - $GhosttyDebugWarning { - visible: bind template.debug; - } + [top] + Adw.TabBar tab_bar { + view: tab_view; + } - Adw.ToastOverlay toast_overlay { - $GhosttySurface surface { - close-request => $surface_close_request(); - clipboard-write => $surface_clipboard_write(); - toggle-fullscreen => $surface_toggle_fullscreen(); - toggle-maximize => $surface_toggle_maximize(); + Box { + orientation: vertical; + + $GhosttyDebugWarning { + visible: bind template.debug; + } + + Adw.ToastOverlay toast_overlay { + Adw.TabView tab_view { + $GhosttySurface surface { + close-request => $surface_close_request(); + clipboard-write => $surface_clipboard_write(); + toggle-fullscreen => $surface_toggle_fullscreen(); + toggle-maximize => $surface_toggle_maximize(); + } + } + } } } }; From e7cab27c38cd71513941ff990bc1f7c897c814ca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 07:13:16 -0700 Subject: [PATCH 02/28] apprt/gtk-ng: tab bar properties --- src/apprt/gtk-ng/class/window.zig | 103 +++++++++++++++++++++++++++-- src/apprt/gtk-ng/ui/1.5/window.blp | 17 +++-- 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index ce40462f8..1a8902bb9 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -128,6 +128,57 @@ pub const Window = extern struct { }, ); }; + + pub const @"tabs-autohide" = struct { + pub const name = "tabs-autohide"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Autohide Tab Bar", + .blurb = "If true, tab bar should autohide.", + .default = true, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = Self.getTabsAutohide, + }), + }, + ); + }; + + pub const @"tabs-wide" = struct { + pub const name = "tabs-wide"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Wide Tabs", + .blurb = "If true, tabs will be in the wide expanded style.", + .default = true, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = Self.getTabsWide, + }), + }, + ); + }; + + pub const @"tabs-visible" = struct { + pub const name = "tabs-visible"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Tab Bar Visiblity", + .blurb = "If true, tab bar should be visible.", + .default = true, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = Self.getTabsVisible, + }), + }, + ); + }; }; const Private = struct { @@ -224,10 +275,18 @@ pub const Window = extern struct { fn syncAppearance(self: *Window) void { // TODO: CSD/SSD - // Trigger our headerbar visibility to refresh - self.as(gobject.Object).notifyByPspec(properties.@"headerbar-visible".impl.param_spec); - // Trigger background opacity to refresh - self.as(gobject.Object).notifyByPspec(properties.@"background-opaque".impl.param_spec); + // Trigger all our dynamic properties that depend on the config. + inline for (&.{ + "background-opaque", + "headerbar-visible", + "tabs-autohide", + "tabs-visible", + "tabs-wide", + }) |key| { + self.as(gobject.Object).notifyByPspec( + @field(properties, key).impl.param_spec, + ); + } } fn toggleCssClass(self: *Window, class: [:0]const u8, value: bool) void { @@ -301,6 +360,37 @@ pub const Window = extern struct { return config.@"background-opacity" >= 1.0; } + fn getTabsAutohide(self: *Self) bool { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return true; + return switch (config.@"window-show-tab-bar") { + // Auto we always autohide... obviously. + .auto => true, + + // Always we never autohide because we always show the tab bar. + .always => false, + + // Never we autohide because it doesn't actually matter, + // since getTabsVisible will return false. + .never => true, + }; + } + + fn getTabsVisible(self: *Self) bool { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return true; + return switch (config.@"window-show-tab-bar") { + .always, .auto => true, + .never => false, + }; + } + + fn getTabsWide(self: *Self) bool { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return true; + return config.@"gtk-wide-tabs"; + } + fn propConfig( _: *adw.ApplicationWindow, _: *gobject.ParamSpec, @@ -611,10 +701,13 @@ pub const Window = extern struct { // Properties gobject.ext.registerProperties(class, &.{ properties.@"active-surface".impl, + properties.@"background-opaque".impl, properties.config.impl, properties.debug.impl, properties.@"headerbar-visible".impl, - properties.@"background-opaque".impl, + properties.@"tabs-autohide".impl, + properties.@"tabs-visible".impl, + properties.@"tabs-wide".impl, }); // Bindings diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 59e722d76..0b3ed8887 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -57,7 +57,10 @@ template $GhosttyWindow: Adw.ApplicationWindow { [top] Adw.TabBar tab_bar { + autohide: bind template.tabs-autohide; + expand-tabs: bind template.tabs-wide; view: tab_view; + visible: bind template.tabs-visible; } Box { @@ -69,11 +72,15 @@ template $GhosttyWindow: Adw.ApplicationWindow { Adw.ToastOverlay toast_overlay { Adw.TabView tab_view { - $GhosttySurface surface { - close-request => $surface_close_request(); - clipboard-write => $surface_clipboard_write(); - toggle-fullscreen => $surface_toggle_fullscreen(); - toggle-maximize => $surface_toggle_maximize(); + Adw.TabPage { + title: bind (template.active-surface as <$GhosttySurface>).title; + + child: $GhosttySurface surface { + close-request => $surface_close_request(); + clipboard-write => $surface_clipboard_write(); + toggle-fullscreen => $surface_toggle_fullscreen(); + toggle-maximize => $surface_toggle_maximize(); + }; } } } From d7e42e6614a885101a605c4f7f4b55152dc03bf7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 07:32:19 -0700 Subject: [PATCH 03/28] apprt/gtk-ng: tab bar top/bottom --- src/apprt/gtk-ng/class/window.zig | 15 +++++++++++++++ src/apprt/gtk-ng/ui/1.5/window.blp | 2 +- valgrind.supp | 25 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 1a8902bb9..ae624afb4 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -187,6 +187,8 @@ pub const Window = extern struct { // Template bindings surface: *Surface, + tab_bar: *adw.TabBar, + toolbar: *adw.ToolbarView, toast_overlay: *adw.ToastOverlay, pub var offset: c_int = 0; @@ -287,6 +289,17 @@ pub const Window = extern struct { @field(properties, key).impl.param_spec, ); } + + // Remainder uses the config + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return; + + // Move the tab bar to the proper location. + priv.toolbar.remove(priv.tab_bar.as(gtk.Widget)); + switch (config.@"gtk-tabs-location") { + .top => priv.toolbar.addTopBar(priv.tab_bar.as(gtk.Widget)), + .bottom => priv.toolbar.addBottomBar(priv.tab_bar.as(gtk.Widget)), + } } fn toggleCssClass(self: *Window, class: [:0]const u8, value: bool) void { @@ -712,6 +725,8 @@ pub const Window = extern struct { // Bindings class.bindTemplateChildPrivate("surface", .{}); + class.bindTemplateChildPrivate("tab_bar", .{}); + class.bindTemplateChildPrivate("toolbar", .{}); class.bindTemplateChildPrivate("toast_overlay", .{}); // Template Callbacks diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 0b3ed8887..b46fd6415 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -23,7 +23,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { enable-new-tab: true; view: tab_view; - Adw.ToolbarView { + Adw.ToolbarView toolbar { top-bar-style: raised; bottom-bar-style: raised; diff --git a/valgrind.supp b/valgrind.supp index 3b074607d..966347ad4 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -142,6 +142,31 @@ ... } +{ + GDK GLArea + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:g_malloc + fun:gdk_memory_texture_from_texture + fun:gdk_gl_texture_release + fun:delete_one_texture + fun:g_list_foreach + fun:g_list_free_full + fun:gtk_gl_area_unrealize + ... +} + +{ + GDK GLArea Snapshot + Memcheck:Leak + match-leak-kinds: definite + fun:*alloc + ... + fun:gtk_gl_area_snapshot + ... +} + { GSK GPU Rendering Memcheck:Leak From bc6cbdc41a1ce4b21708d35efd91e390b19da39f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 07:35:40 -0700 Subject: [PATCH 04/28] apprt/gtk-ng: toolbar style config --- src/apprt/gtk-ng/class/window.zig | 35 +++++++++++++++++++++++++++++- src/apprt/gtk-ng/ui/1.5/window.blp | 4 ++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index ae624afb4..2f9ccb80f 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -170,7 +170,7 @@ pub const Window = extern struct { Self, bool, .{ - .nick = "Tab Bar Visiblity", + .nick = "Tab Bar Visibility", .blurb = "If true, tab bar should be visible.", .default = true, .accessor = gobject.ext.typedAccessor(Self, bool, .{ @@ -179,6 +179,27 @@ pub const Window = extern struct { }, ); }; + + pub const @"toolbar-style" = struct { + pub const name = "toolbar-style"; + const impl = gobject.ext.defineProperty( + name, + Self, + adw.ToolbarStyle, + .{ + .nick = "Toolbar Style", + .blurb = "The style for the toolbar top/bottom bars.", + .default = .raised, + .accessor = gobject.ext.typedAccessor( + Self, + adw.ToolbarStyle, + .{ + .getter = Self.getToolbarStyle, + }, + ), + }, + ); + }; }; const Private = struct { @@ -284,6 +305,7 @@ pub const Window = extern struct { "tabs-autohide", "tabs-visible", "tabs-wide", + "toolbar-style", }) |key| { self.as(gobject.Object).notifyByPspec( @field(properties, key).impl.param_spec, @@ -404,6 +426,16 @@ pub const Window = extern struct { return config.@"gtk-wide-tabs"; } + fn getToolbarStyle(self: *Self) adw.ToolbarStyle { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return .raised; + return switch (config.@"gtk-toolbar-style") { + .flat => .flat, + .raised => .raised, + .@"raised-border" => .raised_border, + }; + } + fn propConfig( _: *adw.ApplicationWindow, _: *gobject.ParamSpec, @@ -721,6 +753,7 @@ pub const Window = extern struct { properties.@"tabs-autohide".impl, properties.@"tabs-visible".impl, properties.@"tabs-wide".impl, + properties.@"toolbar-style".impl, }); // Bindings diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index b46fd6415..8197a8e02 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -24,8 +24,8 @@ template $GhosttyWindow: Adw.ApplicationWindow { view: tab_view; Adw.ToolbarView toolbar { - top-bar-style: raised; - bottom-bar-style: raised; + top-bar-style: bind template.toolbar-style; + bottom-bar-style: bind template.toolbar-style; [top] Adw.HeaderBar { From e768b54d898b7b0f6b53e6415a79f07931e76c66 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 08:36:05 -0700 Subject: [PATCH 05/28] apprt/gtk-ng: new tab button in header --- src/apprt/gtk-ng/ui/1.5/window.blp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 8197a8e02..438d0d428 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -1,7 +1,6 @@ using Gtk 4.0; using Adw 1; -// TODO:gtk-tabs-location -// TODO:gtk-toolbar-style + template $GhosttyWindow: Adw.ApplicationWindow { styles [ "window", @@ -35,6 +34,14 @@ template $GhosttyWindow: Adw.ApplicationWindow { title: bind (template.active-surface as <$GhosttySurface>).title; }; + [start] + Adw.SplitButton { + icon-name: "tab-new-symbolic"; + tooltip-text: _("New Tab"); + dropdown-tooltip: _("New Split"); + menu-model: split_menu; + } + [end] Gtk.Box { Gtk.ToggleButton { From 775f3dfca37b8916d4d930c4d57c1bb7806c45ee Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 10:25:24 -0700 Subject: [PATCH 06/28] apprt/gtk-ng: basic tab creation --- src/apprt/gtk-ng/build/gresource.zig | 5 +- src/apprt/gtk-ng/class/tab.zig | 190 +++++++++++++++++++++++++++ src/apprt/gtk-ng/class/window.zig | 22 ++-- src/apprt/gtk-ng/ui/1.5/tab.blp | 18 +++ src/apprt/gtk-ng/ui/1.5/window.blp | 13 +- 5 files changed, 225 insertions(+), 23 deletions(-) create mode 100644 src/apprt/gtk-ng/class/tab.zig create mode 100644 src/apprt/gtk-ng/ui/1.5/tab.blp diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index 2daa6f20e..0c73f925b 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -37,12 +37,13 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 4, .name = "clipboard-confirmation-dialog" }, .{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" }, .{ .major = 1, .minor = 2, .name = "config-errors-dialog" }, + .{ .major = 1, .minor = 2, .name = "debug-warning" }, + .{ .major = 1, .minor = 3, .name = "debug-warning" }, .{ .major = 1, .minor = 2, .name = "resize-overlay" }, .{ .major = 1, .minor = 2, .name = "surface" }, .{ .major = 1, .minor = 3, .name = "surface-child-exited" }, + .{ .major = 1, .minor = 5, .name = "tab" }, .{ .major = 1, .minor = 5, .name = "window" }, - .{ .major = 1, .minor = 2, .name = "debug-warning" }, - .{ .major = 1, .minor = 3, .name = "debug-warning" }, }; /// CSS files in css_path diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig new file mode 100644 index 000000000..0dc104e0e --- /dev/null +++ b/src/apprt/gtk-ng/class/tab.zig @@ -0,0 +1,190 @@ +const std = @import("std"); +const build_config = @import("../../../build_config.zig"); +const assert = std.debug.assert; +const adw = @import("adw"); +const gio = @import("gio"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const i18n = @import("../../../os/main.zig").i18n; +const apprt = @import("../../../apprt.zig"); +const input = @import("../../../input.zig"); +const CoreSurface = @import("../../../Surface.zig"); +const gtk_version = @import("../gtk_version.zig"); +const adw_version = @import("../adw_version.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 CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; +const Surface = @import("surface.zig").Surface; + +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 focus 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, + .{ + .nick = "Active Surface", + .blurb = "The currently active 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, + .{ + .nick = "Config", + .blurb = "The configuration that this surface is using.", + .accessor = C.privateObjFieldAccessor("config"), + }, + ); + }; + }; + + const Private = struct { + /// The configuration that this surface is using. + config: ?*Config = null, + + // Template bindings + surface: *Surface, + + 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 { + const priv = self.private(); + priv.surface.setParent(parent); + } + + fn init(self: *Self, _: *Class) callconv(.C) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // If our configuration is null then we get the configuration + // from the application. + const priv = self.private(); + if (priv.config == null) { + const app = Application.default(); + priv.config = app.getConfig(); + } + + // We need to do this so that the title initializes properly, + // I think because its a dynamic getter. + self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec); + } + + //--------------------------------------------------------------- + // Properties + + /// Get the currently active surface. See the "active-surface" property. + /// This does not ref the value. + pub fn getActiveSurface(self: *Self) *Surface { + const priv = self.private(); + return priv.surface; + } + + //--------------------------------------------------------------- + // 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), + ); + } + + //--------------------------------------------------------------- + // Signal handlers + + 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(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, + }); + + // Bindings + class.bindTemplateChildPrivate("surface", .{}); + + // Template Callbacks + //class.bindTemplateCallback("close_request", &windowCloseRequest); + + // Virtual methods + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + } + + pub const as = C.Class.as; + pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; + }; +}; diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 2f9ccb80f..b022704a4 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -19,6 +19,7 @@ const Config = @import("config.zig").Config; const Application = @import("application.zig").Application; const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const Surface = @import("surface.zig").Surface; +const Tab = @import("tab.zig").Tab; const DebugWarning = @import("debug_warning.zig").DebugWarning; const log = std.log.scoped(.gtk_ghostty_window); @@ -207,8 +208,8 @@ pub const Window = extern struct { config: ?*Config = null, // Template bindings - surface: *Surface, tab_bar: *adw.TabBar, + tab_view: *adw.TabView, toolbar: *adw.ToolbarView, toast_overlay: *adw.ToastOverlay, @@ -220,10 +221,13 @@ pub const Window = extern struct { .application = app, }); - if (parent_) |parent| { - const priv = self.private(); - priv.surface.setParent(parent); - } + // Create our initial tab + const priv = self.private(); + const tab = gobject.ext.newInstance(Tab, .{ + .config = priv.config, + }); + if (parent_) |p| tab.setParent(p); + _ = priv.tab_view.append(tab.as(gtk.Widget)); return self; } @@ -364,7 +368,8 @@ pub const Window = extern struct { /// This does not ref the value. fn getActiveSurface(self: *Self) ?*Surface { const priv = self.private(); - return priv.surface; + _ = priv; + return null; } fn getHeaderbarVisible(self: *Self) bool { @@ -595,8 +600,8 @@ pub const Window = extern struct { ) callconv(.c) void { // Todo _ = scope; + _ = surface; - assert(surface == self.private().surface); self.as(gtk.Window).close(); } @@ -732,7 +737,6 @@ pub const Window = extern struct { pub const Instance = Self; fn init(class: *Class) callconv(.C) void { - gobject.ext.ensureType(Surface); gobject.ext.ensureType(DebugWarning); gtk.Widget.Class.setTemplateFromResource( class.as(gtk.Widget.Class), @@ -757,8 +761,8 @@ pub const Window = extern struct { }); // Bindings - class.bindTemplateChildPrivate("surface", .{}); class.bindTemplateChildPrivate("tab_bar", .{}); + class.bindTemplateChildPrivate("tab_view", .{}); class.bindTemplateChildPrivate("toolbar", .{}); class.bindTemplateChildPrivate("toast_overlay", .{}); diff --git a/src/apprt/gtk-ng/ui/1.5/tab.blp b/src/apprt/gtk-ng/ui/1.5/tab.blp new file mode 100644 index 000000000..54a1be630 --- /dev/null +++ b/src/apprt/gtk-ng/ui/1.5/tab.blp @@ -0,0 +1,18 @@ +using Gtk 4.0; + +template $GhosttyTab: Box { + styles [ + "tab", + ] + + hexpand: true; + vexpand: true; + // A tab currently just contains a surface directly. When we introduce + // splits we probably want to replace this with the split widget type. + $GhosttySurface surface { + // close-request => $surface_close_request(); + // clipboard-write => $surface_clipboard_write(); + // toggle-fullscreen => $surface_toggle_fullscreen(); + // toggle-maximize => $surface_toggle_maximize(); + } +} diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 438d0d428..f4b8b5d36 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -78,18 +78,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { } Adw.ToastOverlay toast_overlay { - Adw.TabView tab_view { - Adw.TabPage { - title: bind (template.active-surface as <$GhosttySurface>).title; - - child: $GhosttySurface surface { - close-request => $surface_close_request(); - clipboard-write => $surface_clipboard_write(); - toggle-fullscreen => $surface_toggle_fullscreen(); - toggle-maximize => $surface_toggle_maximize(); - }; - } - } + Adw.TabView tab_view {} } } } From fa45f971f4e16920870234f49407271b554ce507 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 12:15:57 -0700 Subject: [PATCH 07/28] apprt/gtk-ng: title bindings --- src/apprt/gtk-ng/class/tab.zig | 53 ++++++++++++++++++++++++++++++ src/apprt/gtk-ng/class/window.zig | 44 ++++++++++++++++++++++++- src/apprt/gtk-ng/ui/1.5/window.blp | 7 ++-- 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 0dc104e0e..3d8437115 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -72,12 +72,36 @@ pub const Tab = extern struct { }, ); }; + + 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, + ?[:0]const u8, + .{ + .nick = "Title", + .blurb = "The title of the active surface.", + .default = null, + .accessor = C.privateStringFieldAccessor("title"), + }, + ); + }; }; const Private = struct { /// The configuration that this surface is using. config: ?*Config = null, + /// The title to show for this tab. This is usally set to a binding + /// with the active surface but can be manually set to anything. + title: ?[:0]const u8 = null, + + /// The binding groups for the current active surface. + surface_bindings: *gobject.BindingGroup, + // Template bindings surface: *Surface, @@ -106,6 +130,19 @@ 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", + .{}, + ); + + // TODO: Eventually this should be set dynamically based on the + // current active surface. + priv.surface_bindings.setSource(priv.surface.as(gobject.Object)); + // We need to do this so that the title initializes properly, // I think because its a dynamic getter. self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec); @@ -130,6 +167,7 @@ pub const Tab = extern struct { v.unref(); priv.config = null; } + priv.surface_bindings.setSource(null); gtk.Widget.disposeTemplate( self.as(gtk.Widget), @@ -142,6 +180,19 @@ pub const Tab = extern struct { ); } + fn finalize(self: *Self) callconv(.C) void { + const priv = self.private(); + if (priv.title) |v| { + glib.free(@constCast(@ptrCast(v))); + priv.title = null; + } + priv.surface_bindings.unref(); + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } //--------------------------------------------------------------- // Signal handlers @@ -171,6 +222,7 @@ pub const Tab = extern struct { gobject.ext.registerProperties(class, &.{ properties.@"active-surface".impl, properties.config.impl, + properties.title.impl, }); // Bindings @@ -181,6 +233,7 @@ pub const Tab = extern struct { // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); } pub const as = C.Class.as; diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index b022704a4..548a8b664 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -204,6 +204,9 @@ pub const Window = extern struct { }; const Private = struct { + /// Binding group for our active tab. + tab_bindings: *gobject.BindingGroup, + /// The configuration that this surface is using. config: ?*Config = null, @@ -221,7 +224,9 @@ pub const Window = extern struct { .application = app, }); - // Create our initial tab + // Create our initial tab. This will trigger the selected-page + // signal handler which will setup the remainder of the bindings + // for this to all work. const priv = self.private(); const tab = gobject.ext.newInstance(Tab, .{ .config = priv.config, @@ -248,6 +253,10 @@ pub const Window = extern struct { self.as(gtk.Widget).addCssClass("devel"); } + // Setup some of our objects that are never null + priv.tab_bindings = gobject.BindingGroup.new(); + priv.tab_bindings.bind("title", self.as(gobject.Object), "title", .{}); + // Set our window icon. We can't set this in the blueprint file // because its dependent on the build config. self.as(gtk.Window).setIconName(build_config.bundle_id); @@ -517,6 +526,7 @@ pub const Window = extern struct { v.unref(); priv.config = null; } + priv.tab_bindings.setSource(null); gtk.Widget.disposeTemplate( self.as(gtk.Widget), @@ -529,6 +539,16 @@ pub const Window = extern struct { ); } + fn finalize(self: *Self) callconv(.C) void { + const priv = self.private(); + priv.tab_bindings.unref(); + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + //--------------------------------------------------------------- // Signal handlers @@ -570,6 +590,26 @@ pub const Window = extern struct { self.as(gtk.Window).destroy(); } + fn tabViewSelectedPage( + _: *adw.TabView, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + const priv = self.private(); + + // Always reset our binding source in case we have no pages. + priv.tab_bindings.setSource(null); + + // Get our current page which MUST be a Tab object. + const page = priv.tab_view.getSelectedPage() orelse return; + const child = page.getChild(); + assert(gobject.ext.isA(child, Tab)); + + // Setup our binding group. This ensures things like the title + // are synced from the active tab. + priv.tab_bindings.setSource(child.as(gobject.Object)); + } + fn surfaceClipboardWrite( _: *Surface, clipboard_type: apprt.Clipboard, @@ -768,6 +808,7 @@ pub const Window = extern struct { // Template Callbacks class.bindTemplateCallback("close_request", &windowCloseRequest); + class.bindTemplateCallback("selected_page", &tabViewSelectedPage); class.bindTemplateCallback("surface_clipboard_write", &surfaceClipboardWrite); class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest); class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen); @@ -780,6 +821,7 @@ pub const Window = extern struct { // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); } pub const as = C.Class.as; diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index f4b8b5d36..dbfcbda96 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -16,7 +16,6 @@ template $GhosttyWindow: Adw.ApplicationWindow { // GTK4 grabs F10 input by default to focus the menubar icon. We want // to disable this so that terminal programs can capture F10 (such as htop) handle-menubar-accel: false; - title: bind (template.active-surface as <$GhosttySurface>).title; content: Adw.TabOverview tab_overview { enable-new-tab: true; @@ -31,7 +30,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { visible: bind template.headerbar-visible; title-widget: Adw.WindowTitle { - title: bind (template.active-surface as <$GhosttySurface>).title; + title: bind template.title; }; [start] @@ -78,7 +77,9 @@ template $GhosttyWindow: Adw.ApplicationWindow { } Adw.ToastOverlay toast_overlay { - Adw.TabView tab_view {} + Adw.TabView tab_view { + notify::selected-page => $selected_page(); + } } } } From 5279badd5bf0d25125da1986afabdd3bede85b70 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 19:46:11 -0700 Subject: [PATCH 08/28] apprt/gtk-ng: bind a bunch on page-attach/detach --- :w | 857 +++++++++++++++++++++++++++++ src/apprt/gtk-ng/class/window.zig | 50 +- src/apprt/gtk-ng/ui/1.5/window.blp | 2 + 3 files changed, 908 insertions(+), 1 deletion(-) create mode 100644 :w diff --git a/:w b/:w new file mode 100644 index 000000000..93782a896 --- /dev/null +++ b/:w @@ -0,0 +1,857 @@ +const std = @import("std"); +const build_config = @import("../../../build_config.zig"); +const assert = std.debug.assert; +const adw = @import("adw"); +const gio = @import("gio"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const i18n = @import("../../../os/main.zig").i18n; +const apprt = @import("../../../apprt.zig"); +const input = @import("../../../input.zig"); +const CoreSurface = @import("../../../Surface.zig"); +const gtk_version = @import("../gtk_version.zig"); +const adw_version = @import("../adw_version.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 CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; +const Surface = @import("surface.zig").Surface; +const Tab = @import("tab.zig").Tab; +const DebugWarning = @import("debug_warning.zig").DebugWarning; + +const log = std.log.scoped(.gtk_ghostty_window); + +pub const Window = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = adw.ApplicationWindow; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyWindow", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + /// The active surface is the focus 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, + .{ + .nick = "Active Surface", + .blurb = "The currently active 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, + .{ + .nick = "Config", + .blurb = "The configuration that this surface is using.", + .accessor = C.privateObjFieldAccessor("config"), + }, + ); + }; + + pub const debug = struct { + pub const name = "debug"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Debug", + .blurb = "True if runtime safety checks are enabled.", + .default = build_config.is_debug, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = struct { + pub fn getter(_: *Window) bool { + return build_config.is_debug; + } + }.getter, + }), + }, + ); + }; + + pub const @"headerbar-visible" = struct { + pub const name = "headerbar-visible"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Headerbar Visible", + .blurb = "True if the headerbar is visible.", + .default = true, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = Self.getHeaderbarVisible, + }), + }, + ); + }; + + pub const @"background-opaque" = struct { + pub const name = "background-opaque"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Background Opaque", + .blurb = "True if the background should be opaque.", + .default = true, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = Self.getBackgroundOpaque, + }), + }, + ); + }; + + pub const @"tabs-autohide" = struct { + pub const name = "tabs-autohide"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Autohide Tab Bar", + .blurb = "If true, tab bar should autohide.", + .default = true, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = Self.getTabsAutohide, + }), + }, + ); + }; + + pub const @"tabs-wide" = struct { + pub const name = "tabs-wide"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Wide Tabs", + .blurb = "If true, tabs will be in the wide expanded style.", + .default = true, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = Self.getTabsWide, + }), + }, + ); + }; + + pub const @"tabs-visible" = struct { + pub const name = "tabs-visible"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .nick = "Tab Bar Visibility", + .blurb = "If true, tab bar should be visible.", + .default = true, + .accessor = gobject.ext.typedAccessor(Self, bool, .{ + .getter = Self.getTabsVisible, + }), + }, + ); + }; + + pub const @"toolbar-style" = struct { + pub const name = "toolbar-style"; + const impl = gobject.ext.defineProperty( + name, + Self, + adw.ToolbarStyle, + .{ + .nick = "Toolbar Style", + .blurb = "The style for the toolbar top/bottom bars.", + .default = .raised, + .accessor = gobject.ext.typedAccessor( + Self, + adw.ToolbarStyle, + .{ + .getter = Self.getToolbarStyle, + }, + ), + }, + ); + }; + }; + + const Private = struct { + /// Binding group for our active tab. + tab_bindings: *gobject.BindingGroup, + + /// The configuration that this surface is using. + config: ?*Config = null, + + // Template bindings + tab_bar: *adw.TabBar, + tab_view: *adw.TabView, + toolbar: *adw.ToolbarView, + toast_overlay: *adw.ToastOverlay, + + pub var offset: c_int = 0; + }; + + pub fn new(app: *Application, parent_: ?*CoreSurface) *Self { + const self = gobject.ext.newInstance(Self, .{ + .application = app, + }); + + // Create our initial tab. This will trigger the selected-page + // signal handler which will setup the remainder of the bindings + // for this to all work. + const priv = self.private(); + const tab = gobject.ext.newInstance(Tab, .{ + .config = priv.config, + }); + if (parent_) |p| tab.setParent(p); + _ = priv.tab_view.append(tab.as(gtk.Widget)); + + return self; + } + + fn init(self: *Self, _: *Class) callconv(.C) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // If our configuration is null then we get the configuration + // from the application. + const priv = self.private(); + if (priv.config == null) { + const app = Application.default(); + priv.config = app.getConfig(); + } + + // Add our dev CSS class if we're in debug mode. + if (comptime build_config.is_debug) { + self.as(gtk.Widget).addCssClass("devel"); + } + + // Setup our tab binding group. This ensures certain properties + // are only synced from the currently active tab. + priv.tab_bindings = gobject.BindingGroup.new(); + priv.tab_bindings.bind("title", self.as(gobject.Object), "title", .{}); + + // Set our window icon. We can't set this in the blueprint file + // because its dependent on the build config. + self.as(gtk.Window).setIconName(build_config.bundle_id); + + // Initialize our actions + self.initActionMap(); + + // We always sync our appearance at the end because loading our + // config and such can affect our bindings which ar setup initially + // in initTemplate. + self.syncAppearance(); + + // We need to do this so that the title initializes properly, + // I think because its a dynamic getter. + self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec); + } + + /// Setup our action map. + fn initActionMap(self: *Self) void { + const actions = .{ + .{ "about", actionAbout, null }, + .{ "close", actionClose, null }, + .{ "new-window", actionNewWindow, null }, + .{ "copy", actionCopy, null }, + .{ "paste", actionPaste, null }, + .{ "reset", actionReset, null }, + .{ "clear", actionClear, null }, + }; + + const action_map = self.as(gio.ActionMap); + inline for (actions) |entry| { + const action = gio.SimpleAction.new( + entry[0], + entry[2], + ); + defer action.unref(); + _ = gio.SimpleAction.signals.activate.connect( + action, + *Self, + entry[1], + self, + .{}, + ); + action_map.addAction(action.as(gio.Action)); + } + } + + /// Updates various appearance properties. This should always be safe + /// to call multiple times. This should be called whenever a change + /// happens that might affect how the window appears (config change, + /// fullscreen, etc.). + fn syncAppearance(self: *Window) void { + // TODO: CSD/SSD + + // Trigger all our dynamic properties that depend on the config. + inline for (&.{ + "background-opaque", + "headerbar-visible", + "tabs-autohide", + "tabs-visible", + "tabs-wide", + "toolbar-style", + }) |key| { + self.as(gobject.Object).notifyByPspec( + @field(properties, key).impl.param_spec, + ); + } + + // Remainder uses the config + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return; + + // Move the tab bar to the proper location. + priv.toolbar.remove(priv.tab_bar.as(gtk.Widget)); + switch (config.@"gtk-tabs-location") { + .top => priv.toolbar.addTopBar(priv.tab_bar.as(gtk.Widget)), + .bottom => priv.toolbar.addBottomBar(priv.tab_bar.as(gtk.Widget)), + } + } + + fn toggleCssClass(self: *Window, class: [:0]const u8, value: bool) void { + const widget = self.as(gtk.Widget); + if (value) + widget.addCssClass(class.ptr) + else + widget.removeCssClass(class.ptr); + } + + /// Perform a binding action on the window's active surface. + fn performBindingAction( + self: *Window, + action: input.Binding.Action, + ) void { + const surface = self.getActiveSurface() orelse return; + const core_surface = surface.core() orelse return; + _ = core_surface.performBindingAction(action) catch |err| { + log.warn("error performing binding action error={}", .{err}); + return; + }; + } + + /// Queue a simple text-based toast. All text-based toasts share the + /// same timeout for consistency. + /// + // This is not `pub` because we should be using signals emitted by + // other widgets to trigger our toasts. Other objects should not + // trigger toasts directly. + fn addToast(self: *Window, title: [*:0]const u8) void { + const toast = adw.Toast.new(title); + toast.setTimeout(3); + self.private().toast_overlay.addToast(toast); + } + + //--------------------------------------------------------------- + // Properties + + /// Get the currently active surface. See the "active-surface" property. + /// This does not ref the value. + fn getActiveSurface(self: *Self) ?*Surface { + const priv = self.private(); + _ = priv; + return null; + } + + fn getHeaderbarVisible(self: *Self) bool { + // TODO: CSD/SSD + // TODO: QuickTerminal + + // If we're fullscreen we never show the header bar. + if (self.as(gtk.Window).isFullscreen() != 0) return false; + + // The remainder needs a config + const config_obj = self.private().config orelse return true; + const config = config_obj.get(); + + // *Conditionally* disable the header bar when maximized, + // and gtk-titlebar-hide-when-maximized is set + if (self.as(gtk.Window).isMaximized() != 0 and + config.@"gtk-titlebar-hide-when-maximized") + { + return false; + } + + return config.@"gtk-titlebar"; + } + + fn getBackgroundOpaque(self: *Self) bool { + const priv = self.private(); + const config = (priv.config orelse return true).get(); + return config.@"background-opacity" >= 1.0; + } + + fn getTabsAutohide(self: *Self) bool { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return true; + return switch (config.@"window-show-tab-bar") { + // Auto we always autohide... obviously. + .auto => true, + + // Always we never autohide because we always show the tab bar. + .always => false, + + // Never we autohide because it doesn't actually matter, + // since getTabsVisible will return false. + .never => true, + }; + } + + fn getTabsVisible(self: *Self) bool { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return true; + return switch (config.@"window-show-tab-bar") { + .always, .auto => true, + .never => false, + }; + } + + fn getTabsWide(self: *Self) bool { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return true; + return config.@"gtk-wide-tabs"; + } + + fn getToolbarStyle(self: *Self) adw.ToolbarStyle { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return .raised; + return switch (config.@"gtk-toolbar-style") { + .flat => .flat, + .raised => .raised, + .@"raised-border" => .raised_border, + }; + } + + fn propConfig( + _: *adw.ApplicationWindow, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + self.addToast(i18n._("Reloaded the configuration")); + self.syncAppearance(); + } + + fn propFullscreened( + _: *adw.ApplicationWindow, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + self.syncAppearance(); + } + + fn propMaximized( + _: *adw.ApplicationWindow, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + self.syncAppearance(); + } + + fn propMenuActive( + button: *gtk.MenuButton, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + // Debian 12 is stuck on GTK 4.8 + if (!gtk_version.atLeast(4, 10, 0)) return; + + // We only care if we're activating. If we're activating then + // we need to check the validity of our menu items. + const active = button.getActive() != 0; + if (!active) return; + + const has_selection = selection: { + const surface = self.getActiveSurface() orelse + break :selection false; + const core_surface = surface.core() orelse + break :selection false; + break :selection core_surface.hasSelection(); + }; + + const action_map: *gio.ActionMap = gobject.ext.cast( + gio.ActionMap, + self, + ) orelse return; + const action: *gio.SimpleAction = gobject.ext.cast( + gio.SimpleAction, + action_map.lookupAction("copy") orelse return, + ) orelse return; + action.setEnabled(@intFromBool(has_selection)); + } + + /// Add or remove "background" CSS class depending on if the background + /// should be opaque. + fn propBackgroundOpaque( + _: *adw.ApplicationWindow, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + self.toggleCssClass("background", self.getBackgroundOpaque()); + } + + //--------------------------------------------------------------- + // Virtual methods + + fn dispose(self: *Self) callconv(.C) void { + const priv = self.private(); + if (priv.config) |v| { + v.unref(); + priv.config = null; + } + priv.tab_bindings.setSource(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(); + priv.tab_bindings.unref(); + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + //--------------------------------------------------------------- + // Signal handlers + + fn windowCloseRequest( + _: *gtk.Window, + self: *Self, + ) callconv(.c) c_int { + // If our surface needs confirmation then we show confirmation. + // This will have to be expanded to a list when we have tabs + // or splits. + confirm: { + const surface = self.getActiveSurface() orelse break :confirm; + const core_surface = surface.core() orelse break :confirm; + if (!core_surface.needsConfirmQuit()) break :confirm; + + // Show a confirmation dialog + const dialog: *CloseConfirmationDialog = .new(.app); + _ = CloseConfirmationDialog.signals.@"close-request".connect( + dialog, + *Self, + closeConfirmationClose, + self, + .{}, + ); + + // Show it + dialog.present(self.as(gtk.Widget)); + return @intFromBool(true); + } + + self.as(gtk.Window).destroy(); + return @intFromBool(false); + } + + fn closeConfirmationClose( + _: *CloseConfirmationDialog, + self: *Self, + ) callconv(.c) void { + self.as(gtk.Window).destroy(); + } + + fn tabViewSelectedPage( + _: *adw.TabView, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + const priv = self.private(); + + // Always reset our binding source in case we have no pages. + priv.tab_bindings.setSource(null); + + // Get our current page which MUST be a Tab object. + const page = priv.tab_view.getSelectedPage() orelse return; + const child = page.getChild(); + assert(gobject.ext.isA(child, Tab)); + + // Setup our binding group. This ensures things like the title + // are synced from the active tab. + priv.tab_bindings.setSource(child.as(gobject.Object)); + } + + fn tabViewPageAttached( + _: *adw.TabView, + page: *adw.TabPage, + _: c_int, + self: *Self, + ) callconv(.c) void { + // Get the attached page which must be a Tab object. + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse return; + _ = tab; + + // Attach listeners for the + _ = self; + } + + fn tabViewPageDetached( + _: *adw.TabView, + page: *adw.TabPage, + _: c_int, + self: *Self, + ) callconv(.c) void { + _ = page; + _ = self; + } + + fn surfaceClipboardWrite( + _: *Surface, + clipboard_type: apprt.Clipboard, + text: [*:0]const u8, + self: *Self, + ) callconv(.c) void { + // We only toast for the standard clipboard. + if (clipboard_type != .standard) return; + + // We only toast if configured to + const priv = self.private(); + const config_obj = priv.config orelse return; + const config = config_obj.get(); + if (!config.@"app-notifications".@"clipboard-copy") { + return; + } + + if (text[0] != 0) + self.addToast(i18n._("Copied to clipboard")) + else + self.addToast(i18n._("Cleared clipboard")); + } + + fn surfaceCloseRequest( + surface: *Surface, + scope: *const Surface.CloseScope, + self: *Self, + ) callconv(.c) void { + // Todo + _ = scope; + _ = surface; + + self.as(gtk.Window).close(); + } + + fn surfaceToggleFullscreen( + surface: *Surface, + self: *Self, + ) callconv(.c) void { + _ = surface; + if (self.as(gtk.Window).isFullscreen() != 0) { + self.as(gtk.Window).unfullscreen(); + } else { + self.as(gtk.Window).fullscreen(); + } + + // We react to the changes in the propFullscreen callback + } + + fn surfaceToggleMaximize( + surface: *Surface, + self: *Self, + ) callconv(.c) void { + _ = surface; + if (self.as(gtk.Window).isMaximized() != 0) { + self.as(gtk.Window).unmaximize(); + } else { + self.as(gtk.Window).maximize(); + } + + // We react to the changes in the propMaximized callback + } + + fn actionAbout( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + const name = "Ghostty"; + const icon = "com.mitchellh.ghostty"; + const website = "https://ghostty.org"; + + if (adw_version.supportsDialogs()) { + adw.showAboutDialog( + self.as(gtk.Widget), + "application-name", + name, + "developer-name", + i18n._("Ghostty Developers"), + "application-icon", + icon, + "version", + build_config.version_string.ptr, + "issue-url", + "https://github.com/ghostty-org/ghostty/issues", + "website", + website, + @as(?*anyopaque, null), + ); + } else { + gtk.showAboutDialog( + self.as(gtk.Window), + "program-name", + name, + "logo-icon-name", + icon, + "title", + i18n._("About Ghostty"), + "version", + build_config.version_string.ptr, + "website", + website, + @as(?*anyopaque, null), + ); + } + } + + fn actionClose( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + self.as(gtk.Window).close(); + } + + fn actionNewWindow( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.performBindingAction(.new_window); + } + + fn actionCopy( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.performBindingAction(.copy_to_clipboard); + } + + fn actionPaste( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.performBindingAction(.paste_from_clipboard); + } + + fn actionReset( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.performBindingAction(.reset); + } + + fn actionClear( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.performBindingAction(.clear_screen); + } + + 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(DebugWarning); + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + comptime gresource.blueprint(.{ + .major = 1, + .minor = 5, + .name = "window", + }), + ); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.@"active-surface".impl, + properties.@"background-opaque".impl, + properties.config.impl, + properties.debug.impl, + properties.@"headerbar-visible".impl, + properties.@"tabs-autohide".impl, + properties.@"tabs-visible".impl, + properties.@"tabs-wide".impl, + properties.@"toolbar-style".impl, + }); + + // Bindings + class.bindTemplateChildPrivate("tab_bar", .{}); + class.bindTemplateChildPrivate("tab_view", .{}); + class.bindTemplateChildPrivate("toolbar", .{}); + class.bindTemplateChildPrivate("toast_overlay", .{}); + + // Template Callbacks + class.bindTemplateCallback("close_request", &windowCloseRequest); + class.bindTemplateCallback("selected_page", &tabViewSelectedPage); + class.bindTemplateCallback("surface_clipboard_write", &surfaceClipboardWrite); + class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest); + class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen); + class.bindTemplateCallback("surface_toggle_maximize", &surfaceToggleMaximize); + class.bindTemplateCallback("notify_config", &propConfig); + class.bindTemplateCallback("notify_fullscreened", &propFullscreened); + class.bindTemplateCallback("notify_maximized", &propMaximized); + class.bindTemplateCallback("notify_menu_active", &propMenuActive); + class.bindTemplateCallback("notify_background_opaque", &propBackgroundOpaque); + + // 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; + }; +}; diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 548a8b664..673b84546 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -253,7 +253,8 @@ pub const Window = extern struct { self.as(gtk.Widget).addCssClass("devel"); } - // Setup some of our objects that are never null + // Setup our tab binding group. This ensures certain properties + // are only synced from the currently active tab. priv.tab_bindings = gobject.BindingGroup.new(); priv.tab_bindings.bind("title", self.as(gobject.Object), "title", .{}); @@ -610,6 +611,51 @@ pub const Window = extern struct { priv.tab_bindings.setSource(child.as(gobject.Object)); } + fn tabViewPageAttached( + _: *adw.TabView, + page: *adw.TabPage, + _: c_int, + self: *Self, + ) callconv(.c) void { + // Get the attached page which must be a Tab object. + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse return; + + // Attach listeners for the surface. + // TODO: When we have a split tree we'll want to attach to that. + const surface = tab.getActiveSurface(); + _ = Surface.signals.@"close-request".connect( + surface, + *Self, + surfaceCloseRequest, + self, + .{}, + ); + } + + fn tabViewPageDetached( + _: *adw.TabView, + page: *adw.TabPage, + _: c_int, + self: *Self, + ) callconv(.c) void { + // We need to get the tab to disconnect the signals. + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse return; + + // Remove all the signals that have this window as the userdata. + const surface = tab.getActiveSurface(); + _ = gobject.signalHandlersDisconnectMatched( + surface.as(gobject.Object), + .{ .data = true }, + 0, + 0, + null, + null, + self, + ); + } + fn surfaceClipboardWrite( _: *Surface, clipboard_type: apprt.Clipboard, @@ -809,6 +855,8 @@ pub const Window = extern struct { // Template Callbacks class.bindTemplateCallback("close_request", &windowCloseRequest); class.bindTemplateCallback("selected_page", &tabViewSelectedPage); + class.bindTemplateCallback("page_attached", &tabViewPageAttached); + class.bindTemplateCallback("page_detached", &tabViewPageDetached); class.bindTemplateCallback("surface_clipboard_write", &surfaceClipboardWrite); class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest); class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index dbfcbda96..3c03a2751 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -79,6 +79,8 @@ template $GhosttyWindow: Adw.ApplicationWindow { Adw.ToastOverlay toast_overlay { Adw.TabView tab_view { notify::selected-page => $selected_page(); + page-attached => $page_attached(); + page-detached => $page_detached(); } } } From ed25a57d08b67f5c5de85b2679d767f27ab78fca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 21:22:15 -0700 Subject: [PATCH 09/28] apprt/gtk-ng: hook up all existing surface signals --- src/apprt/gtk-ng/class/window.zig | 57 ++++++++++++++++++++++++++++--- src/apprt/gtk-ng/ui/1.5/tab.blp | 3 -- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 673b84546..3c4a9112f 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -622,6 +622,25 @@ pub const Window = extern struct { const tab = gobject.ext.cast(Tab, child) orelse return; // Attach listeners for the surface. + // + // Interesting behavior here that was previously undocumented but + // I'm going to make it explicit here: we accept all the signals here + // (like toggle-fullscreen) regardless of whether the surface or tab + // is focused. At the time of writing this we have no API that could + // really trigger these that way but its theoretically possible. + // + // What is DEFINITELY possible is something like OSC52 triggering + // a clipboard-write signal on an unfocused tab/surface. We definitely + // want to show the user a notification about that but our notification + // right now is a toast that doesn't make it clear WHO used the + // clipboard. We probably want to change that in the future. + // + // I'm not sure how desirable all the above is, and we probably + // should be thoughtful about future signals here. But all of this + // behavior is consistent with macOS and the previous GTK apprt, + // but that behavior was all implicit and not documented, so here + // I am. + // // TODO: When we have a split tree we'll want to attach to that. const surface = tab.getActiveSurface(); _ = Surface.signals.@"close-request".connect( @@ -631,6 +650,27 @@ pub const Window = extern struct { self, .{}, ); + _ = Surface.signals.@"clipboard-write".connect( + surface, + *Self, + surfaceClipboardWrite, + self, + .{}, + ); + _ = Surface.signals.@"toggle-fullscreen".connect( + surface, + *Self, + surfaceToggleFullscreen, + self, + .{}, + ); + _ = Surface.signals.@"toggle-maximize".connect( + surface, + *Self, + surfaceToggleMaximize, + self, + .{}, + ); } fn tabViewPageDetached( @@ -680,15 +720,22 @@ pub const Window = extern struct { } fn surfaceCloseRequest( - surface: *Surface, + _: *Surface, scope: *const Surface.CloseScope, self: *Self, ) callconv(.c) void { - // Todo - _ = scope; - _ = surface; + switch (scope.*) { + // Handled directly by the tab. If the surface is the last + // surface then the tab will emit its own signal to request + // closing itself. + .surface => return, - self.as(gtk.Window).close(); + // Also handled directly by the tab. + .tab => return, + + // The only one we care about! + .window => self.as(gtk.Window).close(), + } } fn surfaceToggleFullscreen( diff --git a/src/apprt/gtk-ng/ui/1.5/tab.blp b/src/apprt/gtk-ng/ui/1.5/tab.blp index 54a1be630..cd2d5d254 100644 --- a/src/apprt/gtk-ng/ui/1.5/tab.blp +++ b/src/apprt/gtk-ng/ui/1.5/tab.blp @@ -11,8 +11,5 @@ template $GhosttyTab: Box { // splits we probably want to replace this with the split widget type. $GhosttySurface surface { // close-request => $surface_close_request(); - // clipboard-write => $surface_clipboard_write(); - // toggle-fullscreen => $surface_toggle_fullscreen(); - // toggle-maximize => $surface_toggle_maximize(); } } From 431a6328dc572c3659fee190706a63dae87854eb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 21:37:32 -0700 Subject: [PATCH 10/28] apprt/gtk-ng: handle surface close request --- src/apprt/gtk-ng/class/tab.zig | 42 +++++++++++++++++++++++++++++-- src/apprt/gtk-ng/class/window.zig | 28 +++++++++++++++++++++ src/apprt/gtk-ng/ui/1.5/tab.blp | 2 +- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 3d8437115..95ba74a0e 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -91,11 +91,25 @@ pub const Tab = extern struct { }; }; + 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 to show for this tab. This is usally set to a binding + /// 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. title: ?[:0]const u8 = null, @@ -196,6 +210,27 @@ pub const Tab = extern struct { //--------------------------------------------------------------- // Signal handlers + fn surfaceCloseRequest( + _: *Surface, + scope: *const Surface.CloseScope, + self: *Self, + ) callconv(.c) void { + switch (scope.*) { + // Handled upstream... we don't control our window close. + .window => return, + + // Presently both the same, results in the tab closing. + .surface, .tab => { + signals.@"close-request".impl.emit( + self, + null, + .{}, + null, + ); + }, + } + } + const C = Common(Self, Private); pub const as = C.as; pub const ref = C.ref; @@ -229,7 +264,10 @@ pub const Tab = extern struct { class.bindTemplateChildPrivate("surface", .{}); // Template Callbacks - //class.bindTemplateCallback("close_request", &windowCloseRequest); + class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest); + + // Signals + signals.@"close-request".impl.register(.{}); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 3c4a9112f..18696f32a 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -621,6 +621,15 @@ pub const Window = extern struct { const child = page.getChild(); const tab = gobject.ext.cast(Tab, child) orelse return; + // Attach listeners for the tab. + _ = Tab.signals.@"close-request".connect( + tab, + *Self, + tabCloseRequest, + self, + .{}, + ); + // Attach listeners for the surface. // // Interesting behavior here that was previously undocumented but @@ -682,6 +691,15 @@ pub const Window = extern struct { // We need to get the tab to disconnect the signals. const child = page.getChild(); const tab = gobject.ext.cast(Tab, child) orelse return; + _ = gobject.signalHandlersDisconnectMatched( + tab.as(gobject.Object), + .{ .data = true }, + 0, + 0, + null, + null, + self, + ); // Remove all the signals that have this window as the userdata. const surface = tab.getActiveSurface(); @@ -696,6 +714,16 @@ pub const Window = extern struct { ); } + fn tabCloseRequest( + tab: *Tab, + self: *Self, + ) callconv(.c) void { + const priv = self.private(); + const page = priv.tab_view.getPage(tab.as(gtk.Widget)); + // TODO: connect close page handler to tab to check for confirmation + priv.tab_view.closePage(page); + } + fn surfaceClipboardWrite( _: *Surface, clipboard_type: apprt.Clipboard, diff --git a/src/apprt/gtk-ng/ui/1.5/tab.blp b/src/apprt/gtk-ng/ui/1.5/tab.blp index cd2d5d254..476244576 100644 --- a/src/apprt/gtk-ng/ui/1.5/tab.blp +++ b/src/apprt/gtk-ng/ui/1.5/tab.blp @@ -10,6 +10,6 @@ template $GhosttyTab: Box { // A tab currently just contains a surface directly. When we introduce // splits we probably want to replace this with the split widget type. $GhosttySurface surface { - // close-request => $surface_close_request(); + close-request => $surface_close_request(); } } From c0e7b92e911e403f4c62cef2c584dc5b89ffc156 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 07:30:53 -0700 Subject: [PATCH 11/28] apprt/gtk-ng: close tab confirmation --- .../class/close_confirmation_dialog.zig | 42 +++++++--- src/apprt/gtk-ng/class/tab.zig | 8 ++ src/apprt/gtk-ng/class/window.zig | 76 +++++++++++++++++++ src/apprt/gtk-ng/ui/1.5/window.blp | 1 + 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk-ng/class/close_confirmation_dialog.zig b/src/apprt/gtk-ng/class/close_confirmation_dialog.zig index cd22f9b31..579f792ce 100644 --- a/src/apprt/gtk-ng/class/close_confirmation_dialog.zig +++ b/src/apprt/gtk-ng/class/close_confirmation_dialog.zig @@ -57,6 +57,17 @@ pub const CloseConfirmationDialog = extern struct { void, ); }; + + pub const cancel = struct { + pub const name = "cancel"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; }; const Private = struct { @@ -72,14 +83,15 @@ pub const CloseConfirmationDialog = extern struct { fn init(self: *Self, _: *Class) callconv(.C) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); + } + pub fn present(self: *Self, parent: ?*gtk.Widget) void { // Setup our title/body text. const priv = self.private(); self.as(Dialog.Parent).setHeading(priv.target.title()); self.as(Dialog.Parent).setBody(priv.target.body()); - } - pub fn present(self: *Self, parent: ?*gtk.Widget) void { + // Show it self.as(Dialog).present(parent); } @@ -91,13 +103,21 @@ pub const CloseConfirmationDialog = extern struct { self: *Self, response_id: [*:0]const u8, ) callconv(.C) void { - if (std.mem.orderZ(u8, response_id, "close") != .eq) return; - signals.@"close-request".impl.emit( - self, - null, - .{}, - null, - ); + if (std.mem.orderZ(u8, response_id, "close") == .eq) { + signals.@"close-request".impl.emit( + self, + null, + .{}, + null, + ); + } else { + signals.cancel.impl.emit( + self, + null, + .{}, + null, + ); + } } fn dispose(self: *Self) callconv(.C) void { @@ -141,6 +161,7 @@ pub const CloseConfirmationDialog = extern struct { // Signals signals.@"close-request".impl.register(.{}); + signals.cancel.impl.register(.{}); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); @@ -158,11 +179,13 @@ pub const CloseConfirmationDialog = extern struct { /// together into one struct that is the sole source of truth. pub const Target = enum(c_int) { app, + tab, window, pub fn title(self: Target) [*:0]const u8 { return switch (self) { .app => i18n._("Quit Ghostty?"), + .tab => i18n._("Close Tab?"), .window => i18n._("Close Window?"), }; } @@ -170,6 +193,7 @@ pub const Target = enum(c_int) { pub fn body(self: Target) [*:0]const u8 { return switch (self) { .app => i18n._("All terminal sessions will be terminated."), + .tab => i18n._("All terminal sessions in this tab will be terminated."), .window => i18n._("All terminal sessions in this window will be terminated."), }; } diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 95ba74a0e..9787e991e 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -172,6 +172,14 @@ pub const Tab = extern struct { return priv.surface; } + /// Returns true if this tab needs confirmation before quitting based + /// on the various Ghostty configurations. + pub fn getNeedsConfirmQuit(self: *Self) bool { + const surface = self.getActiveSurface(); + const core_surface = surface.core() orelse return false; + return core_surface.needsConfirmQuit(); + } + //--------------------------------------------------------------- // Virtual methods diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 18696f32a..c922bfd9a 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -591,6 +591,81 @@ pub const Window = extern struct { self.as(gtk.Window).destroy(); } + fn closeConfirmationCloseTab( + _: *CloseConfirmationDialog, + page: *adw.TabPage, + ) callconv(.c) void { + const tab_view_widget = page + .getChild() + .as(gtk.Widget) + .getAncestor(gobject.ext.typeFor(adw.TabView)) orelse { + log.warn("close confirmation caled for non-existent page", .{}); + return; + }; + const tab_view = gobject.ext.cast( + adw.TabView, + tab_view_widget, + ).?; + tab_view.closePageFinish(page, @intFromBool(true)); + } + + fn closeConfirmationCancelTab( + _: *CloseConfirmationDialog, + page: *adw.TabPage, + ) callconv(.c) void { + const tab_view_widget = page + .getChild() + .as(gtk.Widget) + .getAncestor(gobject.ext.typeFor(adw.TabView)) orelse { + log.warn("close confirmation caled for non-existent page", .{}); + return; + }; + const tab_view = gobject.ext.cast( + adw.TabView, + tab_view_widget, + ).?; + tab_view.closePageFinish(page, @intFromBool(false)); + } + + fn tabViewClosePage( + _: *adw.TabView, + page: *adw.TabPage, + self: *Self, + ) callconv(.c) c_int { + const priv = self.private(); + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse + return @intFromBool(false); + + // If the tab says it doesn't need confirmation then we go ahead + // and close immediately. + if (!tab.getNeedsConfirmQuit()) { + priv.tab_view.closePageFinish(page, @intFromBool(true)); + return @intFromBool(true); + } + + // Show a confirmation dialog + const dialog: *CloseConfirmationDialog = .new(.tab); + _ = CloseConfirmationDialog.signals.@"close-request".connect( + dialog, + *adw.TabPage, + closeConfirmationCloseTab, + page, + .{}, + ); + _ = CloseConfirmationDialog.signals.cancel.connect( + dialog, + *adw.TabPage, + closeConfirmationCancelTab, + page, + .{}, + ); + + // Show it + dialog.present(child); + return @intFromBool(true); + } + fn tabViewSelectedPage( _: *adw.TabView, _: *gobject.ParamSpec, @@ -929,6 +1004,7 @@ pub const Window = extern struct { // Template Callbacks class.bindTemplateCallback("close_request", &windowCloseRequest); + class.bindTemplateCallback("close_page", &tabViewClosePage); class.bindTemplateCallback("selected_page", &tabViewSelectedPage); class.bindTemplateCallback("page_attached", &tabViewPageAttached); class.bindTemplateCallback("page_detached", &tabViewPageDetached); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 3c03a2751..70974d1c7 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -79,6 +79,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { Adw.ToastOverlay toast_overlay { Adw.TabView tab_view { notify::selected-page => $selected_page(); + close-page => $close_page(); page-attached => $page_attached(); page-detached => $page_detached(); } From 3f440821d972ab4ad4d9dcf24572d9cac9926011 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 09:29:47 -0700 Subject: [PATCH 12/28] fix up suppressions --- src/apprt/gtk-ng/class/window.zig | 4 ---- valgrind.supp | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index c922bfd9a..bd939ba23 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -1008,10 +1008,6 @@ pub const Window = extern struct { class.bindTemplateCallback("selected_page", &tabViewSelectedPage); class.bindTemplateCallback("page_attached", &tabViewPageAttached); class.bindTemplateCallback("page_detached", &tabViewPageDetached); - class.bindTemplateCallback("surface_clipboard_write", &surfaceClipboardWrite); - class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest); - class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen); - class.bindTemplateCallback("surface_toggle_maximize", &surfaceToggleMaximize); class.bindTemplateCallback("notify_config", &propConfig); class.bindTemplateCallback("notify_fullscreened", &propFullscreened); class.bindTemplateCallback("notify_maximized", &propMaximized); diff --git a/valgrind.supp b/valgrind.supp index 966347ad4..724a829d2 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -376,6 +376,7 @@ Memcheck:Leak match-leak-kinds: possible fun:*alloc + ... fun:FcFontSet* ... fun:fc_thread_func From 3bb6cdff4eed0d717a12b0eb488c0971951280b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 09:33:33 -0700 Subject: [PATCH 13/28] apprt/gtk-ng: window getActiveSurface --- src/apprt/gtk-ng/class/window.zig | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index bd939ba23..5745c333f 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -87,7 +87,7 @@ pub const Window = extern struct { .default = build_config.is_debug, .accessor = gobject.ext.typedAccessor(Self, bool, .{ .getter = struct { - pub fn getter(_: *Window) bool { + pub fn getter(_: *Self) bool { return build_config.is_debug; } }.getter, @@ -309,7 +309,7 @@ pub const Window = extern struct { /// to call multiple times. This should be called whenever a change /// happens that might affect how the window appears (config change, /// fullscreen, etc.). - fn syncAppearance(self: *Window) void { + fn syncAppearance(self: *Self) void { // TODO: CSD/SSD // Trigger all our dynamic properties that depend on the config. @@ -338,7 +338,7 @@ pub const Window = extern struct { } } - fn toggleCssClass(self: *Window, class: [:0]const u8, value: bool) void { + fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void { const widget = self.as(gtk.Widget); if (value) widget.addCssClass(class.ptr) @@ -348,7 +348,7 @@ pub const Window = extern struct { /// Perform a binding action on the window's active surface. fn performBindingAction( - self: *Window, + self: *Self, action: input.Binding.Action, ) void { const surface = self.getActiveSurface() orelse return; @@ -365,7 +365,7 @@ pub const Window = extern struct { // This is not `pub` because we should be using signals emitted by // other widgets to trigger our toasts. Other objects should not // trigger toasts directly. - fn addToast(self: *Window, title: [*:0]const u8) void { + fn addToast(self: *Self, title: [*:0]const u8) void { const toast = adw.Toast.new(title); toast.setTimeout(3); self.private().toast_overlay.addToast(toast); @@ -377,9 +377,17 @@ pub const Window = extern struct { /// Get the currently active surface. See the "active-surface" property. /// This does not ref the value. fn getActiveSurface(self: *Self) ?*Surface { + const tab = self.getSelectedTab() orelse return null; + return tab.getActiveSurface(); + } + + /// Get the currently selected tab as a Tab object. + fn getSelectedTab(self: *Self) ?*Tab { const priv = self.private(); - _ = priv; - return null; + const page = priv.tab_view.getSelectedPage() orelse return null; + const child = page.getChild(); + assert(gobject.ext.isA(child, Tab)); + return gobject.ext.cast(Tab, child); } fn getHeaderbarVisible(self: *Self) bool { From e4fb46f23002f85e41065cce6c3b6277e51fcee2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 09:38:05 -0700 Subject: [PATCH 14/28] apprt/gtk-ng: close window if no tabs left --- src/apprt/gtk-ng/class/window.zig | 15 ++++++++++++++- src/apprt/gtk-ng/ui/1.5/window.blp | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 5745c333f..44f3eb0c5 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -807,6 +807,18 @@ pub const Window = extern struct { priv.tab_view.closePage(page); } + fn tabViewNPages( + _: *adw.TabView, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + const priv = self.private(); + if (priv.tab_view.getNPages() == 0) { + // If we have no pages left then we want to close window. + self.as(gtk.Window).close(); + } + } + fn surfaceClipboardWrite( _: *Surface, clipboard_type: apprt.Clipboard, @@ -1013,9 +1025,10 @@ pub const Window = extern struct { // Template Callbacks class.bindTemplateCallback("close_request", &windowCloseRequest); class.bindTemplateCallback("close_page", &tabViewClosePage); - class.bindTemplateCallback("selected_page", &tabViewSelectedPage); class.bindTemplateCallback("page_attached", &tabViewPageAttached); class.bindTemplateCallback("page_detached", &tabViewPageDetached); + class.bindTemplateCallback("notify_n_pages", &tabViewNPages); + class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage); class.bindTemplateCallback("notify_config", &propConfig); class.bindTemplateCallback("notify_fullscreened", &propFullscreened); class.bindTemplateCallback("notify_maximized", &propMaximized); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 70974d1c7..cefabc91f 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -78,7 +78,8 @@ template $GhosttyWindow: Adw.ApplicationWindow { Adw.ToastOverlay toast_overlay { Adw.TabView tab_view { - notify::selected-page => $selected_page(); + notify::n-pages => $notify_n_pages(); + notify::selected-page => $notify_selected_page(); close-page => $close_page(); page-attached => $page_attached(); page-detached => $page_detached(); From 70010ec50a23f0e4b56aad23ebf4bd5117dd3962 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 09:38:31 -0700 Subject: [PATCH 15/28] typos --- src/apprt/gtk-ng/class/window.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 44f3eb0c5..30446b8e4 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -607,7 +607,7 @@ pub const Window = extern struct { .getChild() .as(gtk.Widget) .getAncestor(gobject.ext.typeFor(adw.TabView)) orelse { - log.warn("close confirmation caled for non-existent page", .{}); + log.warn("close confirmation called for non-existent page", .{}); return; }; const tab_view = gobject.ext.cast( @@ -625,7 +625,7 @@ pub const Window = extern struct { .getChild() .as(gtk.Widget) .getAncestor(gobject.ext.typeFor(adw.TabView)) orelse { - log.warn("close confirmation caled for non-existent page", .{}); + log.warn("close confirmation called for non-existent page", .{}); return; }; const tab_view = gobject.ext.cast( From 06828111074160d4b854e62f9da16f81edf04a33 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 09:48:13 -0700 Subject: [PATCH 16/28] apprt/gtk-ng: window close confirmation --- src/apprt/gtk-ng/class/window.zig | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 30446b8e4..a17f41ff6 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -390,6 +390,25 @@ pub const Window = extern struct { return gobject.ext.cast(Tab, child); } + /// Returns true if this window needs confirmation before quitting. + pub fn getNeedsConfirmQuit(self: *Self) bool { + const priv = self.private(); + const n = priv.tab_view.getNPages(); + assert(n >= 0); + + for (0..@intCast(n)) |i| { + const page = priv.tab_view.getNthPage(@intCast(i)); + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse { + log.warn("unexpected non-Tab child in tab view", .{}); + continue; + }; + if (tab.getNeedsConfirmQuit()) return true; + } + + return false; + } + fn getHeaderbarVisible(self: *Self) bool { // TODO: CSD/SSD // TODO: QuickTerminal @@ -565,16 +584,9 @@ pub const Window = extern struct { _: *gtk.Window, self: *Self, ) callconv(.c) c_int { - // If our surface needs confirmation then we show confirmation. - // This will have to be expanded to a list when we have tabs - // or splits. - confirm: { - const surface = self.getActiveSurface() orelse break :confirm; - const core_surface = surface.core() orelse break :confirm; - if (!core_surface.needsConfirmQuit()) break :confirm; - + if (self.getNeedsConfirmQuit()) { // Show a confirmation dialog - const dialog: *CloseConfirmationDialog = .new(.app); + const dialog: *CloseConfirmationDialog = .new(.window); _ = CloseConfirmationDialog.signals.@"close-request".connect( dialog, *Self, From b475cd28d52bb4ddd698e3f18f9feac6034ffdf6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 09:53:20 -0700 Subject: [PATCH 17/28] apprt/gtk-ng: new tab, slightly broken --- src/apprt/gtk-ng/class/application.zig | 28 +++++++++- src/apprt/gtk-ng/class/window.zig | 76 ++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 636700284..b29f79ec0 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -500,6 +500,8 @@ pub const Application = extern struct { .mouse_shape => Action.mouseShape(target, value), .mouse_visibility => Action.mouseVisibility(target, value), + .new_tab => return Action.newTab(target), + .new_window => try Action.newWindow( self, switch (target) { @@ -532,7 +534,6 @@ pub const Application = extern struct { .toggle_fullscreen => Action.toggleFullscreen(target), // Unimplemented but todo on gtk-ng branch - .new_tab, .goto_tab, .move_tab, .new_split, @@ -1272,6 +1273,31 @@ const Action = struct { } } + pub fn newTab(target: apprt.Target) bool { + switch (target) { + .app => { + log.warn("new tab to app is unexpected", .{}); + return false; + }, + + .surface => |core| { + // Get the window ancestor of the surface. Surfaces shouldn't + // be aware they might be in windows but at the app level we + // can do this. + const surface = core.rt_surface.surface; + const window_widget = surface + .as(gtk.Widget) + .getAncestor(gobject.ext.typeFor(Window)) orelse { + log.warn("surface is not in a window, ignoring new_tab", .{}); + return false; + }; + const window = gobject.ext.cast(Window, window_widget).?; + window.newTab(core); + return true; + }, + } + } + pub fn newWindow( self: *Application, parent: ?*CoreSurface, diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index a17f41ff6..79d7d8f53 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -223,17 +223,7 @@ pub const Window = extern struct { const self = gobject.ext.newInstance(Self, .{ .application = app, }); - - // Create our initial tab. This will trigger the selected-page - // signal handler which will setup the remainder of the bindings - // for this to all work. - const priv = self.private(); - const tab = gobject.ext.newInstance(Tab, .{ - .config = priv.config, - }); - if (parent_) |p| tab.setParent(p); - _ = priv.tab_view.append(tab.as(gtk.Widget)); - + self.newTab(parent_); return self; } @@ -280,6 +270,8 @@ pub const Window = extern struct { const actions = .{ .{ "about", actionAbout, null }, .{ "close", actionClose, null }, + .{ "close-tab", actionCloseTab, null }, + .{ "new-tab", actionNewTab, null }, .{ "new-window", actionNewWindow, null }, .{ "copy", actionCopy, null }, .{ "paste", actionPaste, null }, @@ -305,6 +297,50 @@ pub const Window = extern struct { } } + /// Create a new tab with the given parent. The tab will be inserted + /// at the position dictated by the `window-new-tab-position` config. + /// The new tab will be selected. + pub fn newTab(self: *Self, parent_: ?*CoreSurface) void { + const priv = self.private(); + const tab_view = priv.tab_view; + + // Create our new tab object + const tab = gobject.ext.newInstance(Tab, .{ + .config = priv.config, + }); + if (parent_) |p| tab.setParent(p); + + // Get the position that we should insert the new tab at. + const config = if (priv.config) |v| v.get() else { + // If we don't have a config we just append it at the end. + // This should never happen. + _ = tab_view.append(tab.as(gtk.Widget)); + return; + }; + const position = switch (config.@"window-new-tab-position") { + .current => current: { + const selected = tab_view.getSelectedPage() orelse + break :current tab_view.getNPages(); + const current = tab_view.getPagePosition(selected); + break :current current + 1; + }, + + .end => tab_view.getNPages(), + }; + + // Add the page and select it + const page = tab_view.insert(tab.as(gtk.Widget), position); + tab_view.setSelectedPage(page); + + // Create some property bindings + _ = tab.as(gobject.Object).bindProperty( + "title", + page.as(gobject.Object), + "title", + .{ .sync_create = true }, + ); + } + /// Updates various appearance properties. This should always be safe /// to call multiple times. This should be called whenever a change /// happens that might affect how the window appears (config change, @@ -391,7 +427,7 @@ pub const Window = extern struct { } /// Returns true if this window needs confirmation before quitting. - pub fn getNeedsConfirmQuit(self: *Self) bool { + fn getNeedsConfirmQuit(self: *Self) bool { const priv = self.private(); const n = priv.tab_view.getNPages(); assert(n >= 0); @@ -953,6 +989,14 @@ pub const Window = extern struct { self.as(gtk.Window).close(); } + fn actionCloseTab( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.performBindingAction(.close_tab); + } + fn actionNewWindow( _: *gio.SimpleAction, _: ?*glib.Variant, @@ -961,6 +1005,14 @@ pub const Window = extern struct { self.performBindingAction(.new_window); } + fn actionNewTab( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.performBindingAction(.new_tab); + } + fn actionCopy( _: *gio.SimpleAction, _: ?*glib.Variant, From ccc8dac0a57ba0713e9a9f04d8998fba1f85b8aa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 10:22:15 -0700 Subject: [PATCH 18/28] apprt/gtk-ng: new tab button --- src/apprt/gtk-ng/class/window.zig | 5 +++++ src/apprt/gtk-ng/ui/1.5/window.blp | 1 + valgrind.supp | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 79d7d8f53..4487825d0 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -616,6 +616,10 @@ pub const Window = extern struct { //--------------------------------------------------------------- // Signal handlers + fn btnNewTab(_: *adw.SplitButton, self: *Window) callconv(.c) void { + self.performBindingAction(.new_tab); + } + fn windowCloseRequest( _: *gtk.Window, self: *Self, @@ -1087,6 +1091,7 @@ pub const Window = extern struct { class.bindTemplateChildPrivate("toast_overlay", .{}); // Template Callbacks + class.bindTemplateCallback("new_tab", &btnNewTab); class.bindTemplateCallback("close_request", &windowCloseRequest); class.bindTemplateCallback("close_page", &tabViewClosePage); class.bindTemplateCallback("page_attached", &tabViewPageAttached); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index cefabc91f..1026a9f66 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -35,6 +35,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { [start] Adw.SplitButton { + clicked => $new_tab(); icon-name: "tab-new-symbolic"; tooltip-text: _("New Tab"); dropdown-tooltip: _("New Split"); diff --git a/valgrind.supp b/valgrind.supp index 724a829d2..75a28ba4b 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -146,8 +146,8 @@ GDK GLArea Memcheck:Leak match-leak-kinds: possible - fun:malloc - fun:g_malloc + fun:*alloc + ... fun:gdk_memory_texture_from_texture fun:gdk_gl_texture_release fun:delete_one_texture From 0da6b2049d29cb768180da2cbce63f2533128778 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 10:29:24 -0700 Subject: [PATCH 19/28] apprt/gtk-ng: tab overview new tab --- src/apprt/gtk-ng/class/window.zig | 19 ++++++++++++++++--- src/apprt/gtk-ng/ui/1.5/window.blp | 1 + 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 4487825d0..0eb514b98 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -301,6 +301,10 @@ pub const Window = extern struct { /// at the position dictated by the `window-new-tab-position` config. /// The new tab will be selected. pub fn newTab(self: *Self, parent_: ?*CoreSurface) void { + _ = self.newTabPage(parent_); + } + + fn newTabPage(self: *Self, parent_: ?*CoreSurface) *adw.TabPage { const priv = self.private(); const tab_view = priv.tab_view; @@ -314,8 +318,7 @@ pub const Window = extern struct { const config = if (priv.config) |v| v.get() else { // If we don't have a config we just append it at the end. // This should never happen. - _ = tab_view.append(tab.as(gtk.Widget)); - return; + return tab_view.append(tab.as(gtk.Widget)); }; const position = switch (config.@"window-new-tab-position") { .current => current: { @@ -339,6 +342,8 @@ pub const Window = extern struct { "title", .{ .sync_create = true }, ); + + return page; } /// Updates various appearance properties. This should always be safe @@ -616,10 +621,17 @@ pub const Window = extern struct { //--------------------------------------------------------------- // Signal handlers - fn btnNewTab(_: *adw.SplitButton, self: *Window) callconv(.c) void { + fn btnNewTab(_: *adw.SplitButton, self: *Self) callconv(.c) void { self.performBindingAction(.new_tab); } + fn tabOverviewCreateTab( + _: *adw.TabOverview, + self: *Self, + ) callconv(.c) *adw.TabPage { + return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null); + } + fn windowCloseRequest( _: *gtk.Window, self: *Self, @@ -1092,6 +1104,7 @@ pub const Window = extern struct { // Template Callbacks class.bindTemplateCallback("new_tab", &btnNewTab); + class.bindTemplateCallback("overview_create_tab", &tabOverviewCreateTab); class.bindTemplateCallback("close_request", &windowCloseRequest); class.bindTemplateCallback("close_page", &tabViewClosePage); class.bindTemplateCallback("page_attached", &tabViewPageAttached); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 1026a9f66..c3ec1ca29 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -18,6 +18,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { handle-menubar-accel: false; content: Adw.TabOverview tab_overview { + create-tab => $overview_create_tab(); enable-new-tab: true; view: tab_view; From 16bb579babc44a1e7a2680ce6629fc783611588f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 10:32:02 -0700 Subject: [PATCH 20/28] update supps --- valgrind.supp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/valgrind.supp b/valgrind.supp index 75a28ba4b..040328648 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -135,10 +135,6 @@ ... fun:gsk_gpu_node_processor_process fun:gsk_gpu_frame_render - fun:gsk_gpu_renderer_render - fun:gsk_renderer_render - fun:gtk_widget_render - fun:surface_render ... } From 8b14ab12216758adec0b82dc148c48686255791f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 10:39:42 -0700 Subject: [PATCH 21/28] apprt/gtk-ng: workaround for tab overview selection --- src/apprt/gtk-ng/class/surface.zig | 7 ++++ src/apprt/gtk-ng/class/window.zig | 53 ++++++++++++++++++++++++++++++ src/apprt/gtk-ng/ui/1.5/window.blp | 1 + 3 files changed, 61 insertions(+) diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index bb432cf6f..3544fb6d3 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -1050,6 +1050,13 @@ pub const Surface = extern struct { ); } + /// Focus this surface. This properly focuses the input part of + /// our surface. + pub fn grabFocus(self: *Self) void { + const priv = self.private(); + _ = priv.gl_area.as(gtk.Widget).grabFocus(); + } + //--------------------------------------------------------------- // Virtual Methods diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 0eb514b98..59ef7055d 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -210,6 +210,9 @@ pub const Window = extern struct { /// The configuration that this surface is using. config: ?*Config = null, + /// See tabOverviewOpen for why we have this. + tab_overview_focus_timer: ?c_uint = null, + // Template bindings tab_bar: *adw.TabBar, tab_view: *adw.TabView, @@ -632,6 +635,55 @@ pub const Window = extern struct { return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null); } + fn tabOverviewOpen( + tab_overview: *adw.TabOverview, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + // We only care about when the tab overview is closed. + if (tab_overview.getOpen() != 0) return; + + // On tab overview close, focus is sometimes lost. This is an + // upstream issue in libadwaita[1]. When this is resolved we + // can put a runtime version check here to avoid this workaround. + // + // Our workaround is to start a timer after 500ms to refocus + // the currently selected tab. We choose 500ms because the adw + // animation is 400ms. + // + // [1]: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/670 + + // If we have an old timer remove it + const priv = self.private(); + if (priv.tab_overview_focus_timer) |timer| { + _ = glib.Source.remove(timer); + } + + // Restart our timer + priv.tab_overview_focus_timer = glib.timeoutAdd( + 500, + tabOverviewFocusTimer, + self, + ); + } + + fn tabOverviewFocusTimer( + ud: ?*anyopaque, + ) callconv(.c) c_int { + const self: *Self = @ptrCast(@alignCast(ud orelse return 0)); + + // Always note our timer is removed + self.private().tab_overview_focus_timer = null; + + // Get our currently active surface which should respect the newly + // selected tab. Grab focus. + const surface = self.getActiveSurface() orelse return 0; + surface.grabFocus(); + + // Remove the timer + return 0; + } + fn windowCloseRequest( _: *gtk.Window, self: *Self, @@ -1105,6 +1157,7 @@ pub const Window = extern struct { // Template Callbacks class.bindTemplateCallback("new_tab", &btnNewTab); class.bindTemplateCallback("overview_create_tab", &tabOverviewCreateTab); + class.bindTemplateCallback("overview_notify_open", &tabOverviewOpen); class.bindTemplateCallback("close_request", &windowCloseRequest); class.bindTemplateCallback("close_page", &tabViewClosePage); class.bindTemplateCallback("page_attached", &tabViewPageAttached); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index c3ec1ca29..26f303460 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -19,6 +19,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { content: Adw.TabOverview tab_overview { create-tab => $overview_create_tab(); + notify::open => $overview_notify_open(); enable-new-tab: true; view: tab_view; From 179fa8e5aa7672992e3201d268dde8c41134f1df Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 10:43:13 -0700 Subject: [PATCH 22/28] remove bad file --- :w | 857 ------------------------------------------------------------- 1 file changed, 857 deletions(-) delete mode 100644 :w diff --git a/:w b/:w deleted file mode 100644 index 93782a896..000000000 --- a/:w +++ /dev/null @@ -1,857 +0,0 @@ -const std = @import("std"); -const build_config = @import("../../../build_config.zig"); -const assert = std.debug.assert; -const adw = @import("adw"); -const gio = @import("gio"); -const glib = @import("glib"); -const gobject = @import("gobject"); -const gtk = @import("gtk"); - -const i18n = @import("../../../os/main.zig").i18n; -const apprt = @import("../../../apprt.zig"); -const input = @import("../../../input.zig"); -const CoreSurface = @import("../../../Surface.zig"); -const gtk_version = @import("../gtk_version.zig"); -const adw_version = @import("../adw_version.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 CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; -const Surface = @import("surface.zig").Surface; -const Tab = @import("tab.zig").Tab; -const DebugWarning = @import("debug_warning.zig").DebugWarning; - -const log = std.log.scoped(.gtk_ghostty_window); - -pub const Window = extern struct { - const Self = @This(); - parent_instance: Parent, - pub const Parent = adw.ApplicationWindow; - pub const getGObjectType = gobject.ext.defineClass(Self, .{ - .name = "GhosttyWindow", - .instanceInit = &init, - .classInit = &Class.init, - .parent_class = &Class.parent, - .private = .{ .Type = Private, .offset = &Private.offset }, - }); - - pub const properties = struct { - /// The active surface is the focus 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, - .{ - .nick = "Active Surface", - .blurb = "The currently active 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, - .{ - .nick = "Config", - .blurb = "The configuration that this surface is using.", - .accessor = C.privateObjFieldAccessor("config"), - }, - ); - }; - - pub const debug = struct { - pub const name = "debug"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .nick = "Debug", - .blurb = "True if runtime safety checks are enabled.", - .default = build_config.is_debug, - .accessor = gobject.ext.typedAccessor(Self, bool, .{ - .getter = struct { - pub fn getter(_: *Window) bool { - return build_config.is_debug; - } - }.getter, - }), - }, - ); - }; - - pub const @"headerbar-visible" = struct { - pub const name = "headerbar-visible"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .nick = "Headerbar Visible", - .blurb = "True if the headerbar is visible.", - .default = true, - .accessor = gobject.ext.typedAccessor(Self, bool, .{ - .getter = Self.getHeaderbarVisible, - }), - }, - ); - }; - - pub const @"background-opaque" = struct { - pub const name = "background-opaque"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .nick = "Background Opaque", - .blurb = "True if the background should be opaque.", - .default = true, - .accessor = gobject.ext.typedAccessor(Self, bool, .{ - .getter = Self.getBackgroundOpaque, - }), - }, - ); - }; - - pub const @"tabs-autohide" = struct { - pub const name = "tabs-autohide"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .nick = "Autohide Tab Bar", - .blurb = "If true, tab bar should autohide.", - .default = true, - .accessor = gobject.ext.typedAccessor(Self, bool, .{ - .getter = Self.getTabsAutohide, - }), - }, - ); - }; - - pub const @"tabs-wide" = struct { - pub const name = "tabs-wide"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .nick = "Wide Tabs", - .blurb = "If true, tabs will be in the wide expanded style.", - .default = true, - .accessor = gobject.ext.typedAccessor(Self, bool, .{ - .getter = Self.getTabsWide, - }), - }, - ); - }; - - pub const @"tabs-visible" = struct { - pub const name = "tabs-visible"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .nick = "Tab Bar Visibility", - .blurb = "If true, tab bar should be visible.", - .default = true, - .accessor = gobject.ext.typedAccessor(Self, bool, .{ - .getter = Self.getTabsVisible, - }), - }, - ); - }; - - pub const @"toolbar-style" = struct { - pub const name = "toolbar-style"; - const impl = gobject.ext.defineProperty( - name, - Self, - adw.ToolbarStyle, - .{ - .nick = "Toolbar Style", - .blurb = "The style for the toolbar top/bottom bars.", - .default = .raised, - .accessor = gobject.ext.typedAccessor( - Self, - adw.ToolbarStyle, - .{ - .getter = Self.getToolbarStyle, - }, - ), - }, - ); - }; - }; - - const Private = struct { - /// Binding group for our active tab. - tab_bindings: *gobject.BindingGroup, - - /// The configuration that this surface is using. - config: ?*Config = null, - - // Template bindings - tab_bar: *adw.TabBar, - tab_view: *adw.TabView, - toolbar: *adw.ToolbarView, - toast_overlay: *adw.ToastOverlay, - - pub var offset: c_int = 0; - }; - - pub fn new(app: *Application, parent_: ?*CoreSurface) *Self { - const self = gobject.ext.newInstance(Self, .{ - .application = app, - }); - - // Create our initial tab. This will trigger the selected-page - // signal handler which will setup the remainder of the bindings - // for this to all work. - const priv = self.private(); - const tab = gobject.ext.newInstance(Tab, .{ - .config = priv.config, - }); - if (parent_) |p| tab.setParent(p); - _ = priv.tab_view.append(tab.as(gtk.Widget)); - - return self; - } - - fn init(self: *Self, _: *Class) callconv(.C) void { - gtk.Widget.initTemplate(self.as(gtk.Widget)); - - // If our configuration is null then we get the configuration - // from the application. - const priv = self.private(); - if (priv.config == null) { - const app = Application.default(); - priv.config = app.getConfig(); - } - - // Add our dev CSS class if we're in debug mode. - if (comptime build_config.is_debug) { - self.as(gtk.Widget).addCssClass("devel"); - } - - // Setup our tab binding group. This ensures certain properties - // are only synced from the currently active tab. - priv.tab_bindings = gobject.BindingGroup.new(); - priv.tab_bindings.bind("title", self.as(gobject.Object), "title", .{}); - - // Set our window icon. We can't set this in the blueprint file - // because its dependent on the build config. - self.as(gtk.Window).setIconName(build_config.bundle_id); - - // Initialize our actions - self.initActionMap(); - - // We always sync our appearance at the end because loading our - // config and such can affect our bindings which ar setup initially - // in initTemplate. - self.syncAppearance(); - - // We need to do this so that the title initializes properly, - // I think because its a dynamic getter. - self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec); - } - - /// Setup our action map. - fn initActionMap(self: *Self) void { - const actions = .{ - .{ "about", actionAbout, null }, - .{ "close", actionClose, null }, - .{ "new-window", actionNewWindow, null }, - .{ "copy", actionCopy, null }, - .{ "paste", actionPaste, null }, - .{ "reset", actionReset, null }, - .{ "clear", actionClear, null }, - }; - - const action_map = self.as(gio.ActionMap); - inline for (actions) |entry| { - const action = gio.SimpleAction.new( - entry[0], - entry[2], - ); - defer action.unref(); - _ = gio.SimpleAction.signals.activate.connect( - action, - *Self, - entry[1], - self, - .{}, - ); - action_map.addAction(action.as(gio.Action)); - } - } - - /// Updates various appearance properties. This should always be safe - /// to call multiple times. This should be called whenever a change - /// happens that might affect how the window appears (config change, - /// fullscreen, etc.). - fn syncAppearance(self: *Window) void { - // TODO: CSD/SSD - - // Trigger all our dynamic properties that depend on the config. - inline for (&.{ - "background-opaque", - "headerbar-visible", - "tabs-autohide", - "tabs-visible", - "tabs-wide", - "toolbar-style", - }) |key| { - self.as(gobject.Object).notifyByPspec( - @field(properties, key).impl.param_spec, - ); - } - - // Remainder uses the config - const priv = self.private(); - const config = if (priv.config) |v| v.get() else return; - - // Move the tab bar to the proper location. - priv.toolbar.remove(priv.tab_bar.as(gtk.Widget)); - switch (config.@"gtk-tabs-location") { - .top => priv.toolbar.addTopBar(priv.tab_bar.as(gtk.Widget)), - .bottom => priv.toolbar.addBottomBar(priv.tab_bar.as(gtk.Widget)), - } - } - - fn toggleCssClass(self: *Window, class: [:0]const u8, value: bool) void { - const widget = self.as(gtk.Widget); - if (value) - widget.addCssClass(class.ptr) - else - widget.removeCssClass(class.ptr); - } - - /// Perform a binding action on the window's active surface. - fn performBindingAction( - self: *Window, - action: input.Binding.Action, - ) void { - const surface = self.getActiveSurface() orelse return; - const core_surface = surface.core() orelse return; - _ = core_surface.performBindingAction(action) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; - } - - /// Queue a simple text-based toast. All text-based toasts share the - /// same timeout for consistency. - /// - // This is not `pub` because we should be using signals emitted by - // other widgets to trigger our toasts. Other objects should not - // trigger toasts directly. - fn addToast(self: *Window, title: [*:0]const u8) void { - const toast = adw.Toast.new(title); - toast.setTimeout(3); - self.private().toast_overlay.addToast(toast); - } - - //--------------------------------------------------------------- - // Properties - - /// Get the currently active surface. See the "active-surface" property. - /// This does not ref the value. - fn getActiveSurface(self: *Self) ?*Surface { - const priv = self.private(); - _ = priv; - return null; - } - - fn getHeaderbarVisible(self: *Self) bool { - // TODO: CSD/SSD - // TODO: QuickTerminal - - // If we're fullscreen we never show the header bar. - if (self.as(gtk.Window).isFullscreen() != 0) return false; - - // The remainder needs a config - const config_obj = self.private().config orelse return true; - const config = config_obj.get(); - - // *Conditionally* disable the header bar when maximized, - // and gtk-titlebar-hide-when-maximized is set - if (self.as(gtk.Window).isMaximized() != 0 and - config.@"gtk-titlebar-hide-when-maximized") - { - return false; - } - - return config.@"gtk-titlebar"; - } - - fn getBackgroundOpaque(self: *Self) bool { - const priv = self.private(); - const config = (priv.config orelse return true).get(); - return config.@"background-opacity" >= 1.0; - } - - fn getTabsAutohide(self: *Self) bool { - const priv = self.private(); - const config = if (priv.config) |v| v.get() else return true; - return switch (config.@"window-show-tab-bar") { - // Auto we always autohide... obviously. - .auto => true, - - // Always we never autohide because we always show the tab bar. - .always => false, - - // Never we autohide because it doesn't actually matter, - // since getTabsVisible will return false. - .never => true, - }; - } - - fn getTabsVisible(self: *Self) bool { - const priv = self.private(); - const config = if (priv.config) |v| v.get() else return true; - return switch (config.@"window-show-tab-bar") { - .always, .auto => true, - .never => false, - }; - } - - fn getTabsWide(self: *Self) bool { - const priv = self.private(); - const config = if (priv.config) |v| v.get() else return true; - return config.@"gtk-wide-tabs"; - } - - fn getToolbarStyle(self: *Self) adw.ToolbarStyle { - const priv = self.private(); - const config = if (priv.config) |v| v.get() else return .raised; - return switch (config.@"gtk-toolbar-style") { - .flat => .flat, - .raised => .raised, - .@"raised-border" => .raised_border, - }; - } - - fn propConfig( - _: *adw.ApplicationWindow, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - self.addToast(i18n._("Reloaded the configuration")); - self.syncAppearance(); - } - - fn propFullscreened( - _: *adw.ApplicationWindow, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - self.syncAppearance(); - } - - fn propMaximized( - _: *adw.ApplicationWindow, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - self.syncAppearance(); - } - - fn propMenuActive( - button: *gtk.MenuButton, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - // Debian 12 is stuck on GTK 4.8 - if (!gtk_version.atLeast(4, 10, 0)) return; - - // We only care if we're activating. If we're activating then - // we need to check the validity of our menu items. - const active = button.getActive() != 0; - if (!active) return; - - const has_selection = selection: { - const surface = self.getActiveSurface() orelse - break :selection false; - const core_surface = surface.core() orelse - break :selection false; - break :selection core_surface.hasSelection(); - }; - - const action_map: *gio.ActionMap = gobject.ext.cast( - gio.ActionMap, - self, - ) orelse return; - const action: *gio.SimpleAction = gobject.ext.cast( - gio.SimpleAction, - action_map.lookupAction("copy") orelse return, - ) orelse return; - action.setEnabled(@intFromBool(has_selection)); - } - - /// Add or remove "background" CSS class depending on if the background - /// should be opaque. - fn propBackgroundOpaque( - _: *adw.ApplicationWindow, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - self.toggleCssClass("background", self.getBackgroundOpaque()); - } - - //--------------------------------------------------------------- - // Virtual methods - - fn dispose(self: *Self) callconv(.C) void { - const priv = self.private(); - if (priv.config) |v| { - v.unref(); - priv.config = null; - } - priv.tab_bindings.setSource(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(); - priv.tab_bindings.unref(); - - gobject.Object.virtual_methods.finalize.call( - Class.parent, - self.as(Parent), - ); - } - - //--------------------------------------------------------------- - // Signal handlers - - fn windowCloseRequest( - _: *gtk.Window, - self: *Self, - ) callconv(.c) c_int { - // If our surface needs confirmation then we show confirmation. - // This will have to be expanded to a list when we have tabs - // or splits. - confirm: { - const surface = self.getActiveSurface() orelse break :confirm; - const core_surface = surface.core() orelse break :confirm; - if (!core_surface.needsConfirmQuit()) break :confirm; - - // Show a confirmation dialog - const dialog: *CloseConfirmationDialog = .new(.app); - _ = CloseConfirmationDialog.signals.@"close-request".connect( - dialog, - *Self, - closeConfirmationClose, - self, - .{}, - ); - - // Show it - dialog.present(self.as(gtk.Widget)); - return @intFromBool(true); - } - - self.as(gtk.Window).destroy(); - return @intFromBool(false); - } - - fn closeConfirmationClose( - _: *CloseConfirmationDialog, - self: *Self, - ) callconv(.c) void { - self.as(gtk.Window).destroy(); - } - - fn tabViewSelectedPage( - _: *adw.TabView, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - const priv = self.private(); - - // Always reset our binding source in case we have no pages. - priv.tab_bindings.setSource(null); - - // Get our current page which MUST be a Tab object. - const page = priv.tab_view.getSelectedPage() orelse return; - const child = page.getChild(); - assert(gobject.ext.isA(child, Tab)); - - // Setup our binding group. This ensures things like the title - // are synced from the active tab. - priv.tab_bindings.setSource(child.as(gobject.Object)); - } - - fn tabViewPageAttached( - _: *adw.TabView, - page: *adw.TabPage, - _: c_int, - self: *Self, - ) callconv(.c) void { - // Get the attached page which must be a Tab object. - const child = page.getChild(); - const tab = gobject.ext.cast(Tab, child) orelse return; - _ = tab; - - // Attach listeners for the - _ = self; - } - - fn tabViewPageDetached( - _: *adw.TabView, - page: *adw.TabPage, - _: c_int, - self: *Self, - ) callconv(.c) void { - _ = page; - _ = self; - } - - fn surfaceClipboardWrite( - _: *Surface, - clipboard_type: apprt.Clipboard, - text: [*:0]const u8, - self: *Self, - ) callconv(.c) void { - // We only toast for the standard clipboard. - if (clipboard_type != .standard) return; - - // We only toast if configured to - const priv = self.private(); - const config_obj = priv.config orelse return; - const config = config_obj.get(); - if (!config.@"app-notifications".@"clipboard-copy") { - return; - } - - if (text[0] != 0) - self.addToast(i18n._("Copied to clipboard")) - else - self.addToast(i18n._("Cleared clipboard")); - } - - fn surfaceCloseRequest( - surface: *Surface, - scope: *const Surface.CloseScope, - self: *Self, - ) callconv(.c) void { - // Todo - _ = scope; - _ = surface; - - self.as(gtk.Window).close(); - } - - fn surfaceToggleFullscreen( - surface: *Surface, - self: *Self, - ) callconv(.c) void { - _ = surface; - if (self.as(gtk.Window).isFullscreen() != 0) { - self.as(gtk.Window).unfullscreen(); - } else { - self.as(gtk.Window).fullscreen(); - } - - // We react to the changes in the propFullscreen callback - } - - fn surfaceToggleMaximize( - surface: *Surface, - self: *Self, - ) callconv(.c) void { - _ = surface; - if (self.as(gtk.Window).isMaximized() != 0) { - self.as(gtk.Window).unmaximize(); - } else { - self.as(gtk.Window).maximize(); - } - - // We react to the changes in the propMaximized callback - } - - fn actionAbout( - _: *gio.SimpleAction, - _: ?*glib.Variant, - self: *Self, - ) callconv(.c) void { - const name = "Ghostty"; - const icon = "com.mitchellh.ghostty"; - const website = "https://ghostty.org"; - - if (adw_version.supportsDialogs()) { - adw.showAboutDialog( - self.as(gtk.Widget), - "application-name", - name, - "developer-name", - i18n._("Ghostty Developers"), - "application-icon", - icon, - "version", - build_config.version_string.ptr, - "issue-url", - "https://github.com/ghostty-org/ghostty/issues", - "website", - website, - @as(?*anyopaque, null), - ); - } else { - gtk.showAboutDialog( - self.as(gtk.Window), - "program-name", - name, - "logo-icon-name", - icon, - "title", - i18n._("About Ghostty"), - "version", - build_config.version_string.ptr, - "website", - website, - @as(?*anyopaque, null), - ); - } - } - - fn actionClose( - _: *gio.SimpleAction, - _: ?*glib.Variant, - self: *Self, - ) callconv(.c) void { - self.as(gtk.Window).close(); - } - - fn actionNewWindow( - _: *gio.SimpleAction, - _: ?*glib.Variant, - self: *Window, - ) callconv(.c) void { - self.performBindingAction(.new_window); - } - - fn actionCopy( - _: *gio.SimpleAction, - _: ?*glib.Variant, - self: *Window, - ) callconv(.c) void { - self.performBindingAction(.copy_to_clipboard); - } - - fn actionPaste( - _: *gio.SimpleAction, - _: ?*glib.Variant, - self: *Window, - ) callconv(.c) void { - self.performBindingAction(.paste_from_clipboard); - } - - fn actionReset( - _: *gio.SimpleAction, - _: ?*glib.Variant, - self: *Window, - ) callconv(.c) void { - self.performBindingAction(.reset); - } - - fn actionClear( - _: *gio.SimpleAction, - _: ?*glib.Variant, - self: *Window, - ) callconv(.c) void { - self.performBindingAction(.clear_screen); - } - - 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(DebugWarning); - gtk.Widget.Class.setTemplateFromResource( - class.as(gtk.Widget.Class), - comptime gresource.blueprint(.{ - .major = 1, - .minor = 5, - .name = "window", - }), - ); - - // Properties - gobject.ext.registerProperties(class, &.{ - properties.@"active-surface".impl, - properties.@"background-opaque".impl, - properties.config.impl, - properties.debug.impl, - properties.@"headerbar-visible".impl, - properties.@"tabs-autohide".impl, - properties.@"tabs-visible".impl, - properties.@"tabs-wide".impl, - properties.@"toolbar-style".impl, - }); - - // Bindings - class.bindTemplateChildPrivate("tab_bar", .{}); - class.bindTemplateChildPrivate("tab_view", .{}); - class.bindTemplateChildPrivate("toolbar", .{}); - class.bindTemplateChildPrivate("toast_overlay", .{}); - - // Template Callbacks - class.bindTemplateCallback("close_request", &windowCloseRequest); - class.bindTemplateCallback("selected_page", &tabViewSelectedPage); - class.bindTemplateCallback("surface_clipboard_write", &surfaceClipboardWrite); - class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest); - class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen); - class.bindTemplateCallback("surface_toggle_maximize", &surfaceToggleMaximize); - class.bindTemplateCallback("notify_config", &propConfig); - class.bindTemplateCallback("notify_fullscreened", &propFullscreened); - class.bindTemplateCallback("notify_maximized", &propMaximized); - class.bindTemplateCallback("notify_menu_active", &propMenuActive); - class.bindTemplateCallback("notify_background_opaque", &propBackgroundOpaque); - - // 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; - }; -}; From 4fb790ca4cc6472b6ca1a8dd73a5a1bb8eeaf3dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 10:58:25 -0700 Subject: [PATCH 23/28] apprt/gtk-ng: helper for getAncestor --- src/apprt/gtk-ng/class/application.zig | 9 +++++---- src/apprt/gtk-ng/class/window.zig | 25 +++++++++---------------- src/apprt/gtk-ng/ext.zig | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 src/apprt/gtk-ng/ext.zig diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index b29f79ec0..0d5f34237 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -22,6 +22,7 @@ const xev = @import("../../../global.zig").xev; const CoreConfig = configpkg.Config; const CoreSurface = @import("../../../Surface.zig"); +const ext = @import("../ext.zig"); const adw_version = @import("../adw_version.zig"); const gtk_version = @import("../gtk_version.zig"); const winprotopkg = @import("../winproto.zig"); @@ -1285,13 +1286,13 @@ const Action = struct { // be aware they might be in windows but at the app level we // can do this. const surface = core.rt_surface.surface; - const window_widget = surface - .as(gtk.Widget) - .getAncestor(gobject.ext.typeFor(Window)) orelse { + const window = ext.getAncestor( + Window, + surface.as(gtk.Widget), + ) orelse { log.warn("surface is not in a window, ignoring new_tab", .{}); return false; }; - const window = gobject.ext.cast(Window, window_widget).?; window.newTab(core); return true; }, diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 59ef7055d..70594f53a 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -11,6 +11,7 @@ const i18n = @import("../../../os/main.zig").i18n; const apprt = @import("../../../apprt.zig"); const input = @import("../../../input.zig"); const CoreSurface = @import("../../../Surface.zig"); +const ext = @import("../ext.zig"); const gtk_version = @import("../gtk_version.zig"); const adw_version = @import("../adw_version.zig"); const gresource = @import("../build/gresource.zig"); @@ -719,17 +720,13 @@ pub const Window = extern struct { _: *CloseConfirmationDialog, page: *adw.TabPage, ) callconv(.c) void { - const tab_view_widget = page - .getChild() - .as(gtk.Widget) - .getAncestor(gobject.ext.typeFor(adw.TabView)) orelse { + const tab_view = ext.getAncestor( + adw.TabView, + page.getChild().as(gtk.Widget), + ) orelse { log.warn("close confirmation called for non-existent page", .{}); return; }; - const tab_view = gobject.ext.cast( - adw.TabView, - tab_view_widget, - ).?; tab_view.closePageFinish(page, @intFromBool(true)); } @@ -737,17 +734,13 @@ pub const Window = extern struct { _: *CloseConfirmationDialog, page: *adw.TabPage, ) callconv(.c) void { - const tab_view_widget = page - .getChild() - .as(gtk.Widget) - .getAncestor(gobject.ext.typeFor(adw.TabView)) orelse { + const tab_view = ext.getAncestor( + adw.TabView, + page.getChild().as(gtk.Widget), + ) orelse { log.warn("close confirmation called for non-existent page", .{}); return; }; - const tab_view = gobject.ext.cast( - adw.TabView, - tab_view_widget, - ).?; tab_view.closePageFinish(page, @intFromBool(false)); } diff --git a/src/apprt/gtk-ng/ext.zig b/src/apprt/gtk-ng/ext.zig new file mode 100644 index 000000000..2a6ac8f6d --- /dev/null +++ b/src/apprt/gtk-ng/ext.zig @@ -0,0 +1,19 @@ +//! Extensions/helpers for GTK objects, following a similar naming +//! style to zig-gobject. These should, wherever possible, be Zig-friendly +//! wrappers around existing GTK functionality, rather than complex new +//! helpers. + +const std = @import("std"); +const assert = std.debug.assert; + +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +/// Wrapper around `gtk.Widget.getAncestor` to get the widget ancestor +/// of the given type `T`, or null if it doesn't exist. +pub fn getAncestor(comptime T: type, widget: *gtk.Widget) ?*T { + const ancestor_ = widget.getAncestor(gobject.ext.typeFor(T)); + const ancestor = ancestor_ orelse return null; + // We can assert the unwrap because getAncestor above + return gobject.ext.cast(T, ancestor).?; +} From 2d1232878d9ddbf0b67cfd5d059af08ed4fe1e80 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 14:40:23 -0700 Subject: [PATCH 24/28] apprt/gtk-ng: goto_tab --- src/apprt/gtk-ng/class/application.zig | 29 ++++++++++++- src/apprt/gtk-ng/class/window.zig | 57 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 0d5f34237..2efd6e211 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -497,6 +497,8 @@ pub const Application = extern struct { value.config, ), + .goto_tab => return Action.gotoTab(target, value), + .mouse_over_link => Action.mouseOverLink(target, value), .mouse_shape => Action.mouseShape(target, value), .mouse_visibility => Action.mouseVisibility(target, value), @@ -535,7 +537,6 @@ pub const Application = extern struct { .toggle_fullscreen => Action.toggleFullscreen(target), // Unimplemented but todo on gtk-ng branch - .goto_tab, .move_tab, .new_split, .resize_split, @@ -1216,6 +1217,32 @@ const Action = struct { } } + pub fn gotoTab( + target: apprt.Target, + tab: apprt.action.GotoTab, + ) bool { + switch (target) { + .app => return false, + .surface => |core| { + const surface = core.rt_surface.surface; + const window = ext.getAncestor( + Window, + surface.as(gtk.Widget), + ) orelse { + log.warn("surface is not in a window, ignoring new_tab", .{}); + return false; + }; + + return window.selectTab(switch (tab) { + .previous => .previous, + .next => .next, + .last => .last, + else => .{ .n = @intCast(@intFromEnum(tab)) }, + }); + }, + } + } + pub fn mouseOverLink( target: apprt.Target, value: apprt.action.MouseOverLink, diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 70594f53a..8b5074968 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -350,6 +350,63 @@ pub const Window = extern struct { return page; } + pub const SelectTab = union(enum) { + previous, + next, + last, + n: usize, + }; + + /// Select the tab as requested. Returns true if the tab selection + /// changed. + pub fn selectTab(self: *Self, n: SelectTab) bool { + const priv = self.private(); + const tab_view = priv.tab_view; + + // Get our current tab numeric position + const selected = tab_view.getSelectedPage() orelse return false; + const current = tab_view.getPagePosition(selected); + + // Get our total + const total = tab_view.getNPages(); + + const goto: c_int = switch (n) { + .previous => if (current > 0) + current - 1 + else + total - 1, + + .next => if (current < total - 1) + current + 1 + else + 0, + + .last => total - 1, + + .n => |v| n: { + // 1-indexed + if (v == 0) return false; + + const n_int = std.math.cast( + c_int, + v, + ) orelse return false; + break :n @min(n_int - 1, total - 1); + }, + }; + assert(goto >= 0); + assert(goto < total); + + // If our target is the same as our current then we do nothing. + if (goto == current) return false; + + // Add the page and select it + const page = tab_view.getNthPage(goto); + tab_view.setSelectedPage(page); + + return true; + } + /// Updates various appearance properties. This should always be safe /// to call multiple times. This should be called whenever a change /// happens that might affect how the window appears (config change, From a5188142ba5508e3e07a89116b6f3b17cec02814 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 14:52:31 -0700 Subject: [PATCH 25/28] apprt/gtk-ng: move tab --- src/apprt/gtk-ng/class/application.zig | 27 ++++++++++++++++- src/apprt/gtk-ng/class/window.zig | 41 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 2efd6e211..138d2b56e 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -503,6 +503,8 @@ pub const Application = extern struct { .mouse_shape => Action.mouseShape(target, value), .mouse_visibility => Action.mouseVisibility(target, value), + .move_tab => return Action.moveTab(target, value), + .new_tab => return Action.newTab(target), .new_window => try Action.newWindow( @@ -537,7 +539,6 @@ pub const Application = extern struct { .toggle_fullscreen => Action.toggleFullscreen(target), // Unimplemented but todo on gtk-ng branch - .move_tab, .new_split, .resize_split, .equalize_splits, @@ -1301,6 +1302,30 @@ const Action = struct { } } + pub fn moveTab( + target: apprt.Target, + value: apprt.action.MoveTab, + ) bool { + switch (target) { + .app => return false, + .surface => |core| { + const surface = core.rt_surface.surface; + const window = ext.getAncestor( + Window, + surface.as(gtk.Widget), + ) orelse { + log.warn("surface is not in a window, ignoring new_tab", .{}); + return false; + }; + + return window.moveTab( + surface, + @intCast(value.amount), + ); + }, + } + } + pub fn newTab(target: apprt.Target) bool { switch (target) { .app => { diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 8b5074968..8a1045fe7 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -407,6 +407,47 @@ pub const Window = extern struct { return true; } + /// Move the tab containing the given surface by the given amount. + /// Returns if this affected any tab positioning. + pub fn moveTab( + self: *Self, + surface: *Surface, + amount: isize, + ) bool { + const priv = self.private(); + const tab_view = priv.tab_view; + + // If we have one tab we never move. + const total = tab_view.getNPages(); + if (total == 1) return false; + + // Get the tab that contains the given surface. + const tab = ext.getAncestor( + Tab, + surface.as(gtk.Widget), + ) orelse return false; + + // Get the page position that contains the tab. + const page = tab_view.getPage(tab.as(gtk.Widget)); + const pos = tab_view.getPagePosition(page); + + // Move it + const desired_pos: c_int = desired: { + const initial: c_int = @intCast(pos + amount); + const max = total - 1; + break :desired if (initial < 0) + max + initial + 1 + else if (initial > max) + initial - max - 1 + else + initial; + }; + assert(desired_pos >= 0); + assert(desired_pos < total); + + return tab_view.reorderPage(page, desired_pos) != 0; + } + /// Updates various appearance properties. This should always be safe /// to call multiple times. This should be called whenever a change /// happens that might affect how the window appears (config change, From 2847aeb1815763252b8894ac1e79875cae5332ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 14:56:14 -0700 Subject: [PATCH 26/28] apprt/gtk-ng: toggle tab overview --- src/apprt/gtk-ng/class/application.zig | 21 ++++++++++++++++++++- src/apprt/gtk-ng/class/window.zig | 9 +++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 138d2b56e..1345fb273 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -537,6 +537,7 @@ pub const Application = extern struct { .toggle_maximize => Action.toggleMaximize(target), .toggle_fullscreen => Action.toggleFullscreen(target), + .toggle_tab_overview => return Action.toggleTabOverview(target), // Unimplemented but todo on gtk-ng branch .new_split, @@ -549,7 +550,6 @@ pub const Application = extern struct { .present_terminal, .initial_size, .size_limit, - .toggle_tab_overview, .toggle_split_zoom, .toggle_window_decorations, .prompt_title, @@ -1513,6 +1513,25 @@ const Action = struct { .surface => |v| v.rt_surface.surface.toggleMaximize(), } } + + pub fn toggleTabOverview(target: apprt.Target) bool { + switch (target) { + .app => return false, + .surface => |core| { + const surface = core.rt_surface.surface; + const window = ext.getAncestor( + Window, + surface.as(gtk.Widget), + ) orelse { + log.warn("surface is not in a window, ignoring new_tab", .{}); + return false; + }; + + window.toggleTabOverview(); + return true; + }, + } + } }; /// This sets various GTK-related environment variables as necessary diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 8a1045fe7..145a8c7bf 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -215,6 +215,7 @@ pub const Window = extern struct { tab_overview_focus_timer: ?c_uint = null, // Template bindings + tab_overview: *adw.TabOverview, tab_bar: *adw.TabBar, tab_view: *adw.TabView, toolbar: *adw.ToolbarView, @@ -448,6 +449,13 @@ pub const Window = extern struct { return tab_view.reorderPage(page, desired_pos) != 0; } + pub fn toggleTabOverview(self: *Self) void { + const priv = self.private(); + const tab_overview = priv.tab_overview; + const is_open = tab_overview.getOpen() != 0; + tab_overview.setOpen(@intFromBool(!is_open)); + } + /// Updates various appearance properties. This should always be safe /// to call multiple times. This should be called whenever a change /// happens that might affect how the window appears (config change, @@ -1240,6 +1248,7 @@ pub const Window = extern struct { }); // Bindings + class.bindTemplateChildPrivate("tab_overview", .{}); class.bindTemplateChildPrivate("tab_bar", .{}); class.bindTemplateChildPrivate("tab_view", .{}); class.bindTemplateChildPrivate("toolbar", .{}); From 0cc8b6d10fa296c6548f0999120051850076a2fa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 14:57:48 -0700 Subject: [PATCH 27/28] apprt/gtk-ng: remove all shortcuts on tab view --- src/apprt/gtk-ng/ui/1.5/window.blp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 26f303460..ce1741ebb 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -86,6 +86,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { close-page => $close_page(); page-attached => $page_attached(); page-detached => $page_detached(); + shortcuts: none; } } } From fde50e0f1c7b902c6c96344fd94b46ad509179b5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 15:05:36 -0700 Subject: [PATCH 28/28] apprt/gtk-ng: create-window action --- src/apprt/gtk-ng/class/application.zig | 5 ++++- src/apprt/gtk-ng/class/window.zig | 26 ++++++++++++++++++++++---- src/apprt/gtk-ng/ui/1.5/window.blp | 1 + 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 1345fb273..ce6e4d9f6 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -1355,7 +1355,7 @@ const Action = struct { self: *Application, parent: ?*CoreSurface, ) !void { - const win = Window.new(self, parent); + const win = Window.new(self); // Setup a binding so that whenever our config changes so does the // window. There's never a time when the window config should be out @@ -1368,6 +1368,9 @@ const Action = struct { .{}, ); + // Create a new tab + win.newTab(parent); + // Show the window gtk.Window.present(win.as(gtk.Window)); } diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 145a8c7bf..5a61c725b 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -224,12 +224,10 @@ pub const Window = extern struct { pub var offset: c_int = 0; }; - pub fn new(app: *Application, parent_: ?*CoreSurface) *Self { - const self = gobject.ext.newInstance(Self, .{ + pub fn new(app: *Application) *Self { + return gobject.ext.newInstance(Self, .{ .application = app, }); - self.newTab(parent_); - return self; } fn init(self: *Self, _: *Class) callconv(.C) void { @@ -1012,6 +1010,25 @@ pub const Window = extern struct { ); } + fn tabViewCreateWindow( + _: *adw.TabView, + _: *Self, + ) callconv(.c) *adw.TabView { + // Create a new window without creating a new tab. + const win = gobject.ext.newInstance( + Self, + .{ + .application = Application.default(), + }, + ); + + // We have to show it otherwise it'll just be hidden. + gtk.Window.present(win.as(gtk.Window)); + + // Get our tab view + return win.private().tab_view; + } + fn tabCloseRequest( tab: *Tab, self: *Self, @@ -1262,6 +1279,7 @@ pub const Window = extern struct { class.bindTemplateCallback("close_page", &tabViewClosePage); class.bindTemplateCallback("page_attached", &tabViewPageAttached); class.bindTemplateCallback("page_detached", &tabViewPageDetached); + class.bindTemplateCallback("tab_create_window", &tabViewCreateWindow); class.bindTemplateCallback("notify_n_pages", &tabViewNPages); class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage); class.bindTemplateCallback("notify_config", &propConfig); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index ce1741ebb..07057142b 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -86,6 +86,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { close-page => $close_page(); page-attached => $page_attached(); page-detached => $page_detached(); + create-window => $tab_create_window(); shortcuts: none; } }