Skip to content

Commit e97a474

Browse files
committed
gtk: add option to always display the tab bar
Also fixes crashes in both vanilla GTK and Adwaita implementations of `closeTab`, which erroneously close windows twice when there are no more tabs left (we probably already handle it somewhere else).
1 parent f0d2760 commit e97a474

File tree

4 files changed

+114
-99
lines changed

4 files changed

+114
-99
lines changed

src/apprt/gtk/Window.zig

+73-60
Original file line numberDiff line numberDiff line change
@@ -285,81 +285,94 @@ pub fn init(self: *Window, app: *App) !void {
285285
// Our actions for the menu
286286
initActions(self);
287287

288-
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
289-
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
288+
tab_bar: {
289+
if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or !adwaita.enabled(&self.app.config)) {
290+
// Directly place the headerbar in the window when Adwaita is disabled,
291+
// since the tab bar is a part of the headerbar/GtkNotebook
292+
c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget());
293+
c.gtk_window_set_child(gtk_window, box);
294+
break :tab_bar;
295+
}
290296

291-
c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());
297+
const tab_bar = c.adw_tab_bar_new();
298+
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view);
292299

293-
if (self.app.config.@"gtk-tabs-location" != .hidden) {
294-
const tab_bar = c.adw_tab_bar_new();
295-
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view);
300+
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
296301

297-
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
302+
c.adw_tab_bar_set_autohide(
303+
tab_bar,
304+
@intFromBool(switch (app.config.@"window-show-tab-bar") {
305+
.always => false,
306+
.auto, .never => true,
307+
}),
308+
);
309+
310+
const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar));
311+
312+
c.gtk_widget_set_visible(
313+
tab_bar_widget,
314+
@intFromBool(switch (app.config.@"window-show-tab-bar") {
315+
.always, .auto => true,
316+
.never => false,
317+
}),
318+
);
319+
320+
// Can we use ToolbarView (libadw 1.4+)? If so, put both the headerbar
321+
// and the tab bar as sub-bars of the toolbar view
322+
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0)) {
323+
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
324+
325+
c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());
298326

299-
const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar));
300327
switch (self.app.config.@"gtk-tabs-location") {
301328
// left and right are not supported in libadwaita.
302329
.top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
303330
.bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget),
304-
.hidden => unreachable,
331+
.hidden => {},
332+
}
333+
c.adw_toolbar_view_set_content(toolbar_view, box);
334+
335+
const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"adw-toolbar-style") {
336+
.flat => c.ADW_TOOLBAR_FLAT,
337+
.raised => c.ADW_TOOLBAR_RAISED,
338+
.@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER,
339+
};
340+
c.adw_toolbar_view_set_top_bar_style(toolbar_view, toolbar_style);
341+
c.adw_toolbar_view_set_bottom_bar_style(toolbar_view, toolbar_style);
342+
343+
// Set our application window content.
344+
c.adw_tab_overview_set_child(
345+
@ptrCast(self.tab_overview),
346+
@ptrCast(@alignCast(toolbar_view)),
347+
);
348+
c.adw_application_window_set_content(
349+
@ptrCast(gtk_window),
350+
@ptrCast(@alignCast(self.tab_overview)),
351+
);
352+
} else {
353+
// Older libadwaita versions don't support toolbar views.
354+
// We have to add the tab bar directly to the main box, either
355+
// as the last child of the box (bottom) or right after the
356+
// header bar (every other option except hidden)
357+
switch (app.config.@"gtk-tabs-location") {
358+
.top,
359+
.left,
360+
.right,
361+
=> c.gtk_box_insert_child_after(@ptrCast(box), @ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(self.headerbar.asWidget()))),
362+
363+
.bottom => c.gtk_box_append(
364+
@ptrCast(box),
365+
@ptrCast(@alignCast(tab_bar)),
366+
),
367+
.hidden => {},
305368
}
306-
}
307-
c.adw_toolbar_view_set_content(toolbar_view, box);
308-
309-
const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"adw-toolbar-style") {
310-
.flat => c.ADW_TOOLBAR_FLAT,
311-
.raised => c.ADW_TOOLBAR_RAISED,
312-
.@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER,
313-
};
314-
c.adw_toolbar_view_set_top_bar_style(toolbar_view, toolbar_style);
315-
c.adw_toolbar_view_set_bottom_bar_style(toolbar_view, toolbar_style);
316-
317-
// Set our application window content.
318-
c.adw_tab_overview_set_child(
319-
@ptrCast(self.tab_overview),
320-
@ptrCast(@alignCast(toolbar_view)),
321-
);
322-
c.adw_application_window_set_content(
323-
@ptrCast(gtk_window),
324-
@ptrCast(@alignCast(self.tab_overview)),
325-
);
326-
} else tab_bar: {
327-
switch (self.notebook) {
328-
.adw => |*adw| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
329-
if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar;
330-
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
331-
// an AdwToolbarView.
332-
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
333-
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
334-
switch (app.config.@"gtk-tabs-location") {
335-
.top,
336-
.left,
337-
.right,
338-
=> c.gtk_box_insert_child_after(@ptrCast(box), @ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(self.headerbar.asWidget()))),
339-
340-
.bottom => c.gtk_box_append(
341-
@ptrCast(box),
342-
@ptrCast(@alignCast(tab_bar)),
343-
),
344-
.hidden => unreachable,
345-
}
346-
c.adw_tab_bar_set_view(tab_bar, adw.tab_view);
347-
348-
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
349-
},
350369

