Skip to content

Commit

Permalink
GTK: refactor headerbar into separate Adwaita & GTK structs (#4850)
Browse files Browse the repository at this point in the history
There's one behavioral change here. Before this patch, if
`gtk-titlebar=false` we _never_ created a headerbar. This explicitly
contradicted the comments in the source, and the documentation for
`gtk-titlebar` imply that if a window starts out without a titlebar it
can be brought back later with the `toggle_window_decorations` keybind
action.

After this patch, a headerbar is always created, but if
`gtk-titlebar=false` or `window-decoration=false` it's immediately
hidden.

I'm not sure how this interacts with the current SSD/CSD detection that
seems to happen when running Ghostty on non-Gnome DEs so it'll be
important to get #4724 merged (plus any follow ups) to enable more
explicit control of SSD/CSD.
  • Loading branch information
mitchellh authored Jan 10, 2025
2 parents 96b3db0 + 010f4d1 commit c4ece2a
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 132 deletions.
129 changes: 53 additions & 76 deletions src/apprt/gtk/Window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ window: *c.GtkWindow,
/// The header bar for the window. This is possibly null since it can be
/// disabled using gtk-titlebar. This is either an AdwHeaderBar or
/// GtkHeaderBar depending on if adw is enabled and linked.
header: ?HeaderBar,
headerbar: HeaderBar,

/// The tab overview for the window. This is possibly null since there is no
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
Expand Down Expand Up @@ -78,7 +78,7 @@ pub fn init(self: *Window, app: *App) !void {
self.* = .{
.app = app,
.window = undefined,
.header = null,
.headerbar = undefined,
.tab_overview = null,
.notebook = undefined,
.context_menu = undefined,
Expand Down Expand Up @@ -151,64 +151,56 @@ pub fn init(self: *Window, app: *App) !void {
break :overview tab_overview;
} else null;

// gtk-titlebar can be used to disable the header bar (but keep
// the window manager's decorations). We create this no matter if we
// are decorated or not because we can have a keybind to toggle the
// decorations.
if (app.config.@"gtk-titlebar") {
const header = HeaderBar.init(self);
// gtk-titlebar can be used to disable the header bar (but keep the window
// manager's decorations). We create this no matter if we are decorated or
// not because we can have a keybind to toggle the decorations.
self.headerbar.init();

// If we are not decorated then we hide the titlebar.
header.setVisible(app.config.@"window-decoration");
{
const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
self.headerbar.packEnd(btn);
}

{
const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
header.packEnd(btn);
}
// If we're using an AdwWindow then we can support the tab overview.
if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
const btn = switch (app.config.@"gtk-tabs-location") {
.top, .bottom, .left, .right => btn: {
const btn = c.gtk_toggle_button_new();
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
_ = c.g_object_bind_property(
btn,
"active",
tab_overview,
"open",
c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
);

break :btn btn;
},

// If we're using an AdwWindow then we can support the tab overview.
if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
const btn = switch (app.config.@"gtk-tabs-location") {
.top, .bottom, .left, .right => btn: {
const btn = c.gtk_toggle_button_new();
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
_ = c.g_object_bind_property(
btn,
"active",
tab_overview,
"open",
c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
);

break :btn btn;
},

.hidden => btn: {
const btn = c.adw_tab_button_new();
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
break :btn btn;
},
};

c.gtk_widget_set_focus_on_click(btn, c.FALSE);
header.packEnd(btn);
}
.hidden => btn: {
const btn = c.adw_tab_button_new();
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
break :btn btn;
},
};

{
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
c.gtk_widget_set_tooltip_text(btn, "New Tab");
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(&gtkTabNewClick), self, null, c.G_CONNECT_DEFAULT);
header.packStart(btn);
}
c.gtk_widget_set_focus_on_click(btn, c.FALSE);
self.headerbar.packEnd(btn);
}

self.header = header;
{
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
c.gtk_widget_set_tooltip_text(btn, "New Tab");
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(&gtkTabNewClick), self, null, c.G_CONNECT_DEFAULT);
self.headerbar.packStart(btn);
}

_ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(&gtkWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
Expand All @@ -221,9 +213,7 @@ pub fn init(self: *Window, app: *App) !void {
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
// need to stick the headerbar into the content box.
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
if (self.header) |h| {
c.gtk_box_append(@ptrCast(box), h.asWidget());
}
c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget());
}

// In debug we show a warning and apply the 'devel' class to the window.
Expand Down Expand Up @@ -298,10 +288,7 @@ pub fn init(self: *Window, app: *App) !void {
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());

if (self.header) |header| {
const header_widget = header.asWidget();
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
}
c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());

if (self.app.config.@"gtk-tabs-location" != .hidden) {
const tab_bar = c.adw_tab_bar_new();
Expand Down Expand Up @@ -374,10 +361,8 @@ pub fn init(self: *Window, app: *App) !void {
box,
);
} else {
c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget());
c.gtk_window_set_child(gtk_window, box);
if (self.header) |h| {
c.gtk_window_set_titlebar(gtk_window, h.asWidget());
}
}
}

Expand Down Expand Up @@ -459,18 +444,12 @@ pub fn deinit(self: *Window) void {

/// Set the title of the window.
pub fn setTitle(self: *Window, title: [:0]const u8) void {
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config) and self.app.config.@"gtk-titlebar") {
if (self.header) |header| header.setTitle(title);
} else {
c.gtk_window_set_title(self.window, title);
}
self.headerbar.setTitle(title);
}

/// Set the subtitle of the window if it has one.
pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void {
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config) and self.app.config.@"gtk-titlebar") {
if (self.header) |header| header.setSubtitle(subtitle);
}
self.headerbar.setSubtitle(subtitle);
}

/// Add a new tab to this window.
Expand Down Expand Up @@ -563,9 +542,7 @@ pub fn toggleWindowDecorations(self: *Window) void {
// decorated state. GTK tends to consider the titlebar part of the frame
// and hides it with decorations, but libadwaita doesn't. This makes it
// explicit.
if (self.header) |headerbar| {
headerbar.setVisible(new_decorated);
}
self.headerbar.setVisible(new_decorated);
}

/// Grabs focus on the currently selected tab.
Expand Down
77 changes: 21 additions & 56 deletions src/apprt/gtk/headerbar.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,93 +4,58 @@ const c = @import("c.zig").c;
const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");

const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else void;
const HeaderBarAdw = @import("headerbar_adw.zig");
const HeaderBarGtk = @import("headerbar_gtk.zig");

pub const HeaderBar = union(enum) {
adw: *AdwHeaderBar,
gtk: *c.GtkHeaderBar,

pub fn init(window: *Window) HeaderBar {
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
adwaita.enabled(&window.app.config))
{
return initAdw(window);
adw: HeaderBarAdw,
gtk: HeaderBarGtk,

pub fn init(self: *HeaderBar) void {
const window: *Window = @fieldParentPtr("headerbar", self);
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&window.app.config)) {
HeaderBarAdw.init(self);
} else {
HeaderBarGtk.init(self);
}

return initGtk();
}

fn initAdw(window: *Window) HeaderBar {
const headerbar = c.adw_header_bar_new();
c.adw_header_bar_set_title_widget(@ptrCast(headerbar), @ptrCast(c.adw_window_title_new(c.gtk_window_get_title(window.window) orelse "Ghostty", null)));
return .{ .adw = @ptrCast(headerbar) };
}

fn initGtk() HeaderBar {
const headerbar = c.gtk_header_bar_new();
return .{ .gtk = @ptrCast(headerbar) };
if (!window.app.config.@"gtk-titlebar" or !window.app.config.@"window-decoration")
self.setVisible(false);
}

pub fn setVisible(self: HeaderBar, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
switch (self) {
inline else => |v| v.setVisible(visible),
}
}

