gtk-ng: add "title bar styles" (#8166)

This PR adds a "tabs" title bar style similar to the macOS title bar
style. When `gtk-titlebar-style=tabs` the title bar and the tab bar will
be merged together.

The config entry for controlling this is kept separate from macOS as
macOS has more styles defined that don't map to a GTK title bar style
and it's likely that users that use both macOS and GTK would want
different settings for each platform.

<img width="922" height="722" alt="Screenshot From 2025-08-06 16-38-28"
src="https://github.com/user-attachments/assets/3c2db235-695a-457e-9c96-5039120263fc"
/>
pull/8185/head
Mitchell Hashimoto 2025-08-08 12:34:21 -07:00 committed by GitHub
commit 17101294aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 178 additions and 19 deletions

View File

@ -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,27 +854,37 @@ 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") {
return switch (config.@"gtk-titlebar-style") {
// If the titlebar style is tabs we cannot autohide.
.tabs => false,
.native => switch (config.@"window-show-tab-bar") {
// Auto we always autohide... obviously.
.auto => true,
@ -855,16 +894,30 @@ pub const Window = extern struct {
// 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;
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 +936,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 +1051,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 +1772,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 +1800,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);

View File

@ -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 <bool>;
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 <bool>;
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 {

View File

@ -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,