351-
.gtk => {},
352-
}
370+
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
353371

354-
// The box is our main child
355-
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
356372
c.adw_application_window_set_content(
357373
@ptrCast(gtk_window),
358374
box,
359375
);
360-
} else {
361-
c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget());
362-
c.gtk_window_set_child(gtk_window, box);
363376
}
364377
}
365378

src/apprt/gtk/notebook_adw.zig

-20
Original file line numberDiff line numberDiff line change
@@ -134,26 +134,6 @@ pub const NotebookAdw = struct {
134134

135135
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
136136
c.adw_tab_view_close_page(self.tab_view, page);
137-
138-
// If we have no more tabs we close the window
139-
if (self.nPages() == 0) {
140-
const window = tab.window.window;
141-
142-
// libadw versions <= 1.3.x leak the final page view
143-
// which causes our surface to not properly cleanup. We
144-
// unref to force the cleanup. This will trigger a critical
145-
// warning from GTK, but I don't know any other workaround.
146-
// Note: I'm not actually sure if 1.4.0 contains the fix,
147-
// I just know that 1.3.x is broken and 1.5.1 is fixed.
148-
// If we know that 1.4.0 is fixed, we can change this.
149-
if (!adwaita.versionAtLeast(1, 4, 0)) {
150-
c.g_object_unref(tab.box);
151-
}
152-
153-
// `self` will become invalid after this call because it will have
154-
// been freed up as part of the process of closing the window.
155-
c.gtk_window_destroy(window);
156-
}
157137
}
158138
};
159139

src/apprt/gtk/notebook_gtk.zig

+13-19
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ pub const NotebookGtk = struct {
3131
};
3232
c.gtk_notebook_set_tab_pos(gtk_notebook, notebook_tab_pos);
3333
c.gtk_notebook_set_scrollable(gtk_notebook, 1);
34-
c.gtk_notebook_set_show_tabs(gtk_notebook, 0);
3534
c.gtk_notebook_set_show_border(gtk_notebook, 0);
3635