pub fn asWidget(self: HeaderBar) *c.GtkWidget {
return switch (self) {
.adw => |headerbar| @ptrCast(@alignCast(headerbar)),
.gtk => |headerbar| @ptrCast(@alignCast(headerbar)),
inline else => |v| v.asWidget(),
};
}

pub fn packEnd(self: HeaderBar, widget: *c.GtkWidget) void {
switch (self) {
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_end(
@ptrCast(@alignCast(headerbar)),
widget,
);
},
.gtk => |headerbar| c.gtk_header_bar_pack_end(
@ptrCast(@alignCast(headerbar)),
widget,
),
inline else => |v| v.packEnd(widget),
}
}

pub fn packStart(self: HeaderBar, widget: *c.GtkWidget) void {
switch (self) {
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_start(
@ptrCast(@alignCast(headerbar)),
widget,
);
},
.gtk => |headerbar| c.gtk_header_bar_pack_start(
@ptrCast(@alignCast(headerbar)),
widget,
),
inline else => |v| v.packStart(widget),
}
}

pub fn setTitle(self: HeaderBar, title: [:0]const u8) void {
switch (self) {
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
const window_title: *c.AdwWindowTitle = @ptrCast(c.adw_header_bar_get_title_widget(@ptrCast(headerbar)));
c.adw_window_title_set_title(window_title, title);
},
// The title is owned by the window when not using Adwaita
.gtk => unreachable,
inline else => |v| v.setTitle(title),
}
}

pub fn setSubtitle(self: HeaderBar, subtitle: [:0]const u8) void {
switch (self) {
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
const window_title: *c.AdwWindowTitle = @ptrCast(c.adw_header_bar_get_title_widget(@ptrCast(headerbar)));
c.adw_window_title_set_subtitle(window_title, subtitle);
},
// There is no subtitle unless Adwaita is used
.gtk => unreachable,
inline else => |v| v.setSubtitle(subtitle),
}
}
};
77 changes: 77 additions & 0 deletions src/apprt/gtk/headerbar_adw.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const HeaderBarAdw = @This();

const std = @import("std");
const c = @import("c.zig").c;

const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");

const HeaderBar = @import("headerbar.zig").HeaderBar;

const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else anyopaque;
const AdwWindowTitle = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwWindowTitle else anyopaque;

/// the window that this headerbar is attached to
window: *Window,
/// the Adwaita headerbar widget
headerbar: *AdwHeaderBar,
/// the Adwaita window title widget
title: *AdwWindowTitle,

pub fn init(headerbar: *HeaderBar) void {
if (!adwaita.versionAtLeast(0, 0, 0)) return;

const window: *Window = @fieldParentPtr("headerbar", headerbar);
headerbar.* = .{
.adw = .{
.window = window,
.headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
.title = @ptrCast(@alignCast(c.adw_window_title_new(
c.gtk_window_get_title(window.window) orelse "Ghostty",
null,
))),
},
};
c.adw_header_bar_set_title_widget(
headerbar.adw.headerbar,
@ptrCast(@alignCast(headerbar.adw.title)),
);
}

pub fn setVisible(self: HeaderBarAdw, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}

pub fn asWidget(self: HeaderBarAdw) *c.GtkWidget {
return @ptrCast(@alignCast(self.headerbar));
}

pub fn packEnd(self: HeaderBarAdw, widget: *c.GtkWidget) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_end(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
}

pub fn packStart(self: HeaderBarAdw, widget: *c.GtkWidget) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_start(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
}

pub fn setTitle(self: HeaderBarAdw, title: [:0]const u8) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_window_title_set_title(self.title, title);
}
}

pub fn setSubtitle(self: HeaderBarAdw, subtitle: [:0]const u8) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_window_title_set_subtitle(self.title, subtitle);
}
}
Loading

0 comments on commit c4ece2a

Please sign in to comment.