From 775f3dfca37b8916d4d930c4d57c1bb7806c45ee Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Jul 2025 10:25:24 -0700 Subject: [PATCH] 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 {} } } }