3736
// This enables all Ghostty terminal tabs to be exchanged across windows.
@@ -81,6 +80,7 @@ pub const NotebookGtk = struct {
8180
log.warn("currentTab", .{});
8281
const page = self.currentPage() orelse return null;
8382
const child = c.gtk_notebook_get_nth_page(self.notebook, page);
83+
8484
return @ptrCast(@alignCast(
8585
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
8686
));
@@ -164,9 +164,7 @@ pub const NotebookGtk = struct {
164164
c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
165165
c.gtk_notebook_set_tab_detachable(self.notebook, box_widget, 1);
166166

167-
if (self.nPages() > 1) {
168-
c.gtk_notebook_set_show_tabs(self.notebook, 1);
169-
}
167+
self.updateShowTabs(window);
170168

171169
// Switch to the new tab
172170
c.gtk_notebook_set_current_page(self.notebook, page_idx);
@@ -183,20 +181,21 @@ pub const NotebookGtk = struct {
183181
c.gtk_notebook_remove_page(self.notebook, page_idx);
184182

185183
const remaining = self.nPages();
186-
switch (remaining) {
187-
// If we have no more tabs we close the window
188-
0 => c.gtk_window_destroy(tab.window.window),
189-
190-
// If we have one more tab we hide the tab bar
191-
1 => c.gtk_notebook_set_show_tabs(self.notebook, 0),
192-
193-
else => {},
194-
}
195184

196185
// If we have remaining tabs, we need to make sure we grab focus.
197186
if (remaining > 0)
198187
(self.currentTab() orelse return).window.focusCurrentTab();
199188
}
189+
190+
fn updateShowTabs(self: *NotebookGtk, window: *Window) void {
191+
const show_tabs = switch (window.app.config.@"window-show-tab-bar") {
192+
.always => true,
193+
.auto => self.nPages() > 1,
194+
.never => false,
195+
};
196+
197+
c.gtk_notebook_set_show_tabs(self.notebook, @intFromBool(show_tabs));
198+
}
200199
};
201200

202201
fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int {
@@ -245,12 +244,7 @@ fn gtkPageRemoved(
245244
log.warn("gtkPageRemoved", .{});
246245
const window: *Window = @ptrCast(@alignCast(ud.?));
247246

248-
// Hide the tab bar if we only have one tab after removal
249-
const remaining = c.gtk_notebook_get_n_pages(window.notebook.gtk.notebook);
250-
251-
if (remaining == 1) {
252-
c.gtk_notebook_set_show_tabs(window.notebook.gtk.notebook, 0);
253-
}
247+
window.notebook.gtk.updateShowTabs(window);
254248
}
255249

256250
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {

src/config/Config.zig

+28
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,27 @@ keybind: Keybinds = .{},
13361336
/// * `end` - Insert the new tab at the end of the tab list.
13371337
@"window-new-tab-position": WindowNewTabPosition = .current,
13381338

1339+
/// Whether to show the tab bar.
1340+
///
1341+
/// Valid values:
1342+
///
1343+
/// - `always`
1344+
///
1345+
/// Always display the tab bar, even when there's only one tab.
1346+
///
1347+
/// - `auto` *(default)*
1348+
///
1349+
/// Automatically show and hide the tab bar. The tab bar is only
1350+
/// shown when there are two or more tabs present.
1351+
///
1352+
/// - `never`
1353+
///
1354+
/// Never show the tab bar. Tabs are only accessible via the tab
1355+
/// overview or by keybind actions.
1356+
///
1357+
/// Currently only supported on Linux (GTK).
1358+
@"window-show-tab-bar": WindowShowTabBar = .auto,
1359+
13391360
/// Background color for the window titlebar. This only takes effect if
13401361
/// window-theme is set to ghostty. Currently only supported in the GTK app
13411362
/// runtime.
@@ -5786,6 +5807,13 @@ pub const WindowNewTabPosition = enum {
57865807
end,
57875808
};
57885809

5810+
/// See window-show-tab-bar
5811+
pub const WindowShowTabBar = enum {
5812+
always,
5813+
auto,
5814+
never,
5815+
};
5816+
57895817
/// See resize-overlay
57905818
pub const ResizeOverlay = enum {
57915819
always,

0 commit comments

Comments
 (0)