diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index b74305acc..30a82d963 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -11,6 +11,7 @@ const gtk = @import("gtk"); const i18n = @import("../../../os/main.zig").i18n; const apprt = @import("../../../apprt.zig"); const configpkg = @import("../../../config.zig"); +const TitlebarStyle = configpkg.Config.GtkTitlebarStyle; const input = @import("../../../input.zig"); const CoreSurface = @import("../../../Surface.zig"); const ext = @import("../ext.zig"); @@ -96,6 +97,25 @@ pub const Window = extern struct { ); }; + pub const @"titlebar-style" = struct { + pub const name = "titlebar-style"; + const impl = gobject.ext.defineProperty( + name, + Self, + TitlebarStyle, + .{ + .default = .native, + .accessor = gobject.ext.typedAccessor( + Self, + TitlebarStyle, + .{ + .getter = Self.getTitlebarStyle, + }, + ), + }, + ); + }; + pub const @"headerbar-visible" = struct { pub const name = "headerbar-visible"; const impl = gobject.ext.defineProperty( @@ -548,6 +568,7 @@ pub const Window = extern struct { "tabs-visible", "tabs-wide", "toolbar-style", + "titlebar-style", }) |key| { self.as(gobject.Object).notifyByPspec( @field(properties, key).impl.param_spec, @@ -814,6 +835,14 @@ pub const Window = extern struct { return false; } + fn isFullscreen(self: *Window) bool { + return self.as(gtk.Window).isFullscreen() != 0; + } + + fn isMaximized(self: *Window) bool { + return self.as(gtk.Window).isMaximized() != 0; + } + fn getHeaderbarVisible(self: *Self) bool { const priv = self.private(); @@ -825,46 +854,72 @@ pub const Window = extern struct { if (priv.quick_terminal) return false; // If we're fullscreen we never show the header bar. - if (self.as(gtk.Window).isFullscreen() != 0) return false; + if (self.isFullscreen()) 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") - { + // *Conditionally* disable the header bar when maximized, and + // gtk-titlebar-hide-when-maximized is set + if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") { return false; } - return config.@"gtk-titlebar"; + return switch (config.@"gtk-titlebar-style") { + // If the titlebar style is tabs never show the titlebar. + .tabs => false, + + // If the titlebar style is native show the titlebar if configured + // to do so. + .native => config.@"gtk-titlebar", + }; } 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, + return switch (config.@"gtk-titlebar-style") { + // If the titlebar style is tabs we cannot autohide. + .tabs => false, - // Never we autohide because it doesn't actually matter, - // since getTabsVisible will return false. - .never => true, + .native => 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, - }; + + switch (config.@"gtk-titlebar-style") { + .tabs => { + // *Conditionally* disable the tab bar when maximized, the titlebar + // style is tabs, and gtk-titlebar-hide-when-maximized is set. + if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") { + return false; + } + + // If the titlebar style is tabs the tab bar must always be visible. + return true; + }, + .native => { + return switch (config.@"window-show-tab-bar") { + .always, .auto => true, + .never => false, + }; + }, + } } fn getTabsWide(self: *Self) bool { @@ -883,6 +938,12 @@ pub const Window = extern struct { }; } + fn getTitlebarStyle(self: *Self) TitlebarStyle { + const priv = self.private(); + const config = if (priv.config) |v| v.get() else return .native; + return config.@"gtk-titlebar-style"; + } + fn propConfig( _: *adw.ApplicationWindow, _: *gobject.ParamSpec, @@ -992,6 +1053,16 @@ pub const Window = extern struct { }; } + fn closureTitlebarStyleIsTab( + _: *Self, + value: TitlebarStyle, + ) callconv(.c) bool { + return switch (value) { + .native => false, + .tabs => true, + }; + } + //--------------------------------------------------------------- // Virtual methods @@ -1703,6 +1774,7 @@ pub const Window = extern struct { properties.@"tabs-visible".impl, properties.@"tabs-wide".impl, properties.@"toolbar-style".impl, + properties.@"titlebar-style".impl, }); // Bindings @@ -1730,6 +1802,7 @@ pub const Window = extern struct { class.bindTemplateCallback("notify_menu_active", &propMenuActive); class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal); class.bindTemplateCallback("notify_scale_factor", &propScaleFactor); + class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 121a1b45a..4ca90dfb5 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -79,6 +79,64 @@ template $GhosttyWindow: Adw.ApplicationWindow { expand-tabs: bind template.tabs-wide; view: tab_view; visible: bind template.tabs-visible; + + [start] + Gtk.Box { + orientation: horizontal; + visible: bind $titlebar_style_is_tabs(template.titlebar-style) as ; + + Gtk.WindowControls { + side: start; + } + + Adw.SplitButton { + styles [ + "flat", + ] + + clicked => $new_tab(); + icon-name: "tab-new-symbolic"; + tooltip-text: _("New Tab"); + dropdown-tooltip: _("New Split"); + menu-model: split_menu; + can-focus: false; + focus-on-click: false; + } + } + + [end] + Gtk.Box { + orientation: horizontal; + visible: bind $titlebar_style_is_tabs(template.titlebar-style) as ; + + Gtk.ToggleButton { + styles [ + "flat", + ] + + 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 { + styles [ + "flat", + ] + + notify::active => $notify_menu_active(); + icon-name: "open-menu-symbolic"; + menu-model: main_menu; + tooltip-text: _("Main Menu"); + can-focus: false; + } + + Gtk.WindowControls { + side: end; + } + } } Box { diff --git a/src/config/Config.zig b/src/config/Config.zig index bca82ff79..2cf5a3e17 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2892,6 +2892,21 @@ else /// more subtle border. @"gtk-toolbar-style": GtkToolbarStyle = .raised, +/// The style of the GTK titlbar. Available values are `native` and `tabs`. +/// +/// The `native` titlebar style is a traditional titlebar with a title, a few +/// buttons and window controls. A separate tab bar will show up below the +/// titlebar if you have multiple tabs open in the window. +/// +/// The `tabs` titlebar merges the tab bar and the traditional titlebar. +/// This frees up vertical space on your screen if you use multiple tabs. One +/// limitation of the `tabs` titlebar is that you cannot drag the titlebar +/// by the titles any longer (as they are tab titles now). Other areas of the +/// `tabs` title bar can be used to drag the window around. +/// +/// The default style is `native`. +@"gtk-titlebar-style": GtkTitlebarStyle = .native, + /// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs /// are the new typical Gnome style where tabs fill their available space. /// If you set this to `false` then tabs will only take up space they need, @@ -6947,6 +6962,21 @@ pub const GtkToolbarStyle = enum { @"raised-border", }; +/// See gtk-titlebar-style +pub const GtkTitlebarStyle = enum(c_int) { + native, + tabs, + + pub const getGObjectType = switch (build_config.app_runtime) { + .gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum( + GtkTitlebarStyle, + .{ .name = "GhosttyGtkTitlebarStyle" }, + ), + + .none => void, + }; +}; + /// See app-notifications pub const AppNotifications = packed struct { @"clipboard-copy": bool = true,