diff --git a/src/apprt/gtk-ng/Surface.zig b/src/apprt/gtk-ng/Surface.zig index 7613abd2d..f32d264cd 100644 --- a/src/apprt/gtk-ng/Surface.zig +++ b/src/apprt/gtk-ng/Surface.zig @@ -39,10 +39,18 @@ pub fn cgroup(self: *Self) ?[]const u8 { return self.surface.cgroupPath(); } +pub fn setPwd(self: *Self, pwd: [:0]const u8) void { + return self.surface.setPwd(pwd); +} + pub fn getTitle(self: *Self) ?[:0]const u8 { return self.surface.getTitle(); } +pub fn setTitle(self: *Self, title: [:0]const u8) void { + return self.surface.setTitle(title); +} + pub fn getContentScale(self: *const Self) !apprt.ContentScale { return self.surface.getContentScale(); } diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index d3e02e28d..41b550c7e 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -1086,6 +1086,7 @@ const Action = struct { const win = Window.new(self); gtk.Window.present(win.as(gtk.Window)); + win.setupInitialFocus(); } pub fn pwd( @@ -1095,13 +1096,7 @@ const Action = struct { switch (target) { .app => log.warn("pwd to app is unexpected", .{}), .surface => |surface| { - var v = gobject.ext.Value.newFrom(value.pwd); - defer v.unset(); - gobject.Object.setProperty( - surface.rt_surface.gobj().as(gobject.Object), - "pwd", - &v, - ); + surface.rt_surface.setPwd(value.pwd); }, } } @@ -1130,13 +1125,7 @@ const Action = struct { switch (target) { .app => log.warn("set_title to app is unexpected", .{}), .surface => |surface| { - var v = gobject.ext.Value.newFrom(value.title); - defer v.unset(); - gobject.Object.setProperty( - surface.rt_surface.gobj().as(gobject.Object), - "title", - &v, - ); + surface.rt_surface.setTitle(value.title); }, } } diff --git a/src/apprt/gtk-ng/class/config.zig b/src/apprt/gtk-ng/class/config.zig index e40602f47..ace1c7bc1 100644 --- a/src/apprt/gtk-ng/class/config.zig +++ b/src/apprt/gtk-ng/class/config.zig @@ -52,6 +52,24 @@ pub const Config = extern struct { }, ); + pub const @"gtk-wide-tabs" = gobject.ext.defineProperty( + "gtk-wide-tabs", + Self, + bool, + .{ + .nick = "gtk-wide-tabs", + .blurb = null, + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ + .getter = Self.gtkWideTabs, + }, + ), + }, + ); + pub const @"has-diagnostics" = gobject.ext.defineProperty( "has-diagnostics", Self, @@ -105,6 +123,12 @@ pub const Config = extern struct { return &self.private().config; } + /// Returns the current value of gtk-wide-tabs. + pub fn gtkWideTabs(self: *Self) bool { + const config = self.get(); + return config.@"gtk-wide-tabs"; + } + /// Returns whether this configuration has any diagnostics. pub fn hasDiagnostics(self: *Self) bool { const config = self.get(); @@ -163,6 +187,7 @@ pub const Config = extern struct { gobject.Object.virtual_methods.finalize.implement(class, &finalize); gobject.ext.registerProperties(class, &.{ properties.@"diagnostics-buffer", + properties.@"gtk-wide-tabs", properties.@"has-diagnostics", }); } diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 9a64cd101..7d7482436 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -1085,11 +1085,42 @@ pub const Surface = extern struct { //--------------------------------------------------------------- // Properties + /// Returns the pwd property without a copy. + pub fn getPwd(self: *Self) ?[:0]const u8 { + return self.private().pwd; + } + + /// Set the pwd property. + pub fn setPwd(self: *Self, pwd: [:0]const u8) void { + const priv = self.private(); + + if (priv.title) |v| { + glib.free(@constCast(@ptrCast(v))); + priv.title = null; + } + + priv.pwd = std.mem.span(glib.strdup(pwd)); + self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec); + } + /// Returns the title property without a copy. pub fn getTitle(self: *Self) ?[:0]const u8 { return self.private().title; } + /// Set the title property. + pub fn setTitle(self: *Self, title: [:0]const u8) void { + const priv = self.private(); + + if (priv.title) |v| { + glib.free(@constCast(@ptrCast(v))); + priv.title = null; + } + + priv.title = std.mem.span(glib.strdup(title)); + self.as(gobject.Object).notifyByPspec(properties.title.impl.param_spec); + } + fn propConfig( self: *Self, _: *gobject.ParamSpec, diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 6e30389a0..6e0fcf375 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -8,6 +8,7 @@ const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Surface = @import("surface.zig").Surface; +const Config = @import("config.zig").Config; const log = std.log.scoped(.gtk_ghostty_window); @@ -23,7 +24,34 @@ pub const Window = extern struct { .private = .{ .Type = Private, .offset = &Private.offset }, }); + pub const properties = struct { + pub const config = struct { + pub const name = "config"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Config, + .{ + .nick = "Config", + .blurb = "The configuration that this window is using.", + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "config", + ), + }, + ); + }; + }; + const Private = struct { + /// The configuration that this window is using. + config: ?*Config = null, + + /// The window title widget. + window_title: *adw.WindowTitle = undefined, + /// The surface in the view. surface: *Surface = undefined, @@ -34,10 +62,19 @@ pub const Window = extern struct { return gobject.ext.newInstance(Self, .{ .application = app }); } + pub fn setupInitialFocus(self: *Self) void { + _ = self.private().surface.as(gtk.Widget).grabFocus(); + } + fn init(self: *Self, _: *Class) callconv(.C) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); - const surface = self.private().surface; + const priv = self.private(); + + const app = Application.default(); + priv.config = app.getConfig(); + + const surface = priv.surface; _ = Surface.signals.@"close-request".connect( surface, *Self, @@ -45,6 +82,32 @@ pub const Window = extern struct { self, .{}, ); + _ = gobject.Object.signals.notify.connect( + surface, + *Self, + &surfaceNotifyHasFocus, + self, + .{ .detail = "has-focus" }, + ); + self.setupSurfacePropertyConnections(surface); + } + + fn setupSurfacePropertyConnections(self: *Self, surface: *Surface) void { + _ = gobject.Object.signals.notify.connect( + surface, + *Self, + &surfaceNotifyTitle, + self, + .{ .detail = "title" }, + ); + + _ = gobject.Object.signals.notify.connect( + surface, + *Self, + &surfaceNotifyPwd, + self, + .{ .detail = "pwd" }, + ); } //--------------------------------------------------------------- @@ -56,12 +119,30 @@ pub const Window = extern struct { getGObjectType(), ); + const priv = self.private(); + if (priv.config) |v| { + v.unref(); + priv.config = null; + } + gobject.Object.virtual_methods.dispose.call( Class.parent, self.as(Parent), ); } + /// Set the title of the window. + fn setTitle(self: *Self, title: [:0]const u8) void { + const window_title = self.private().window_title; + window_title.setTitle(title); + } + + /// Set the subtitle of the window. + fn setSubtitle(self: *Self, subtitle: [:0]const u8) void { + const window_title = self.private().window_title; + window_title.setSubtitle(subtitle); + } + //--------------------------------------------------------------- // Signal handlers @@ -77,6 +158,42 @@ pub const Window = extern struct { self.as(gtk.Window).close(); } + fn surfaceNotifyHasFocus(surface: *Surface, _: *gobject.ParamSpec, self: *Self) callconv(.c) void { + assert(surface == self.private().surface); + + self.setTitle(surface.getTitle().?); + + const subtitle: [:0]const u8 = switch (Application.default().getConfig().get().@"window-subtitle") { + .@"working-directory" => surface.getPwd() orelse "", + .false => "", + }; + self.setSubtitle(subtitle); + } + + fn surfaceNotifyTitle(surface: *Surface, _: *gobject.ParamSpec, self: *Self) callconv(.c) void { + assert(surface == self.private().surface); + + if (surface.as(gtk.Widget).grabFocus() == 0) { + return; + } + + self.setTitle(surface.getTitle().?); + } + + fn surfaceNotifyPwd(surface: *Surface, _: *gobject.ParamSpec, self: *Self) callconv(.c) void { + assert(surface == self.private().surface); + + if (surface.as(gtk.Widget).grabFocus() == 0) { + return; + } + + if (Application.default().getConfig().get().@"window-subtitle" != .@"working-directory") { + return; + } + + self.setSubtitle(surface.getPwd() orelse ""); + } + const C = Common(Self, Private); pub const as = C.as; pub const ref = C.ref; @@ -100,8 +217,14 @@ pub const Window = extern struct { ); // Bindings + class.bindTemplateChildPrivate("window_title", .{}); class.bindTemplateChildPrivate("surface", .{}); + // Properties + gobject.ext.registerProperties(class, &.{ + properties.config.impl, + }); + // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); } diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index 3d17b1d56..1b1d4b756 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -2,6 +2,7 @@ using Gtk 4.0; using Adw 1; template $GhosttySurface: Adw.Bin { + focusable: true; // We need to wrap our Overlay one more time because if you bind a // direct child of your widget to a property, it will double free: // https://gitlab.gnome.org/GNOME/gtk/-/blob/847571a1e314aba79260e4ef282e2ed9ba91a0d9/gtk/gtkwidget.c#L11423-11425 diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 930ee53db..5dc94f58c 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -1,9 +1,192 @@ using Gtk 4.0; using Adw 1; +menu split_menu { + item { + label: _("Split Up"); + action: "win.split-up"; + } + + item { + label: _("Split Down"); + action: "win.split-down"; + } + + item { + label: _("Split Left"); + action: "win.split-left"; + } + + item { + label: _("Split Right"); + action: "win.split-right"; + } +} + +menu main_menu { + section { + item { + label: _("Copy"); + action: "win.copy"; + } + + item { + label: _("Paste"); + action: "win.paste"; + } + } + + section { + item { + label: _("New Window"); + action: "win.new-window"; + } + + item { + label: _("Close Window"); + action: "win.close"; + } + } + + section { + item { + label: _("New Tab"); + action: "win.new-tab"; + } + + item { + label: _("Close Tab"); + action: "win.close-tab"; + } + } + + section { + submenu { + label: _("Split"); + + item { + label: _("Change Title…"); + action: "win.prompt-title"; + } + + item { + label: _("Split Up"); + action: "win.split-up"; + } + + item { + label: _("Split Down"); + action: "win.split-down"; + } + + item { + label: _("Split Left"); + action: "win.split-left"; + } + + item { + label: _("Split Right"); + action: "win.split-right"; + } + } + } + + section { + item { + label: _("Clear"); + action: "win.clear"; + } + + item { + label: _("Reset"); + action: "win.reset"; + } + } + + section { + item { + label: _("Command Palette"); + action: "win.toggle-command-palette"; + } + + item { + label: _("Terminal Inspector"); + action: "win.toggle-inspector"; + } + + item { + label: _("Open Configuration"); + action: "app.open-config"; + } + + item { + label: _("Reload Configuration"); + action: "app.reload-config"; + } + } + + section { + item { + label: _("About Ghostty"); + action: "win.about"; + } + + item { + label: _("Quit"); + action: "app.quit"; + } + } +} + template $GhosttyWindow: Adw.ApplicationWindow { default-width: 800; default-height: 600; - content: $GhosttySurface surface {}; + content: Adw.TabOverview tab_overview { + view: tab_view; + + child: Adw.ToolbarView { + content: Gtk.Box { + orientation: vertical; + + Adw.HeaderBar { + title-widget: Adw.WindowTitle window_title { + title: "Ghostty"; + }; + + [start] + Adw.SplitButton { + icon-name: "tab-new-symbolic"; + menu-model: split_menu; + tooltip-text: _("New Tab"); + dropdown-tooltip: _("New Split"); + } + + [end] + Gtk.Box { + Gtk.ToggleButton { + icon-name: "view-grid-symbolic"; + tooltip-text: _("View Open Tabs"); + active: bind tab_overview.open bidirectional; + } + + Gtk.MenuButton { + icon-name: "open-menu-symbolic"; + menu-model: main_menu; + tooltip-text: _("Main Menu"); + } + } + } + + Adw.TabBar { + view: tab_view; + expand-tabs: bind (template.config as <$GhosttyConfig>).gtk-wide-tabs; + } + + Adw.TabView tab_view { + $GhosttySurface surface {} + } + }; + }; + }; }