From 0e22695ec48cfd74c2fe5366996746012ab1eb27 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 8 Jun 2024 13:23:38 +0200 Subject: [PATCH 01/16] gtk: use adwaita tab view when possible add tab bar view --- src/apprt/gtk/Surface.zig | 2 +- src/apprt/gtk/Tab.zig | 124 ++++++++++---------- src/apprt/gtk/Window.zig | 234 +++++++++++++++++++++++++++++--------- 3 files changed, 242 insertions(+), 118 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 99d0fb9de..6d2b93aef 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -894,7 +894,7 @@ fn updateTitleLabels(self: *Surface) void { // If we have a tab and are the focused child, then we have to update the tab if (self.container.tab()) |tab| { - if (tab.focus_child == self) c.gtk_label_set_text(tab.label_text, title.ptr); + if (tab.focus_child == self) tab.setLabelText(title); // c.gtk_label_set_text(tab.label_text, title.ptr); } // If we have a window and are focused, then we have to update the window title. diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 42a18711b..8a2f05513 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -55,37 +55,23 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { .focus_child = undefined, }; - // Build the tab label - const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0); - const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget)); - const label_text_widget = c.gtk_label_new("Ghostty"); - const label_text: *c.GtkLabel = @ptrCast(label_text_widget); - c.gtk_box_append(label_box, label_text_widget); - self.label_text = label_text; + // // Wide style GTK tabs + // if (window.app.config.@"gtk-wide-tabs") { + // c.gtk_widget_set_hexpand(label_box_widget, 1); + // c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL); + // c.gtk_widget_set_hexpand(label_text_widget, 1); + // c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL); - // Build the close button for the tab - const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic"); - const label_close: *c.GtkButton = @ptrCast(label_close_widget); - c.gtk_button_set_has_frame(label_close, 0); - c.gtk_box_append(label_box, label_close_widget); + // // This ensures that tabs are always equal width. If they're too + // // long, they'll be truncated with an ellipsis. + // c.gtk_label_set_max_width_chars(label_text, 1); + // c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END); - // Wide style GTK tabs - if (window.app.config.@"gtk-wide-tabs") { - c.gtk_widget_set_hexpand(label_box_widget, 1); - c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL); - c.gtk_widget_set_hexpand(label_text_widget, 1); - c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL); - - // This ensures that tabs are always equal width. If they're too - // long, they'll be truncated with an ellipsis. - c.gtk_label_set_max_width_chars(label_text, 1); - c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END); - - // We need to set a minimum width so that at a certain point - // the notebook will have an arrow button rather than shrinking tabs - // to an unreadably small size. - c.gtk_widget_set_size_request(label_text_widget, 100, 1); - } + // // We need to set a minimum width so that at a certain point + // // the notebook will have an arrow button rather than shrinking tabs + // // to an unreadably small size. + // c.gtk_widget_set_size_request(label_text_widget, 100, 1); + // } // Create a Box in which we'll later keep either Surface or Split. // Using a box makes it easier to maintain the tab contents because @@ -106,47 +92,51 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // Add Surface to the Tab c.gtk_box_append(self.box, surface.primaryWidget()); - // Add the notebook page (create tab). - const parent_page_idx = switch (window.app.config.@"window-new-tab-position") { - .current => c.gtk_notebook_get_current_page(window.notebook) + 1, - .end => c.gtk_notebook_get_n_pages(window.notebook), - }; + try window.notebook.addTab(box_widget, "Ghostty"); - const page_idx = c.gtk_notebook_insert_page( - window.notebook, - box_widget, - label_box_widget, - parent_page_idx, - ); - if (page_idx < 0) { - log.warn("failed to add page to notebook", .{}); - return error.GtkAppendPageFailed; - } + // const notebook: *c.GtkNotebook = window.notebook.as_notebook(); - // Tab settings - c.gtk_notebook_set_tab_reorderable(window.notebook, box_widget, 1); - c.gtk_notebook_set_tab_detachable(window.notebook, box_widget, 1); + // // Add the notebook page (create tab). + // const parent_page_idx = switch (window.app.config.@"window-new-tab-position") { + // .current => c.gtk_notebook_get_current_page(notebook) + 1, + // .end => c.gtk_notebook_get_n_pages(notebook), + // }; - // If we have multiple tabs, show the tab bar. - if (c.gtk_notebook_get_n_pages(window.notebook) > 1) { - c.gtk_notebook_set_show_tabs(window.notebook, 1); - } + // const page_idx = c.gtk_notebook_insert_page( + // notebook, + // box_widget, + // label_box_widget, + // parent_page_idx, + // ); + // if (page_idx < 0) { + // log.warn("failed to add page to notebook", .{}); + // return error.GtkAppendPageFailed; + // } + + // // Tab settings + // c.gtk_notebook_set_tab_reorderable(notebook, box_widget, 1); + // c.gtk_notebook_set_tab_detachable(notebook, box_widget, 1); + + // // If we have multiple tabs, show the tab bar. + // if (c.gtk_notebook_get_n_pages(notebook) > 1) { + // c.gtk_notebook_set_show_tabs(notebook, 1); + // } // Set the userdata of the box to point to this tab. c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); - // Clicks - const gesture_tab_click = c.gtk_gesture_click_new(); - c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0); - c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click)); + // // Clicks + // const gesture_tab_click = c.gtk_gesture_click_new(); + // c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0); + // c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click)); - // Attach all events - _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), self, null, c.G_CONNECT_DEFAULT); + // // Attach all events + // _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT); + // _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + // _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), self, null, c.G_CONNECT_DEFAULT); - // Switch to the new tab - c.gtk_notebook_set_current_page(window.notebook, page_idx); + // // Switch to the new tab + // c.gtk_notebook_set_current_page(notebook, page_idx); // We need to grab focus after Surface and Tab is added to the window. When // creating a Tab we want to always focus on the widget. @@ -175,6 +165,18 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { self.elem = elem; } +pub fn setLabelText(self: *Tab, title: [:0]const u8) void { + switch (self.window.notebook) { + .adw_tab_view => |tab_view| { + const page = c.adw_tab_view_get_page(tab_view, @ptrCast(self.box)); + c.adw_tab_page_set_title(page, title.ptr); + }, + .gtk_notebook => { + c.gtk_label_set_text(self.label_text, title.ptr); + } + } +} + /// Remove this tab from the window. pub fn remove(self: *Tab) void { self.window.closeTab(self); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 3446aa304..7bfe62efb 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -8,6 +8,7 @@ const Window = @This(); const std = @import("std"); const builtin = @import("builtin"); const build_config = @import("../../build_config.zig"); +const build_options = @import("build_options"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const configpkg = @import("../../config.zig"); @@ -23,13 +24,139 @@ const c = @import("c.zig").c; const log = std.log.scoped(.gtk); +pub const Notebook = union(enum) { + adw_tab_view: *c.AdwTabView, + gtk_notebook: *c.GtkNotebook, + + pub fn create(window: *Window) @This() { + const app = window.app; + + // Create a notebook to hold our tabs. + const notebook_widget = c.gtk_notebook_new(); + const notebook: *c.GtkNotebook = @ptrCast(notebook_widget); + const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") { + .top => c.GTK_POS_TOP, + .bottom => c.GTK_POS_BOTTOM, + .left => c.GTK_POS_LEFT, + .right => c.GTK_POS_RIGHT, + }; + c.gtk_notebook_set_tab_pos(notebook, notebook_tab_pos); + c.gtk_notebook_set_scrollable(notebook, 1); + c.gtk_notebook_set_show_tabs(notebook, 0); + c.gtk_notebook_set_show_border(notebook, 0); + + // This enables all Ghostty terminal tabs to be exchanged across windows. + c.gtk_notebook_set_group_name(notebook, "ghostty-terminal-tabs"); + + // This is important so the notebook expands to fit available space. + // Otherwise, it will be zero/zero in the box below. + c.gtk_widget_set_vexpand(notebook_widget, 1); + c.gtk_widget_set_hexpand(notebook_widget, 1); + + // If we are in fullscreen mode, new windows start fullscreen. + if (app.config.fullscreen) c.gtk_window_fullscreen(window.window); + + // All of our events + _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT); + + return .{ .gtk_notebook = notebook }; + } + + pub fn as_widget(self: Notebook) *c.GtkWidget { + return switch (self) { + .adw_tab_view => |ptr| @ptrCast(@alignCast(ptr)), + .gtk_notebook => |ptr| @ptrCast(@alignCast(ptr)), + }; + } + + pub fn as_notebook(self: Notebook) *c.GtkNotebook { + return switch (self) { + .adw_tab_view => @panic("adw tab view"), + .gtk_notebook => |notebook| notebook, + }; + } + + pub fn nPages(self: Notebook) c_int { + return switch (self) { + .adw_tab_view => |tab_view| c.adw_tab_view_get_n_pages(tab_view), + .gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook), + }; + } + + pub fn currentPage(self: Notebook) c_int { + switch (self) { + .adw_tab_view => |tab_view| { + const page = c.adw_tab_view_get_selected_page(tab_view); + return c.adw_tab_view_get_page_position(tab_view, page); + }, + .gtk_notebook => |notebook| return c.gtk_notebook_get_current_page(notebook), + } + } + + pub fn currentTab(self: Notebook) ?*Tab { + const child = switch (self) { + .adw_tab_view => |tab_view| child: { + const page = c.adw_tab_view_get_selected_page(tab_view); + const child = c.adw_tab_page_get_child(page); + break :child child; + }, + .gtk_notebook => |notebook| child: { + const page = self.currentPage(); + break :child c.gtk_notebook_get_nth_page(notebook, page); + }, + }; + return @ptrCast(@alignCast( + c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null, + )); + } + + pub fn addTab(self: Notebook, tab_widget: *c.GtkWidget, title: [:0]const u8) !void { + switch (self) { + .adw_tab_view => |tab_view| { + const page = c.adw_tab_view_append(tab_view, tab_widget); + c.adw_tab_page_set_title(page, title); + }, + .gtk_notebook => |notebook| { + // Build the tab label + const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0); + const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget)); + const label_text_widget = c.gtk_label_new("Ghostty"); + // const label_text: *c.GtkLabel = @ptrCast(label_text_widget); + c.gtk_box_append(label_box, label_text_widget); + // self.label_text = label_text; + + // Build the close button for the tab + const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic"); + const label_close: *c.GtkButton = @ptrCast(label_close_widget); + c.gtk_button_set_has_frame(label_close, 0); + c.gtk_box_append(label_box, label_close_widget); + + const parent_page_idx = self.nPages(); + const page_idx = c.gtk_notebook_insert_page( + notebook, + tab_widget, + label_box_widget, + parent_page_idx, + ); + _ = page_idx; + } + } + } +}; + app: *App, /// Our window window: *c.GtkWindow, /// The notebook (tab grouping) for this window. -notebook: *c.GtkNotebook, +/// can be either c.GtkNotebook or c.AdwTabView. +notebook: Notebook, context_menu: *c.GtkWidget, @@ -107,29 +234,6 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_window_set_decorated(gtk_window, 0); } - // Create a notebook to hold our tabs. - const notebook_widget = c.gtk_notebook_new(); - const notebook: *c.GtkNotebook = @ptrCast(notebook_widget); - self.notebook = notebook; - const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") { - .top => c.GTK_POS_TOP, - .bottom => c.GTK_POS_BOTTOM, - .left => c.GTK_POS_LEFT, - .right => c.GTK_POS_RIGHT, - }; - c.gtk_notebook_set_tab_pos(notebook, notebook_tab_pos); - c.gtk_notebook_set_scrollable(notebook, 1); - c.gtk_notebook_set_show_tabs(notebook, 0); - c.gtk_notebook_set_show_border(notebook, 0); - - // This enables all Ghostty terminal tabs to be exchanged across windows. - c.gtk_notebook_set_group_name(notebook, "ghostty-terminal-tabs"); - - // This is important so the notebook expands to fit available space. - // Otherwise, it will be zero/zero in the box below. - c.gtk_widget_set_vexpand(notebook_widget, 1); - c.gtk_widget_set_hexpand(notebook_widget, 1); - // Create our box which will hold our widgets. const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); @@ -141,7 +245,19 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_widget_set_margin_bottom(warning, 10); c.gtk_box_append(@ptrCast(box), warning); } - c.gtk_box_append(@ptrCast(box), notebook_widget); + + const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita"; + if (adwaita) { + log.warn("using adwaita", .{}); + const tab_view = c.adw_tab_view_new(); + const tab_bar = c.adw_tab_bar_new(); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); + c.adw_tab_bar_set_view(tab_bar, tab_view.?); + self.notebook = .{ .adw_tab_view = tab_view.? }; + } else { + self.notebook = Notebook.create(self); + } + c.gtk_box_append(@ptrCast(box), self.notebook.as_widget()); self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu))); c.gtk_widget_set_parent(self.context_menu, window); @@ -155,10 +271,6 @@ pub fn init(self: *Window, app: *App) !void { _ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), self, null, c.G_CONNECT_DEFAULT); // Our actions for the menu initActions(self); @@ -219,22 +331,23 @@ pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { /// Close the tab for the given notebook page. This will automatically /// handle closing the window if there are no more tabs. pub fn closeTab(self: *Window, tab: *Tab) void { - const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return; + const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; // Find page and tab which we're closing const page_idx = getNotebookPageIndex(page); // Remove the page. This will destroy the GTK widgets in the page which // will trigger Tab cleanup. - c.gtk_notebook_remove_page(self.notebook, page_idx); + c.gtk_notebook_remove_page(notebook, page_idx); - const remaining = c.gtk_notebook_get_n_pages(self.notebook); + const remaining = c.gtk_notebook_get_n_pages(notebook); switch (remaining) { // If we have no more tabs we close the window 0 => c.gtk_window_destroy(self.window), // If we have one more tab we hide the tab bar - 1 => c.gtk_notebook_set_show_tabs(self.notebook, 0), + 1 => c.gtk_notebook_set_show_tabs(notebook, 0), else => {}, } @@ -245,7 +358,7 @@ pub fn closeTab(self: *Window, tab: *Tab) void { /// Returns true if this window has any tabs. pub fn hasTabs(self: *const Window) bool { - return c.gtk_notebook_get_n_pages(self.notebook) > 1; + return c.gtk_notebook_get_n_pages(self.notebook.as_notebook()) > 0; } /// Go to the previous tab for a surface. @@ -255,19 +368,20 @@ pub fn gotoPreviousTab(self: *Window, surface: *Surface) void { return; }; - const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return; + const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; const page_idx = getNotebookPageIndex(page); // The next index is the previous or we wrap around. const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: { - const max = c.gtk_notebook_get_n_pages(self.notebook); + const max = c.gtk_notebook_get_n_pages(notebook); break :next_idx max -| 1; }; // Do nothing if we have one tab if (next_idx == page_idx) return; - c.gtk_notebook_set_current_page(self.notebook, next_idx); + c.gtk_notebook_set_current_page(notebook, next_idx); self.focusCurrentTab(); } @@ -278,13 +392,14 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void { return; }; - const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return; + const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; const page_idx = getNotebookPageIndex(page); - const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1; + const max = c.gtk_notebook_get_n_pages(notebook) -| 1; const next_idx = if (page_idx < max) page_idx + 1 else 0; if (next_idx == page_idx) return; - c.gtk_notebook_set_current_page(self.notebook, next_idx); + c.gtk_notebook_set_current_page(notebook, next_idx); self.focusCurrentTab(); } @@ -298,10 +413,11 @@ pub fn gotoLastTab(self: *Window) void { /// Go to the specific tab index. pub fn gotoTab(self: *Window, n: usize) void { if (n == 0) return; - const max = c.gtk_notebook_get_n_pages(self.notebook); + const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + const max = c.gtk_notebook_get_n_pages(notebook); const page_idx = std.math.cast(c_int, n - 1) orelse return; if (page_idx < max) { - c.gtk_notebook_set_current_page(self.notebook, page_idx); + c.gtk_notebook_set_current_page(notebook, page_idx); self.focusCurrentTab(); } } @@ -327,11 +443,13 @@ pub fn toggleWindowDecorations(self: *Window) void { /// Grabs focus on the currently selected tab. fn focusCurrentTab(self: *Window) void { - const page_idx = c.gtk_notebook_get_current_page(self.notebook); - const page = c.gtk_notebook_get_nth_page(self.notebook, page_idx); - const tab: *Tab = @ptrCast(@alignCast( - c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return, - )); + // const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + // const page_idx = c.gtk_notebook_get_current_page(notebook); + // const page = c.gtk_notebook_get_nth_page(notebook, page_idx); + // const tab: *Tab = @ptrCast(@alignCast( + // c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return, + // )); + const tab = self.notebook.currentTab() orelse return; const gl_area = @as(*c.GtkWidget, @ptrCast(tab.focus_child.gl_area)); _ = c.gtk_widget_grab_focus(gl_area); } @@ -379,16 +497,18 @@ fn gtkPageRemoved( ) callconv(.C) void { const self = userdataSelf(ud.?); + const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + // Hide the tab bar if we only have one tab after removal - const remaining = c.gtk_notebook_get_n_pages(self.notebook); + const remaining = c.gtk_notebook_get_n_pages(notebook); if (remaining == 1) { - c.gtk_notebook_set_show_tabs(self.notebook, 0); + c.gtk_notebook_set_show_tabs(notebook, 0); } } fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void { const self = userdataSelf(ud.?); - const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page))); + const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook.as_notebook(), page))); const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box))); const label_text = c.gtk_label_get_text(gtk_label); c.gtk_window_set_title(self.window, label_text); @@ -417,7 +537,7 @@ fn gtkNotebookCreateWindow( // And add it to the new window. tab.window = window; - return window.notebook; + return window.notebook.as_notebook(); } fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { @@ -652,11 +772,13 @@ fn gtkActionReset( /// Returns the surface to use for an action. fn actionSurface(self: *Window) ?*CoreSurface { - const page_idx = c.gtk_notebook_get_current_page(self.notebook); - const page = c.gtk_notebook_get_nth_page(self.notebook, page_idx); - const tab: *Tab = @ptrCast(@alignCast( - c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null, - )); + const tab = self.notebook.currentTab() orelse return null; + // const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + // const page_idx = c.gtk_notebook_get_current_page(notebook); + // const page = c.gtk_notebook_get_nth_page(notebook, page_idx); + // const tab: *Tab = @ptrCast(@alignCast( + // c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null, + // )); return &tab.focus_child.core_surface; } From 42c93d89fcee98e6473bb103898c8ac2ea21e502 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 8 Jun 2024 14:25:35 +0200 Subject: [PATCH 02/16] adw: move notebook to its own file --- src/apprt/gtk/Tab.zig | 17 +- src/apprt/gtk/Window.zig | 278 ++----------------------------- src/apprt/gtk/notebook.zig | 330 +++++++++++++++++++++++++++++++++++++ 3 files changed, 347 insertions(+), 278 deletions(-) create mode 100644 src/apprt/gtk/notebook.zig diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 8a2f05513..a58917b07 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -92,7 +92,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // Add Surface to the Tab c.gtk_box_append(self.box, surface.primaryWidget()); - try window.notebook.addTab(box_widget, "Ghostty"); + try window.notebook.addTab(self, "Ghostty"); // const notebook: *c.GtkNotebook = window.notebook.as_notebook(); @@ -132,12 +132,9 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // // Attach all events // _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT); - // _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); // _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), self, null, c.G_CONNECT_DEFAULT); - // // Switch to the new tab - // c.gtk_notebook_set_current_page(notebook, page_idx); - // We need to grab focus after Surface and Tab is added to the window. When // creating a Tab we want to always focus on the widget. surface.grabFocus(); @@ -166,15 +163,7 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { } pub fn setLabelText(self: *Tab, title: [:0]const u8) void { - switch (self.window.notebook) { - .adw_tab_view => |tab_view| { - const page = c.adw_tab_view_get_page(tab_view, @ptrCast(self.box)); - c.adw_tab_page_set_title(page, title.ptr); - }, - .gtk_notebook => { - c.gtk_label_set_text(self.label_text, title.ptr); - } - } + self.window.notebook.setTabLabel(self, title); } /// Remove this tab from the window. diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 7bfe62efb..c269b51bf 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -21,134 +21,10 @@ const Color = configpkg.Config.Color; const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); const c = @import("c.zig").c; +const Notebook = @import("./notebook.zig").Notebook; const log = std.log.scoped(.gtk); -pub const Notebook = union(enum) { - adw_tab_view: *c.AdwTabView, - gtk_notebook: *c.GtkNotebook, - - pub fn create(window: *Window) @This() { - const app = window.app; - - // Create a notebook to hold our tabs. - const notebook_widget = c.gtk_notebook_new(); - const notebook: *c.GtkNotebook = @ptrCast(notebook_widget); - const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") { - .top => c.GTK_POS_TOP, - .bottom => c.GTK_POS_BOTTOM, - .left => c.GTK_POS_LEFT, - .right => c.GTK_POS_RIGHT, - }; - c.gtk_notebook_set_tab_pos(notebook, notebook_tab_pos); - c.gtk_notebook_set_scrollable(notebook, 1); - c.gtk_notebook_set_show_tabs(notebook, 0); - c.gtk_notebook_set_show_border(notebook, 0); - - // This enables all Ghostty terminal tabs to be exchanged across windows. - c.gtk_notebook_set_group_name(notebook, "ghostty-terminal-tabs"); - - // This is important so the notebook expands to fit available space. - // Otherwise, it will be zero/zero in the box below. - c.gtk_widget_set_vexpand(notebook_widget, 1); - c.gtk_widget_set_hexpand(notebook_widget, 1); - - // If we are in fullscreen mode, new windows start fullscreen. - if (app.config.fullscreen) c.gtk_window_fullscreen(window.window); - - // All of our events - _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT); - - return .{ .gtk_notebook = notebook }; - } - - pub fn as_widget(self: Notebook) *c.GtkWidget { - return switch (self) { - .adw_tab_view => |ptr| @ptrCast(@alignCast(ptr)), - .gtk_notebook => |ptr| @ptrCast(@alignCast(ptr)), - }; - } - - pub fn as_notebook(self: Notebook) *c.GtkNotebook { - return switch (self) { - .adw_tab_view => @panic("adw tab view"), - .gtk_notebook => |notebook| notebook, - }; - } - - pub fn nPages(self: Notebook) c_int { - return switch (self) { - .adw_tab_view => |tab_view| c.adw_tab_view_get_n_pages(tab_view), - .gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook), - }; - } - - pub fn currentPage(self: Notebook) c_int { - switch (self) { - .adw_tab_view => |tab_view| { - const page = c.adw_tab_view_get_selected_page(tab_view); - return c.adw_tab_view_get_page_position(tab_view, page); - }, - .gtk_notebook => |notebook| return c.gtk_notebook_get_current_page(notebook), - } - } - - pub fn currentTab(self: Notebook) ?*Tab { - const child = switch (self) { - .adw_tab_view => |tab_view| child: { - const page = c.adw_tab_view_get_selected_page(tab_view); - const child = c.adw_tab_page_get_child(page); - break :child child; - }, - .gtk_notebook => |notebook| child: { - const page = self.currentPage(); - break :child c.gtk_notebook_get_nth_page(notebook, page); - }, - }; - return @ptrCast(@alignCast( - c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null, - )); - } - - pub fn addTab(self: Notebook, tab_widget: *c.GtkWidget, title: [:0]const u8) !void { - switch (self) { - .adw_tab_view => |tab_view| { - const page = c.adw_tab_view_append(tab_view, tab_widget); - c.adw_tab_page_set_title(page, title); - }, - .gtk_notebook => |notebook| { - // Build the tab label - const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0); - const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget)); - const label_text_widget = c.gtk_label_new("Ghostty"); - // const label_text: *c.GtkLabel = @ptrCast(label_text_widget); - c.gtk_box_append(label_box, label_text_widget); - // self.label_text = label_text; - - // Build the close button for the tab - const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic"); - const label_close: *c.GtkButton = @ptrCast(label_close_widget); - c.gtk_button_set_has_frame(label_close, 0); - c.gtk_box_append(label_box, label_close_widget); - - const parent_page_idx = self.nPages(); - const page_idx = c.gtk_notebook_insert_page( - notebook, - tab_widget, - label_box_widget, - parent_page_idx, - ); - _ = page_idx; - } - } - } -}; - app: *App, /// Our window @@ -246,17 +122,7 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_box_append(@ptrCast(box), warning); } - const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita"; - if (adwaita) { - log.warn("using adwaita", .{}); - const tab_view = c.adw_tab_view_new(); - const tab_bar = c.adw_tab_bar_new(); - c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); - c.adw_tab_bar_set_view(tab_bar, tab_view.?); - self.notebook = .{ .adw_tab_view = tab_view.? }; - } else { - self.notebook = Notebook.create(self); - } + self.notebook = Notebook.create(self, box); c.gtk_box_append(@ptrCast(box), self.notebook.as_widget()); self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu))); @@ -331,34 +197,12 @@ pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { /// Close the tab for the given notebook page. This will automatically /// handle closing the window if there are no more tabs. pub fn closeTab(self: *Window, tab: *Tab) void { - const notebook: *c.GtkNotebook = self.notebook.as_notebook(); - const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; - - // Find page and tab which we're closing - const page_idx = getNotebookPageIndex(page); - - // Remove the page. This will destroy the GTK widgets in the page which - // will trigger Tab cleanup. - c.gtk_notebook_remove_page(notebook, page_idx); - - const remaining = c.gtk_notebook_get_n_pages(notebook); - switch (remaining) { - // If we have no more tabs we close the window - 0 => c.gtk_window_destroy(self.window), - - // If we have one more tab we hide the tab bar - 1 => c.gtk_notebook_set_show_tabs(notebook, 0), - - else => {}, - } - - // If we have remaining tabs, we need to make sure we grab focus. - if (remaining > 0) self.focusCurrentTab(); + self.notebook.closeTab(tab); } /// Returns true if this window has any tabs. pub fn hasTabs(self: *const Window) bool { - return c.gtk_notebook_get_n_pages(self.notebook.as_notebook()) > 0; + return self.notebook.nPages() > 0; } /// Go to the previous tab for a surface. @@ -368,9 +212,9 @@ pub fn gotoPreviousTab(self: *Window, surface: *Surface) void { return; }; - const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; - const page_idx = getNotebookPageIndex(page); + const page_idx = Notebook.getNotebookPageIndex(page); // The next index is the previous or we wrap around. const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: { @@ -392,9 +236,9 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void { return; }; - const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; - const page_idx = getNotebookPageIndex(page); + const page_idx = Notebook.getNotebookPageIndex(page); const max = c.gtk_notebook_get_n_pages(notebook) -| 1; const next_idx = if (page_idx < max) page_idx + 1 else 0; if (next_idx == page_idx) return; @@ -405,15 +249,15 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void { /// Go to the next tab for a surface. pub fn gotoLastTab(self: *Window) void { - const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1; - c.gtk_notebook_set_current_page(self.notebook, max); + const max = self.notebook.nPages() -| 1; + c.gtk_notebook_set_current_page(self.notebook.gtk_notebook, max); self.focusCurrentTab(); } /// Go to the specific tab index. pub fn gotoTab(self: *Window, n: usize) void { if (n == 0) return; - const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; const max = c.gtk_notebook_get_n_pages(notebook); const page_idx = std.math.cast(c_int, n - 1) orelse return; if (page_idx < max) { @@ -442,13 +286,7 @@ pub fn toggleWindowDecorations(self: *Window) void { } /// Grabs focus on the currently selected tab. -fn focusCurrentTab(self: *Window) void { - // const notebook: *c.GtkNotebook = self.notebook.as_notebook(); - // const page_idx = c.gtk_notebook_get_current_page(notebook); - // const page = c.gtk_notebook_get_nth_page(notebook, page_idx); - // const tab: *Tab = @ptrCast(@alignCast( - // c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return, - // )); +pub fn focusCurrentTab(self: *Window) void { const tab = self.notebook.currentTab() orelse return; const gl_area = @as(*c.GtkWidget, @ptrCast(tab.focus_child.gl_area)); _ = c.gtk_widget_grab_focus(gl_area); @@ -465,81 +303,6 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { }; } -fn gtkPageAdded( - notebook: *c.GtkNotebook, - _: *c.GtkWidget, - page_idx: c.guint, - ud: ?*anyopaque, -) callconv(.C) void { - const self = userdataSelf(ud.?); - - // The added page can come from another window with drag and drop, thus we migrate the tab - // window to be self. - const page = c.gtk_notebook_get_nth_page(notebook, @intCast(page_idx)); - const tab: *Tab = @ptrCast(@alignCast( - c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return, - )); - tab.window = self; - - // Whenever a new page is added, we always grab focus of the - // currently selected page. This was added specifically so that when - // we drag a tab out to create a new window ("create-window" event) - // we grab focus in the new window. Without this, the terminal didn't - // have focus. - self.focusCurrentTab(); -} - -fn gtkPageRemoved( - _: *c.GtkNotebook, - _: *c.GtkWidget, - _: c.guint, - ud: ?*anyopaque, -) callconv(.C) void { - const self = userdataSelf(ud.?); - - const notebook: *c.GtkNotebook = self.notebook.as_notebook(); - - // Hide the tab bar if we only have one tab after removal - const remaining = c.gtk_notebook_get_n_pages(notebook); - if (remaining == 1) { - c.gtk_notebook_set_show_tabs(notebook, 0); - } -} - -fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void { - const self = userdataSelf(ud.?); - const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook.as_notebook(), page))); - const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box))); - const label_text = c.gtk_label_get_text(gtk_label); - c.gtk_window_set_title(self.window, label_text); -} - -fn gtkNotebookCreateWindow( - _: *c.GtkNotebook, - page: *c.GtkWidget, - ud: ?*anyopaque, -) callconv(.C) ?*c.GtkNotebook { - // The tab for the page is stored in the widget data. - const tab: *Tab = @ptrCast(@alignCast( - c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null, - )); - - const currentWindow = userdataSelf(ud.?); - const alloc = currentWindow.app.core_app.alloc; - const app = currentWindow.app; - - // Create a new window - const window = Window.create(alloc, app) catch |err| { - log.warn("error creating new window error={}", .{err}); - return null; - }; - - // And add it to the new window. - tab.window = window; - - return window.notebook.as_notebook(); -} - fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { _ = v; log.debug("refocus term request", .{}); @@ -621,19 +384,6 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { alloc.destroy(self); } -fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int { - var value: c.GValue = std.mem.zeroes(c.GValue); - defer c.g_value_unset(&value); - _ = c.g_value_init(&value, c.G_TYPE_INT); - c.g_object_get_property( - @ptrCast(@alignCast(page)), - "position", - &value, - ); - - return c.g_value_get_int(&value); -} - fn gtkActionAbout( _: *c.GSimpleAction, _: *c.GVariant, @@ -773,7 +523,7 @@ fn gtkActionReset( /// Returns the surface to use for an action. fn actionSurface(self: *Window) ?*CoreSurface { const tab = self.notebook.currentTab() orelse return null; - // const notebook: *c.GtkNotebook = self.notebook.as_notebook(); + // const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; // const page_idx = c.gtk_notebook_get_current_page(notebook); // const page = c.gtk_notebook_get_nth_page(notebook, page_idx); // const tab: *Tab = @ptrCast(@alignCast( @@ -782,6 +532,6 @@ fn actionSurface(self: *Window) ?*CoreSurface { return &tab.focus_child.core_surface; } -fn userdataSelf(ud: *anyopaque) *Window { +pub fn userdataSelf(ud: *anyopaque) *Window { return @ptrCast(@alignCast(ud)); } diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig new file mode 100644 index 000000000..8bb6850dc --- /dev/null +++ b/src/apprt/gtk/notebook.zig @@ -0,0 +1,330 @@ +const std = @import("std"); +const c = @import("c.zig").c; +const build_options = @import("build_options"); + +const Window = @import("./Window.zig"); +const userdataSelf = Window.userdataSelf; +const Tab = @import("./Tab.zig"); + +const log = std.log.scoped(.gtk); + +const AdwTabView = if (build_options.libadwaita) c.AdwTabView else anyopaque; + +pub const Notebook = union(enum) { + adw_tab_view: *AdwTabView, + gtk_notebook: *c.GtkNotebook, + + pub fn create(window: *Window, box: *c.GtkWidget) @This() { + const app = window.app; + + const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita"; + + if (adwaita) { + log.warn("using adwaita", .{}); + const tab_view = c.adw_tab_view_new(); + const tab_bar = c.adw_tab_bar_new(); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); + c.adw_tab_bar_set_view(tab_bar, tab_view.?); + + _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); + + return .{ .adw_tab_view = tab_view.? }; + } + + // Create a notebook to hold our tabs. + const notebook_widget = c.gtk_notebook_new(); + const notebook: *c.GtkNotebook = @ptrCast(notebook_widget); + const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") { + .top => c.GTK_POS_TOP, + .bottom => c.GTK_POS_BOTTOM, + .left => c.GTK_POS_LEFT, + .right => c.GTK_POS_RIGHT, + }; + c.gtk_notebook_set_tab_pos(notebook, notebook_tab_pos); + c.gtk_notebook_set_scrollable(notebook, 1); + c.gtk_notebook_set_show_tabs(notebook, 0); + c.gtk_notebook_set_show_border(notebook, 0); + + // This enables all Ghostty terminal tabs to be exchanged across windows. + c.gtk_notebook_set_group_name(notebook, "ghostty-terminal-tabs"); + + // This is important so the notebook expands to fit available space. + // Otherwise, it will be zero/zero in the box below. + c.gtk_widget_set_vexpand(notebook_widget, 1); + c.gtk_widget_set_hexpand(notebook_widget, 1); + + // If we are in fullscreen mode, new windows start fullscreen. + if (app.config.fullscreen) c.gtk_window_fullscreen(window.window); + + // All of our events + _ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT); + + return .{ .gtk_notebook = notebook }; + } + + pub fn as_widget(self: Notebook) *c.GtkWidget { + return switch (self) { + .adw_tab_view => |ptr| @ptrCast(@alignCast(ptr)), + .gtk_notebook => |ptr| @ptrCast(@alignCast(ptr)), + }; + } + + pub fn nPages(self: Notebook) c_int { + return switch (self) { + .adw_tab_view => |tab_view| if (build_options.libadwaita) c.adw_tab_view_get_n_pages(tab_view) else unreachable, + .gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook), + }; + } + + pub fn currentPage(self: Notebook) c_int { + switch (self) { + .adw_tab_view => |tab_view| { + if (!build_options.libadwaita) unreachable; + const page = c.adw_tab_view_get_selected_page(tab_view); + return c.adw_tab_view_get_page_position(tab_view, page); + }, + .gtk_notebook => |notebook| return c.gtk_notebook_get_current_page(notebook), + } + } + + pub fn currentTab(self: Notebook) ?*Tab { + const child = switch (self) { + .adw_tab_view => |tab_view| child: { + if (!build_options.libadwaita) unreachable; + const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null; + const child = c.adw_tab_page_get_child(page); + break :child child; + }, + .gtk_notebook => |notebook| child: { + const page = self.currentPage(); + if (page == -1) return null; + log.info("currentPage_page_idx = {}", .{page}); + break :child c.gtk_notebook_get_nth_page(notebook, page); + }, + }; + return @ptrCast(@alignCast( + c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null, + )); + } + + pub fn setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void { + switch (self) { + .adw_tab_view => |tab_view| { + if (!build_options.libadwaita) unreachable; + const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)); + c.adw_tab_page_set_title(page, title.ptr); + }, + .gtk_notebook => c.gtk_label_set_text(tab.label_text, title.ptr), + } + } + + pub fn addTab(self: Notebook, tab: *Tab, title: [:0]const u8) !void { + const box_widget: *c.GtkWidget = @ptrCast(tab.box); + switch (self) { + .adw_tab_view => |tab_view| { + if (!build_options.libadwaita) unreachable; + + const page = c.adw_tab_view_append(tab_view, box_widget); + c.adw_tab_page_set_title(page, title.ptr); + + // Switch to the new tab + c.adw_tab_view_set_selected_page(tab_view, page); + }, + .gtk_notebook => |notebook| { + // Build the tab label + const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0); + const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget)); + const label_text_widget = c.gtk_label_new("Ghostty"); + const label_text: *c.GtkLabel = @ptrCast(label_text_widget); + c.gtk_box_append(label_box, label_text_widget); + tab.label_text = label_text; + + // Build the close button for the tab + const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic"); + const label_close: *c.GtkButton = @ptrCast(label_close_widget); + c.gtk_button_set_has_frame(label_close, 0); + c.gtk_box_append(label_box, label_close_widget); + + const parent_page_idx = self.nPages(); + const page_idx = c.gtk_notebook_insert_page( + notebook, + box_widget, + label_box_widget, + parent_page_idx, + ); + + if (self.nPages() > 1) { + c.gtk_notebook_set_show_tabs(notebook, 1); + } + + // Switch to the new tab + c.gtk_notebook_set_current_page(notebook, page_idx); + } + } + } + + pub fn closeTab(self: Notebook, tab: *Tab) void { + switch (self) { + .adw_tab_view => |tab_view| { + if (!build_options.libadwaita) unreachable; + + const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return; + c.adw_tab_view_close_page(tab_view, page); + + // If we have no more tabs we close the window + if (self.nPages() == 0) + c.gtk_window_destroy(tab.window.window); + }, + .gtk_notebook => |notebook| { + const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; + + // Find page and tab which we're closing + const page_idx = getNotebookPageIndex(page); + + log.info("page_idx = {}", .{page_idx}); + + // Remove the page. This will destroy the GTK widgets in the page which + // will trigger Tab cleanup. + c.gtk_notebook_remove_page(notebook, page_idx); + + const remaining = self.nPages(); + switch (remaining) { + // If we have no more tabs we close the window + 0 => c.gtk_window_destroy(tab.window.window), + + // If we have one more tab we hide the tab bar + 1 => c.gtk_notebook_set_show_tabs(notebook, 0), + + else => {}, + } + + // If we have remaining tabs, we need to make sure we grab focus. + if (remaining > 0) tab.window.focusCurrentTab(); + } + } + } + + pub fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int { + var value: c.GValue = std.mem.zeroes(c.GValue); + defer c.g_value_unset(&value); + _ = c.g_value_init(&value, c.G_TYPE_INT); + c.g_object_get_property( + @ptrCast(@alignCast(page)), + "position", + &value, + ); + + return c.g_value_get_int(&value); + } +}; + +fn gtkPageRemoved( + _: *c.GtkNotebook, + _: *c.GtkWidget, + _: c.guint, + ud: ?*anyopaque, +) callconv(.C) void { + const self = userdataSelf(ud.?); + + const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; + + // Hide the tab bar if we only have one tab after removal + const remaining = c.gtk_notebook_get_n_pages(notebook); + if (remaining == 1) { + c.gtk_notebook_set_show_tabs(notebook, 0); + } +} + +fn adwPageAttached( + tab_view: *AdwTabView, + page: *c.AdwTabPage, + position: c_int, + ud: ?*anyopaque +) callconv(.C) void { + _ = position; _ = tab_view; + const self = userdataSelf(ud.?); + + const child = c.adw_tab_page_get_child(page); + const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return)); + tab.window = self; + + self.focusCurrentTab(); +} + +fn gtkPageAdded( + notebook: *c.GtkNotebook, + _: *c.GtkWidget, + page_idx: c.guint, + ud: ?*anyopaque, +) callconv(.C) void { + const self = userdataSelf(ud.?); + + // The added page can come from another window with drag and drop, thus we migrate the tab + // window to be self. + const page = c.gtk_notebook_get_nth_page(notebook, @intCast(page_idx)); + const tab: *Tab = @ptrCast(@alignCast( + c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return, + )); + tab.window = self; + + // Whenever a new page is added, we always grab focus of the + // currently selected page. This was added specifically so that when + // we drag a tab out to create a new window ("create-window" event) + // we grab focus in the new window. Without this, the terminal didn't + // have focus. + self.focusCurrentTab(); +} + +fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void { + const self = userdataSelf(ud.?); + const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook.gtk_notebook, page))); + const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box))); + const label_text = c.gtk_label_get_text(gtk_label); + c.gtk_window_set_title(self.window, label_text); +} + +fn adwTabViewCreateWindow( + _: *AdwTabView, + ud: ?*anyopaque, +) callconv(.C) ?*AdwTabView { + const currentWindow = userdataSelf(ud.?); + const window = createWindow(currentWindow) catch |err| { + log.warn("error creating new window error={}", .{err}); + return null; + }; + return window.notebook.adw_tab_view; +} + +fn gtkNotebookCreateWindow( + _: *c.GtkNotebook, + page: *c.GtkWidget, + ud: ?*anyopaque, +) callconv(.C) ?*c.GtkNotebook { + // The tab for the page is stored in the widget data. + const tab: *Tab = @ptrCast(@alignCast( + c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null, + )); + + const currentWindow = userdataSelf(ud.?); + const window = createWindow(currentWindow) catch |err| { + log.warn("error creating new window error={}", .{err}); + return null; + }; + + // And add it to the new window. + tab.window = window; + + return window.notebook.gtk_notebook; +} + +fn createWindow(currentWindow: *Window) !*Window { + const alloc = currentWindow.app.core_app.alloc; + const app = currentWindow.app; + + // Create a new window + return Window.create(alloc, app); +} From 9c8a9f3d6be5a45fbd409edd4f56a662e2df8f96 Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Mon, 10 Jun 2024 09:29:39 +0200 Subject: [PATCH 03/16] adw: respect gtk-wide-tabs update --- src/apprt/gtk/Tab.zig | 15 ++++-------- src/apprt/gtk/notebook.zig | 48 +++++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index a58917b07..fce4c0a6b 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -92,6 +92,8 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // Add Surface to the Tab c.gtk_box_append(self.box, surface.primaryWidget()); + // Set the userdata of the box to point to this tab. + c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); try window.notebook.addTab(self, "Ghostty"); // const notebook: *c.GtkNotebook = window.notebook.as_notebook(); @@ -122,18 +124,11 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // c.gtk_notebook_set_show_tabs(notebook, 1); // } - // Set the userdata of the box to point to this tab. - c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); - // // Clicks - // const gesture_tab_click = c.gtk_gesture_click_new(); - // c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0); - // c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click)); + // // // Attach all events - // _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); - // _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), self, null, c.G_CONNECT_DEFAULT); // We need to grab focus after Surface and Tab is added to the window. When // creating a Tab we want to always focus on the widget. @@ -171,7 +166,7 @@ pub fn remove(self: *Tab) void { self.window.closeTab(self); } -fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { +pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { const tab: *Tab = @ptrCast(@alignCast(ud)); const window = tab.window; window.closeTab(tab); @@ -186,7 +181,7 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { tab.destroy(tab.window.app.core_app.alloc); } -fn gtkTabClick( +pub fn gtkTabClick( gesture: *c.GtkGestureClick, _: c.gint, _: c.gdouble, diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 8bb6850dc..8e43c343b 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -26,6 +26,9 @@ pub const Notebook = union(enum) { c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); c.adw_tab_bar_set_view(tab_bar, tab_view.?); + if (window.app.config.@"gtk-wide-tabs") + c.adw_tab_bar_set_expand_tabs(tab_bar, @intCast(1)); + _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); @@ -92,6 +95,7 @@ pub const Notebook = union(enum) { } pub fn currentTab(self: Notebook) ?*Tab { + log.info("self = {}", .{self}); const child = switch (self) { .adw_tab_view => |tab_view| child: { if (!build_options.libadwaita) unreachable; @@ -128,7 +132,7 @@ pub const Notebook = union(enum) { .adw_tab_view => |tab_view| { if (!build_options.libadwaita) unreachable; - const page = c.adw_tab_view_append(tab_view, box_widget); + const page = c.adw_tab_view_append(tab_view, box_widget); c.adw_tab_page_set_title(page, title.ptr); // Switch to the new tab @@ -138,11 +142,29 @@ pub const Notebook = union(enum) { // Build the tab label const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0); const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget)); - const label_text_widget = c.gtk_label_new("Ghostty"); + const label_text_widget = c.gtk_label_new(title.ptr); const label_text: *c.GtkLabel = @ptrCast(label_text_widget); c.gtk_box_append(label_box, label_text_widget); tab.label_text = label_text; + const window = tab.window; + if (window.app.config.@"gtk-wide-tabs") { + c.gtk_widget_set_hexpand(label_box_widget, 1); + c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL); + c.gtk_widget_set_hexpand(label_text_widget, 1); + c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL); + + // This ensures that tabs are always equal width. If they're too + // long, they'll be truncated with an ellipsis. + c.gtk_label_set_max_width_chars(label_text, 1); + c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END); + + // We need to set a minimum width so that at a certain point + // the notebook will have an arrow button rather than shrinking tabs + // to an unreadably small size. + c.gtk_widget_set_size_request(label_text_widget, 100, 1); + } + // Build the close button for the tab const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic"); const label_close: *c.GtkButton = @ptrCast(label_close_widget); @@ -157,13 +179,21 @@ pub const Notebook = union(enum) { parent_page_idx, ); + // Clicks + const gesture_tab_click = c.gtk_gesture_click_new(); + c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0); + c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click)); + + _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT); + if (self.nPages() > 1) { c.gtk_notebook_set_show_tabs(notebook, 1); } // Switch to the new tab c.gtk_notebook_set_current_page(notebook, page_idx); - } + }, } } @@ -204,7 +234,7 @@ pub const Notebook = union(enum) { // If we have remaining tabs, we need to make sure we grab focus. if (remaining > 0) tab.window.focusCurrentTab(); - } + }, } } @@ -239,13 +269,9 @@ fn gtkPageRemoved( } } -fn adwPageAttached( - tab_view: *AdwTabView, - page: *c.AdwTabPage, - position: c_int, - ud: ?*anyopaque -) callconv(.C) void { - _ = position; _ = tab_view; +fn adwPageAttached(tab_view: *AdwTabView, page: *c.AdwTabPage, position: c_int, ud: ?*anyopaque) callconv(.C) void { + _ = position; + _ = tab_view; const self = userdataSelf(ud.?); const child = c.adw_tab_page_get_child(page); From 5327daac0497ef1f41dc129d8fa3b4c5c78fc97c Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Mon, 10 Jun 2024 10:06:34 +0200 Subject: [PATCH 04/16] adw: implement next/previous --- src/apprt/gtk/Window.zig | 37 +++------------------------ src/apprt/gtk/notebook.zig | 52 +++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index c269b51bf..034abf25e 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -211,21 +211,7 @@ pub fn gotoPreviousTab(self: *Window, surface: *Surface) void { log.info("surface is not attached to a tab bar, cannot navigate", .{}); return; }; - - const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; - const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; - const page_idx = Notebook.getNotebookPageIndex(page); - - // The next index is the previous or we wrap around. - const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: { - const max = c.gtk_notebook_get_n_pages(notebook); - break :next_idx max -| 1; - }; - - // Do nothing if we have one tab - if (next_idx == page_idx) return; - - c.gtk_notebook_set_current_page(notebook, next_idx); + self.notebook.gotoPreviousTab(tab); self.focusCurrentTab(); } @@ -235,15 +221,7 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void { log.info("surface is not attached to a tab bar, cannot navigate", .{}); return; }; - - const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; - const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return; - const page_idx = Notebook.getNotebookPageIndex(page); - const max = c.gtk_notebook_get_n_pages(notebook) -| 1; - const next_idx = if (page_idx < max) page_idx + 1 else 0; - if (next_idx == page_idx) return; - - c.gtk_notebook_set_current_page(notebook, next_idx); + self.notebook.gotoNextTab(tab); self.focusCurrentTab(); } @@ -257,11 +235,10 @@ pub fn gotoLastTab(self: *Window) void { /// Go to the specific tab index. pub fn gotoTab(self: *Window, n: usize) void { if (n == 0) return; - const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; - const max = c.gtk_notebook_get_n_pages(notebook); + const max = self.notebook.nPages(); const page_idx = std.math.cast(c_int, n - 1) orelse return; if (page_idx < max) { - c.gtk_notebook_set_current_page(notebook, page_idx); + self.notebook.gotoNthTab(page_idx); self.focusCurrentTab(); } } @@ -523,12 +500,6 @@ fn gtkActionReset( /// Returns the surface to use for an action. fn actionSurface(self: *Window) ?*CoreSurface { const tab = self.notebook.currentTab() orelse return null; - // const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; - // const page_idx = c.gtk_notebook_get_current_page(notebook); - // const page = c.gtk_notebook_get_nth_page(notebook, page_idx); - // const tab: *Tab = @ptrCast(@alignCast( - // c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null, - // )); return &tab.focus_child.core_surface; } diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 8e43c343b..5e528a3c0 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -24,7 +24,7 @@ pub const Notebook = union(enum) { const tab_view = c.adw_tab_view_new(); const tab_bar = c.adw_tab_bar_new(); c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); - c.adw_tab_bar_set_view(tab_bar, tab_view.?); + c.adw_tab_bar_set_view(tab_bar, tab_view); if (window.app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, @intCast(1)); @@ -115,6 +115,56 @@ pub const Notebook = union(enum) { )); } + pub fn gotoNthTab(self: Notebook, position: c_int) void { + switch (self) { + .adw_tab_view => |tab_view| { + if (!build_options.libadwaita) unreachable; + const page_to_select = c.adw_tab_view_get_nth_page(tab_view, position); + c.adw_tab_view_set_selected_page(tab_view, page_to_select); + }, + .gtk_notebook => |notebook| c.gtk_notebook_set_current_page(notebook, position), + } + } + + pub fn getTabPosition(self: Notebook, tab: *Tab) ?c_int { + return switch (self) { + .adw_tab_view => |tab_view| page_idx: { + if (!build_options.libadwaita) unreachable; + const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return null; + break :page_idx c.adw_tab_view_get_page_position(tab_view, page); + }, + .gtk_notebook => |notebook| page_idx: { + const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return null; + break :page_idx getNotebookPageIndex(page); + }, + }; + } + + pub fn gotoPreviousTab(self: Notebook, tab: *Tab) void { + const page_idx = self.getTabPosition(tab) orelse return; + + // The next index is the previous or we wrap around. + const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: { + const max = self.nPages(); + break :next_idx max -| 1; + }; + + // Do nothing if we have one tab + if (next_idx == page_idx) return; + + self.gotoNthTab(next_idx); + } + + pub fn gotoNextTab(self: Notebook, tab: *Tab) void { + const page_idx = self.getTabPosition(tab) orelse return; + + const max = self.nPages() -| 1; + const next_idx = if (page_idx < max) page_idx + 1 else 0; + if (next_idx == page_idx) return; + + self.gotoNthTab(next_idx); + } + pub fn setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void { switch (self) { .adw_tab_view => |tab_view| { From 439988930dba814d87c7a7f3c48ec65b81d9f4fb Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Mon, 10 Jun 2024 17:43:23 +0200 Subject: [PATCH 05/16] gtk: respect wide-tabs option --- src/apprt/gtk/notebook.zig | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 5e528a3c0..4db1a3e5e 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -26,8 +26,8 @@ pub const Notebook = union(enum) { c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); c.adw_tab_bar_set_view(tab_bar, tab_view); - if (window.app.config.@"gtk-wide-tabs") - c.adw_tab_bar_set_expand_tabs(tab_bar, @intCast(1)); + if (!window.app.config.@"gtk-wide-tabs") + c.adw_tab_bar_set_expand_tabs(tab_bar, 0); _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); @@ -248,6 +248,7 @@ pub const Notebook = union(enum) { } pub fn closeTab(self: Notebook, tab: *Tab) void { + const window = tab.window; switch (self) { .adw_tab_view => |tab_view| { if (!build_options.libadwaita) unreachable; @@ -265,10 +266,8 @@ pub const Notebook = union(enum) { // Find page and tab which we're closing const page_idx = getNotebookPageIndex(page); - log.info("page_idx = {}", .{page_idx}); - // Remove the page. This will destroy the GTK widgets in the page which - // will trigger Tab cleanup. + // will trigger Tab cleanup. The `tab` variable is therefore unusable past that point. c.gtk_notebook_remove_page(notebook, page_idx); const remaining = self.nPages(); @@ -283,7 +282,7 @@ pub const Notebook = union(enum) { } // If we have remaining tabs, we need to make sure we grab focus. - if (remaining > 0) tab.window.focusCurrentTab(); + if (remaining > 0) window.focusCurrentTab(); }, } } From 05e7bf7634b9ed7fceeabf0f787f3c20942d9003 Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Mon, 10 Jun 2024 17:43:33 +0200 Subject: [PATCH 06/16] gtk: add devel class to window in debug builds --- src/apprt/gtk/Window.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 034abf25e..8ff113a92 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -113,13 +113,14 @@ pub fn init(self: *Window, app: *App) !void { // Create our box which will hold our widgets. const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); - // In debug we show a warning. This is a really common issue where - // people build from source in debug and performance is really bad. + // In debug we show a warning and apply the 'devel' class to the window. + // This is a really common issue where people build from source in debug and performance is really bad. if (comptime std.debug.runtime_safety) { const warning = c.gtk_label_new("⚠️ You're running a debug build of Ghostty! Performance will be degraded."); c.gtk_widget_set_margin_top(warning, 10); c.gtk_widget_set_margin_bottom(warning, 10); c.gtk_box_append(@ptrCast(box), warning); + c.gtk_widget_add_css_class(@ptrCast(gtk_window), "devel"); } self.notebook = Notebook.create(self, box); From 618a7a3feafe8f71acef847c94102580409155ed Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Wed, 12 Jun 2024 10:07:14 +0200 Subject: [PATCH 07/16] adw: update window title on selected page change --- src/apprt/gtk/notebook.zig | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 4db1a3e5e..dfefe3e99 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -31,6 +31,7 @@ pub const Notebook = union(enum) { _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT); return .{ .adw_tab_view = tab_view.? }; } @@ -354,12 +355,19 @@ fn gtkPageAdded( self.focusCurrentTab(); } +fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void { + const window = userdataSelf(ud.?); + const page = c.adw_tab_view_get_selected_page(window.notebook.adw_tab_view); + const title = c.adw_tab_page_get_title(page); + c.gtk_window_set_title(window.window, title); +} + fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void { - const self = userdataSelf(ud.?); - const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook.gtk_notebook, page))); + const window = userdataSelf(ud.?); + const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(window.notebook.gtk_notebook, page))); const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box))); const label_text = c.gtk_label_get_text(gtk_label); - c.gtk_window_set_title(self.window, label_text); + c.gtk_window_set_title(window.window, label_text); } fn adwTabViewCreateWindow( From bdf618d7af7d58d71b12061aa083cb532cc035b5 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 12 Jun 2024 20:43:41 +0200 Subject: [PATCH 08/16] gtk: adw toolbar view gate toolbar view on ADWAITA_MINOR >= 4 use the right destructor on window creation errors --- src/apprt/gtk/Tab.zig | 7 ++----- src/apprt/gtk/Window.zig | 36 ++++++++++++++++++++++++++++++------ src/apprt/gtk/notebook.zig | 16 +++++++++------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index fce4c0a6b..aa0c36a5b 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -124,10 +124,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // c.gtk_notebook_set_show_tabs(notebook, 1); // } - - // - - // // Attach all events + // Attach all events _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); // We need to grab focus after Surface and Tab is added to the window. When @@ -174,7 +171,7 @@ pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { _ = v; - log.debug("tab box destroy", .{}); + log.info("tab box destroy", .{}); // When our box is destroyed, we want to destroy our tab, too. const tab: *Tab = @ptrCast(@alignCast(ud)); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 8ff113a92..e80e68220 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -59,10 +59,17 @@ pub fn init(self: *Window, app: *App) !void { .context_menu = undefined, }; + const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita"; + // Create the window - const window = c.gtk_application_window_new(app.app); + const adw_window = adwaita and app.config.@"gtk-titlebar" and c.ADW_MINOR_VERSION >= 4; + const window: *c.GtkWidget = if (adw_window) + c.adw_application_window_new(app.app) + else + c.gtk_application_window_new(app.app); + const gtk_window: *c.GtkWindow = @ptrCast(window); - errdefer c.gtk_window_destroy(gtk_window); + errdefer if (adw_window) c.adw_application_window_destroy(window) else c.gtk_application_window_destroy(gtk_window); self.window = gtk_window; c.gtk_window_set_title(gtk_window, "Ghostty"); c.gtk_window_set_default_size(gtk_window, 1000, 600); @@ -78,6 +85,8 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_widget_set_opacity(@ptrCast(window), app.config.@"background-opacity"); } + var header: ?*c.GtkHeaderBar = null; + // Internally, GTK ensures that only one instance of this provider exists in the provider list // for the display. const display = c.gdk_display_get_default(); @@ -88,8 +97,7 @@ pub fn init(self: *Window, app: *App) !void { // are decorated or not because we can have a keybind to toggle the // decorations. if (app.config.@"gtk-titlebar") { - const header = c.gtk_header_bar_new(); - c.gtk_window_set_titlebar(gtk_window, header); + header = @ptrCast(c.gtk_header_bar_new()); { const btn = c.gtk_menu_button_new(); c.gtk_widget_set_tooltip_text(btn, "Main Menu"); @@ -142,8 +150,24 @@ pub fn init(self: *Window, app: *App) !void { // Our actions for the menu initActions(self); - // The box is our main child - c.gtk_window_set_child(gtk_window, box); + if (build_options.libadwaita and app.config.@"gtk-adwaita" and app.config.@"gtk-titlebar" and header != null and c.ADW_MINOR_VERSION >= 4) { + const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); + c.adw_toolbar_view_add_top_bar(toolbar_view, @ptrCast(@alignCast(header.?))); + + const tab_bar = c.adw_tab_bar_new(); + c.adw_tab_bar_set_view(tab_bar, self.notebook.adw_tab_view); + + if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); + + c.adw_toolbar_view_add_top_bar(toolbar_view, @ptrCast(@alignCast(tab_bar))); + c.adw_toolbar_view_set_content(toolbar_view, box); + + c.adw_application_window_set_content(@ptrCast(gtk_window), @ptrCast(@alignCast(toolbar_view))); + } else { + // The box is our main child + c.gtk_window_set_child(gtk_window, box); + if (header) |h| c.gtk_window_set_titlebar(gtk_window, @ptrCast(@alignCast(h))); + } // Show the window c.gtk_widget_show(window); diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index dfefe3e99..5174d7399 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -20,14 +20,16 @@ pub const Notebook = union(enum) { const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita"; if (adwaita) { - log.warn("using adwaita", .{}); const tab_view = c.adw_tab_view_new(); - const tab_bar = c.adw_tab_bar_new(); - c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); - c.adw_tab_bar_set_view(tab_bar, tab_view); - if (!window.app.config.@"gtk-wide-tabs") - c.adw_tab_bar_set_expand_tabs(tab_bar, 0); + if (!window.app.config.@"gtk-titlebar" or c.ADW_MINOR_VERSION < 4) { + const tab_bar = c.adw_tab_bar_new(); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); + c.adw_tab_bar_set_view(tab_bar, tab_view); + + if (!window.app.config.@"gtk-wide-tabs") + c.adw_tab_bar_set_expand_tabs(tab_bar, 0); + } _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); @@ -357,7 +359,7 @@ fn gtkPageAdded( fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void { const window = userdataSelf(ud.?); - const page = c.adw_tab_view_get_selected_page(window.notebook.adw_tab_view); + const page = c.adw_tab_view_get_selected_page(window.notebook.adw_tab_view) orelse return; const title = c.adw_tab_page_get_title(page); c.gtk_window_set_title(window.window, title); } From 0c286a049a03058ff847dd5a7371c6134c3f5019 Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Tue, 20 Aug 2024 11:46:04 +0200 Subject: [PATCH 09/16] gtk: add adwaita banner --- src/apprt/gtk/Surface.zig | 2 +- src/apprt/gtk/Tab.zig | 48 +------------------------------------- src/apprt/gtk/Window.zig | 19 +++++++++------ src/apprt/gtk/notebook.zig | 17 +++++++------- 4 files changed, 22 insertions(+), 64 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 6d2b93aef..eb317e640 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -894,7 +894,7 @@ fn updateTitleLabels(self: *Surface) void { // If we have a tab and are the focused child, then we have to update the tab if (self.container.tab()) |tab| { - if (tab.focus_child == self) tab.setLabelText(title); // c.gtk_label_set_text(tab.label_text, title.ptr); + if (tab.focus_child == self) tab.setLabelText(title); } // If we have a window and are focused, then we have to update the window title. diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index aa0c36a5b..3595bb977 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -55,24 +55,6 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { .focus_child = undefined, }; - // // Wide style GTK tabs - // if (window.app.config.@"gtk-wide-tabs") { - // c.gtk_widget_set_hexpand(label_box_widget, 1); - // c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL); - // c.gtk_widget_set_hexpand(label_text_widget, 1); - // c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL); - - // // This ensures that tabs are always equal width. If they're too - // // long, they'll be truncated with an ellipsis. - // c.gtk_label_set_max_width_chars(label_text, 1); - // c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END); - - // // We need to set a minimum width so that at a certain point - // // the notebook will have an arrow button rather than shrinking tabs - // // to an unreadably small size. - // c.gtk_widget_set_size_request(label_text_widget, 100, 1); - // } - // Create a Box in which we'll later keep either Surface or Split. // Using a box makes it easier to maintain the tab contents because // we never need to change the root widget of the notebook page (tab). @@ -96,34 +78,6 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); try window.notebook.addTab(self, "Ghostty"); - // const notebook: *c.GtkNotebook = window.notebook.as_notebook(); - - // // Add the notebook page (create tab). - // const parent_page_idx = switch (window.app.config.@"window-new-tab-position") { - // .current => c.gtk_notebook_get_current_page(notebook) + 1, - // .end => c.gtk_notebook_get_n_pages(notebook), - // }; - - // const page_idx = c.gtk_notebook_insert_page( - // notebook, - // box_widget, - // label_box_widget, - // parent_page_idx, - // ); - // if (page_idx < 0) { - // log.warn("failed to add page to notebook", .{}); - // return error.GtkAppendPageFailed; - // } - - // // Tab settings - // c.gtk_notebook_set_tab_reorderable(notebook, box_widget, 1); - // c.gtk_notebook_set_tab_detachable(notebook, box_widget, 1); - - // // If we have multiple tabs, show the tab bar. - // if (c.gtk_notebook_get_n_pages(notebook) > 1) { - // c.gtk_notebook_set_show_tabs(notebook, 1); - // } - // Attach all events _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); @@ -171,7 +125,7 @@ pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { _ = v; - log.info("tab box destroy", .{}); + log.debug("tab box destroy", .{}); // When our box is destroyed, we want to destroy our tab, too. const tab: *Tab = @ptrCast(@alignCast(ud)); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index e80e68220..3e4c9bbda 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -124,10 +124,16 @@ pub fn init(self: *Window, app: *App) !void { // In debug we show a warning and apply the 'devel' class to the window. // This is a really common issue where people build from source in debug and performance is really bad. if (comptime std.debug.runtime_safety) { - const warning = c.gtk_label_new("⚠️ You're running a debug build of Ghostty! Performance will be degraded."); - c.gtk_widget_set_margin_top(warning, 10); - c.gtk_widget_set_margin_bottom(warning, 10); - c.gtk_box_append(@ptrCast(box), warning); + const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded."; + if (adwaita and c.ADW_MINOR_VERSION >= 3) { + const banner = c.adw_banner_new(warning_text); + c.gtk_box_append(@ptrCast(box), @ptrCast(banner)); + } else { + const warning = c.gtk_label_new(warning_text); + c.gtk_widget_set_margin_top(warning, 10); + c.gtk_widget_set_margin_bottom(warning, 10); + c.gtk_box_append(@ptrCast(box), warning); + } c.gtk_widget_add_css_class(@ptrCast(gtk_window), "devel"); } @@ -253,8 +259,7 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void { /// Go to the next tab for a surface. pub fn gotoLastTab(self: *Window) void { const max = self.notebook.nPages() -| 1; - c.gtk_notebook_set_current_page(self.notebook.gtk_notebook, max); - self.focusCurrentTab(); + self.gotoTab(@intCast(max)); } /// Go to the specific tab index. @@ -528,6 +533,6 @@ fn actionSurface(self: *Window) ?*CoreSurface { return &tab.focus_child.core_surface; } -pub fn userdataSelf(ud: *anyopaque) *Window { +fn userdataSelf(ud: *anyopaque) *Window { return @ptrCast(@alignCast(ud)); } diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 5174d7399..bbfaa95e9 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -3,7 +3,6 @@ const c = @import("c.zig").c; const build_options = @import("build_options"); const Window = @import("./Window.zig"); -const userdataSelf = Window.userdataSelf; const Tab = @import("./Tab.zig"); const log = std.log.scoped(.gtk); @@ -24,7 +23,7 @@ pub const Notebook = union(enum) { if (!window.app.config.@"gtk-titlebar" or c.ADW_MINOR_VERSION < 4) { const tab_bar = c.adw_tab_bar_new(); - c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); + c.gtk_box_prepend(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); c.adw_tab_bar_set_view(tab_bar, tab_view); if (!window.app.config.@"gtk-wide-tabs") @@ -310,7 +309,7 @@ fn gtkPageRemoved( _: c.guint, ud: ?*anyopaque, ) callconv(.C) void { - const self = userdataSelf(ud.?); + const self: *Window = @ptrCast(@alignCast(ud.?)); const notebook: *c.GtkNotebook = self.notebook.gtk_notebook; @@ -324,7 +323,7 @@ fn gtkPageRemoved( fn adwPageAttached(tab_view: *AdwTabView, page: *c.AdwTabPage, position: c_int, ud: ?*anyopaque) callconv(.C) void { _ = position; _ = tab_view; - const self = userdataSelf(ud.?); + const self: *Window = @ptrCast(@alignCast(ud.?)); const child = c.adw_tab_page_get_child(page); const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return)); @@ -339,7 +338,7 @@ fn gtkPageAdded( page_idx: c.guint, ud: ?*anyopaque, ) callconv(.C) void { - const self = userdataSelf(ud.?); + const self: *Window = @ptrCast(@alignCast(ud.?)); // The added page can come from another window with drag and drop, thus we migrate the tab // window to be self. @@ -358,14 +357,14 @@ fn gtkPageAdded( } fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void { - const window = userdataSelf(ud.?); + const window: *Window = @ptrCast(@alignCast(ud.?)); const page = c.adw_tab_view_get_selected_page(window.notebook.adw_tab_view) orelse return; const title = c.adw_tab_page_get_title(page); c.gtk_window_set_title(window.window, title); } fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void { - const window = userdataSelf(ud.?); + const window: *Window = @ptrCast(@alignCast(ud.?)); const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(window.notebook.gtk_notebook, page))); const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box))); const label_text = c.gtk_label_get_text(gtk_label); @@ -376,7 +375,7 @@ fn adwTabViewCreateWindow( _: *AdwTabView, ud: ?*anyopaque, ) callconv(.C) ?*AdwTabView { - const currentWindow = userdataSelf(ud.?); + const currentWindow: *Window = @ptrCast(@alignCast(ud.?)); const window = createWindow(currentWindow) catch |err| { log.warn("error creating new window error={}", .{err}); return null; @@ -394,7 +393,7 @@ fn gtkNotebookCreateWindow( c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null, )); - const currentWindow = userdataSelf(ud.?); + const currentWindow: *Window = @ptrCast(@alignCast(ud.?)); const window = createWindow(currentWindow) catch |err| { log.warn("error creating new window error={}", .{err}); return null; From 7aa6b0008a6af4575b976881d0d886d600a9b417 Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Wed, 11 Sep 2024 14:07:01 +0200 Subject: [PATCH 10/16] adw: add support for gtk-tabs-location = bottom this falls back to top when using either right or left. --- src/apprt/gtk/Window.zig | 13 +++++++++---- src/apprt/gtk/notebook.zig | 18 +++++++++++------- src/config/Config.zig | 3 +++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 3e4c9bbda..3e6ecc1fd 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -138,7 +138,6 @@ pub fn init(self: *Window, app: *App) !void { } self.notebook = Notebook.create(self, box); - c.gtk_box_append(@ptrCast(box), self.notebook.as_widget()); self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu))); c.gtk_widget_set_parent(self.context_menu, window); @@ -158,14 +157,20 @@ pub fn init(self: *Window, app: *App) !void { if (build_options.libadwaita and app.config.@"gtk-adwaita" and app.config.@"gtk-titlebar" and header != null and c.ADW_MINOR_VERSION >= 4) { const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); - c.adw_toolbar_view_add_top_bar(toolbar_view, @ptrCast(@alignCast(header.?))); + const header_widget: *c.GtkWidget = @ptrCast(@alignCast(header.?)); + c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget); const tab_bar = c.adw_tab_bar_new(); c.adw_tab_bar_set_view(tab_bar, self.notebook.adw_tab_view); if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); - c.adw_toolbar_view_add_top_bar(toolbar_view, @ptrCast(@alignCast(tab_bar))); + const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar)); + switch (self.app.config.@"gtk-tabs-location") { + // left and right is not supported in libadwaita. + .top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget), + .bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget), + } c.adw_toolbar_view_set_content(toolbar_view, box); c.adw_application_window_set_content(@ptrCast(gtk_window), @ptrCast(@alignCast(toolbar_view))); @@ -258,7 +263,7 @@ pub fn gotoNextTab(self: *Window, surface: *Surface) void { /// Go to the next tab for a surface. pub fn gotoLastTab(self: *Window) void { - const max = self.notebook.nPages() -| 1; + const max = self.notebook.nPages() -| 1; self.gotoTab(@intCast(max)); } diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index bbfaa95e9..5b79e3b20 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -19,11 +19,17 @@ pub const Notebook = union(enum) { const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita"; if (adwaita) { - const tab_view = c.adw_tab_view_new(); + const tab_view = c.adw_tab_view_new().?; + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_view))); if (!window.app.config.@"gtk-titlebar" or c.ADW_MINOR_VERSION < 4) { const tab_bar = c.adw_tab_bar_new(); - c.gtk_box_prepend(@ptrCast(box), @ptrCast(@alignCast(tab_bar))); + + switch (app.config.@"gtk-tabs-location") { + // left and right is not supported in libadwaita. + .top, .left, .right => c.gtk_box_prepend(@ptrCast(box), @ptrCast(@alignCast(tab_bar))), + .bottom => c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))), + } c.adw_tab_bar_set_view(tab_bar, tab_view); if (!window.app.config.@"gtk-wide-tabs") @@ -34,7 +40,7 @@ pub const Notebook = union(enum) { _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT); - return .{ .adw_tab_view = tab_view.? }; + return .{ .adw_tab_view = tab_view }; } // Create a notebook to hold our tabs. @@ -59,19 +65,17 @@ pub const Notebook = union(enum) { c.gtk_widget_set_vexpand(notebook_widget, 1); c.gtk_widget_set_hexpand(notebook_widget, 1); - // If we are in fullscreen mode, new windows start fullscreen. - if (app.config.fullscreen) c.gtk_window_fullscreen(window.window); - // All of our events _ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT); + c.gtk_box_append(@ptrCast(box), notebook_widget); return .{ .gtk_notebook = notebook }; } - pub fn as_widget(self: Notebook) *c.GtkWidget { + pub fn asWidget(self: Notebook) *c.GtkWidget { return switch (self) { .adw_tab_view => |ptr| @ptrCast(@alignCast(ptr)), .gtk_notebook => |ptr| @ptrCast(@alignCast(ptr)), diff --git a/src/config/Config.zig b/src/config/Config.zig index 1f9a78f31..e2f3d0ba2 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1421,6 +1421,9 @@ keybind: Keybinds = .{}, /// Determines the side of the screen that the GTK tab bar will stick to. /// Top, bottom, left, and right are supported. The default is top. +/// +/// If this option has value `left` or `right` when using `libadwaita`, it falls +/// back to `top`. @"gtk-tabs-location": GtkTabsLocation = .top, /// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs From 1ab850fa948bc146be72dfa1e247feef12792189 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Sep 2024 09:21:39 -0700 Subject: [PATCH 11/16] apprt/gtk: move adw enabling and version checks into shared file --- src/apprt/gtk/Window.zig | 20 ++++++++++++----- src/apprt/gtk/adwaita.zig | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 src/apprt/gtk/adwaita.zig diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 3e6ecc1fd..ed489a98d 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -21,6 +21,7 @@ const Color = configpkg.Config.Color; const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); const c = @import("c.zig").c; +const adwaita = @import("adwaita.zig"); const Notebook = @import("./notebook.zig").Notebook; const log = std.log.scoped(.gtk); @@ -59,10 +60,11 @@ pub fn init(self: *Window, app: *App) !void { .context_menu = undefined, }; - const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita"; - // Create the window - const adw_window = adwaita and app.config.@"gtk-titlebar" and c.ADW_MINOR_VERSION >= 4; + const adw_window = adwaita.enabled(&app.config) and + app.config.@"gtk-titlebar" and + comptime adwaita.versionAtLeast(1, 4, 0) and + adwaita.versionAtLeast(1, 4, 0); const window: *c.GtkWidget = if (adw_window) c.adw_application_window_new(app.app) else @@ -125,7 +127,10 @@ pub fn init(self: *Window, app: *App) !void { // This is a really common issue where people build from source in debug and performance is really bad. if (comptime std.debug.runtime_safety) { const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded."; - if (adwaita and c.ADW_MINOR_VERSION >= 3) { + if (adwaita.enabled(&app.config) and + comptime adwaita.versionAtLeast(1, 3, 0) and + adwaita.versionAtLeast(1, 3, 0)) + { const banner = c.adw_banner_new(warning_text); c.gtk_box_append(@ptrCast(box), @ptrCast(banner)); } else { @@ -155,7 +160,12 @@ pub fn init(self: *Window, app: *App) !void { // Our actions for the menu initActions(self); - if (build_options.libadwaita and app.config.@"gtk-adwaita" and app.config.@"gtk-titlebar" and header != null and c.ADW_MINOR_VERSION >= 4) { + if (adwaita.enabled(&app.config) and + app.config.@"gtk-titlebar" and + header != null and + comptime adwaita.versionAtLeast(1, 4, 0) and + adwaita.versionAtLeast(1, 4, 0)) + { const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); const header_widget: *c.GtkWidget = @ptrCast(@alignCast(header.?)); diff --git a/src/apprt/gtk/adwaita.zig b/src/apprt/gtk/adwaita.zig new file mode 100644 index 000000000..c1efd34e3 --- /dev/null +++ b/src/apprt/gtk/adwaita.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const c = @import("c.zig").c; +const build_options = @import("build_options"); +const Config = @import("../../config.zig").Config; + +/// Returns true if Ghostty is configured to build with libadwaita and +/// the configuration has enabled adwaita. +pub fn enabled(config: *const Config) bool { + return build_options.libadwaita and + config.@"gtk-adwaita"; +} + +/// Verifies that the running libadwaita version is at least the given +/// version. This will return false if Ghostty is configured to +/// not build with libadwaita. +/// +/// This can be run in both a comptime and runtime context. If it +/// is run in a comptime context, it will only check the version +/// in the headers. If it is run in a runtime context, it will +/// check the actual version of the library we are linked against. +/// So generally you probably want to do both checks! +pub fn versionAtLeast( + comptime major: u16, + comptime minor: u16, + comptime micro: u16, +) bool { + if (comptime !build_options.libadwaita) return false; + + // If our header has lower versions than the given version, + // we can return false immediately. This prevents us from + // compiling against unknown symbols and makes runtime checks + // very slightly faster. + if (comptime c.ADW_MAJOR_VERSION < major or + c.ADW_MINOR_VERSION < minor or + c.ADW_MICRO_VERSION < micro) return false; + + // If we're in comptime then we can't check the runtime version. + if (@inComptime()) return true; + + // We use the functions instead of the constants such as + // c.ADW_MINOR_VERSION because the function gets the actual + // runtime version. + return c.adw_get_major_version() >= major and + c.adw_get_minor_version() >= minor and + c.adw_get_micro_version() >= micro; +} From afc95fefe28f204036122cc8b3ba356d35b7e978 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Sep 2024 09:37:49 -0700 Subject: [PATCH 12/16] apprt/gtk: use adwaita helpers, move out some complicated logic --- src/apprt/gtk/App.zig | 17 +++--- src/apprt/gtk/Window.zig | 20 +++---- src/apprt/gtk/adwaita.zig | 5 ++ src/apprt/gtk/notebook.zig | 104 +++++++++++++++++++++---------------- 4 files changed, 83 insertions(+), 63 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 8c4e41111..79e8846e2 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -21,8 +21,7 @@ const Config = configpkg.Config; const CoreApp = @import("../../App.zig"); const CoreSurface = @import("../../Surface.zig"); -const build_options = @import("build_options"); - +const adwaita = @import("adwaita.zig"); const cgroup = @import("cgroup.zig"); const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); @@ -143,8 +142,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // Create our GTK Application which encapsulates our process. const app: *c.GtkApplication = app: { - const adwaita = build_options.libadwaita and config.@"gtk-adwaita"; - log.debug("creating GTK application id={s} single-instance={} adwaita={}", .{ app_id, single_instance, @@ -152,10 +149,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { }); // If not libadwaita, create a standard GTK application. - if (!adwaita) break :app @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new( - app_id.ptr, - app_flags, - ))) orelse return error.GtkInitFailed; + if ((comptime adwaita.comptimeEnabled()) and + !adwaita.enabled(&config)) + { + break :app @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new( + app_id.ptr, + app_flags, + ))) orelse return error.GtkInitFailed; + } // Use libadwaita if requested. Using an AdwApplication lets us use // Adwaita widgets and access things such as the color scheme. diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index ed489a98d..0d5a2b27b 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -8,7 +8,6 @@ const Window = @This(); const std = @import("std"); const builtin = @import("builtin"); const build_config = @import("../../build_config.zig"); -const build_options = @import("build_options"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const configpkg = @import("../../config.zig"); @@ -61,9 +60,10 @@ pub fn init(self: *Window, app: *App) !void { }; // Create the window - const adw_window = adwaita.enabled(&app.config) and + const adw_window = (comptime adwaita.comptimeEnabled()) and + adwaita.enabled(&app.config) and app.config.@"gtk-titlebar" and - comptime adwaita.versionAtLeast(1, 4, 0) and + (comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0); const window: *c.GtkWidget = if (adw_window) c.adw_application_window_new(app.app) @@ -127,8 +127,9 @@ pub fn init(self: *Window, app: *App) !void { // This is a really common issue where people build from source in debug and performance is really bad. if (comptime std.debug.runtime_safety) { const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded."; - if (adwaita.enabled(&app.config) and - comptime adwaita.versionAtLeast(1, 3, 0) and + if ((comptime adwaita.comptimeEnabled()) and + (comptime adwaita.versionAtLeast(1, 3, 0)) and + adwaita.enabled(&app.config) and adwaita.versionAtLeast(1, 3, 0)) { const banner = c.adw_banner_new(warning_text); @@ -160,11 +161,12 @@ pub fn init(self: *Window, app: *App) !void { // Our actions for the menu initActions(self); - if (adwaita.enabled(&app.config) and + if ((comptime adwaita.comptimeEnabled()) and + (comptime adwaita.versionAtLeast(1, 4, 0)) and + adwaita.enabled(&app.config) and + adwaita.versionAtLeast(1, 4, 0) and app.config.@"gtk-titlebar" and - header != null and - comptime adwaita.versionAtLeast(1, 4, 0) and - adwaita.versionAtLeast(1, 4, 0)) + header != null) { const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); diff --git a/src/apprt/gtk/adwaita.zig b/src/apprt/gtk/adwaita.zig index c1efd34e3..db509db3a 100644 --- a/src/apprt/gtk/adwaita.zig +++ b/src/apprt/gtk/adwaita.zig @@ -3,6 +3,11 @@ const c = @import("c.zig").c; const build_options = @import("build_options"); const Config = @import("../../config.zig").Config; +/// Returns true if Ghostty is configured to build with libadwaita. +pub fn comptimeEnabled() bool { + return build_options.libadwaita; +} + /// Returns true if Ghostty is configured to build with libadwaita and /// the configuration has enabled adwaita. pub fn enabled(config: *const Config) bool { diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 5b79e3b20..a3d672f24 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -1,50 +1,32 @@ const std = @import("std"); +const assert = std.debug.assert; const c = @import("c.zig").c; -const build_options = @import("build_options"); -const Window = @import("./Window.zig"); -const Tab = @import("./Tab.zig"); +const Window = @import("Window.zig"); +const Tab = @import("Tab.zig"); +const adwaita = @import("adwaita.zig"); const log = std.log.scoped(.gtk); -const AdwTabView = if (build_options.libadwaita) c.AdwTabView else anyopaque; +const AdwTabView = if (adwaita.comptimeEnabled()) c.AdwTabView else anyopaque; +/// An abstraction over the GTK notebook and Adwaita tab view to manage +/// all the terminal tabs in a window. pub const Notebook = union(enum) { adw_tab_view: *AdwTabView, gtk_notebook: *c.GtkNotebook, pub fn create(window: *Window, box: *c.GtkWidget) @This() { const app = window.app; + if (adwaita.enabled(&app.config)) return initAdw(window, box); + return initGtk(window, box); + } - const adwaita = build_options.libadwaita and app.config.@"gtk-adwaita"; - - if (adwaita) { - const tab_view = c.adw_tab_view_new().?; - - c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_view))); - if (!window.app.config.@"gtk-titlebar" or c.ADW_MINOR_VERSION < 4) { - const tab_bar = c.adw_tab_bar_new(); - - switch (app.config.@"gtk-tabs-location") { - // left and right is not supported in libadwaita. - .top, .left, .right => c.gtk_box_prepend(@ptrCast(box), @ptrCast(@alignCast(tab_bar))), - .bottom => c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))), - } - c.adw_tab_bar_set_view(tab_bar, tab_view); - - if (!window.app.config.@"gtk-wide-tabs") - c.adw_tab_bar_set_expand_tabs(tab_bar, 0); - } - - _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT); - - return .{ .adw_tab_view = tab_view }; - } + fn initGtk(window: *Window, box: *c.GtkWidget) Notebook { + const app = window.app; // Create a notebook to hold our tabs. - const notebook_widget = c.gtk_notebook_new(); + const notebook_widget: *c.GtkWidget = c.gtk_notebook_new(); const notebook: *c.GtkNotebook = @ptrCast(notebook_widget); const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") { .top => c.GTK_POS_TOP, @@ -75,44 +57,74 @@ pub const Notebook = union(enum) { return .{ .gtk_notebook = notebook }; } - pub fn asWidget(self: Notebook) *c.GtkWidget { - return switch (self) { - .adw_tab_view => |ptr| @ptrCast(@alignCast(ptr)), - .gtk_notebook => |ptr| @ptrCast(@alignCast(ptr)), - }; + fn initAdw(window: *Window, box: *c.GtkWidget) Notebook { + const app = window.app; + assert(adwaita.enabled(&app.config)); + + const tab_view: *c.AdwTabView = c.adw_tab_view_new().?; + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_view))); + if (comptime !adwaita.versionAtLeast(1, 4, 0) or + !adwaita.versionAtLeast(1, 4, 0) or + !app.config.@"gtk-titlebar") + { + const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?; + switch (app.config.@"gtk-tabs-location") { + // left and right is not supported in libadwaita. + .top, + .left, + .right, + => c.gtk_box_prepend(@ptrCast(box), @ptrCast(@alignCast(tab_bar))), + + .bottom => c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_bar))), + } + c.adw_tab_bar_set_view(tab_bar, tab_view); + + if (!app.config.@"gtk-wide-tabs") { + c.adw_tab_bar_set_expand_tabs(tab_bar, 0); + } + } + + _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT); + + return .{ .adw_tab_view = tab_view }; } pub fn nPages(self: Notebook) c_int { return switch (self) { - .adw_tab_view => |tab_view| if (build_options.libadwaita) c.adw_tab_view_get_n_pages(tab_view) else unreachable, .gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook), + .adw_tab_view => |tab_view| if (comptime adwaita.comptimeEnabled()) + c.adw_tab_view_get_n_pages(tab_view) + else + unreachable, }; } pub fn currentPage(self: Notebook) c_int { switch (self) { .adw_tab_view => |tab_view| { - if (!build_options.libadwaita) unreachable; + if (comptime !adwaita.comptimeEnabled()) unreachable; const page = c.adw_tab_view_get_selected_page(tab_view); return c.adw_tab_view_get_page_position(tab_view, page); }, + .gtk_notebook => |notebook| return c.gtk_notebook_get_current_page(notebook), } } pub fn currentTab(self: Notebook) ?*Tab { - log.info("self = {}", .{self}); const child = switch (self) { .adw_tab_view => |tab_view| child: { - if (!build_options.libadwaita) unreachable; + if (comptime !adwaita.comptimeEnabled()) unreachable; const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null; const child = c.adw_tab_page_get_child(page); break :child child; }, + .gtk_notebook => |notebook| child: { const page = self.currentPage(); if (page == -1) return null; - log.info("currentPage_page_idx = {}", .{page}); break :child c.gtk_notebook_get_nth_page(notebook, page); }, }; @@ -124,7 +136,7 @@ pub const Notebook = union(enum) { pub fn gotoNthTab(self: Notebook, position: c_int) void { switch (self) { .adw_tab_view => |tab_view| { - if (!build_options.libadwaita) unreachable; + if (comptime !adwaita.comptimeEnabled()) unreachable; const page_to_select = c.adw_tab_view_get_nth_page(tab_view, position); c.adw_tab_view_set_selected_page(tab_view, page_to_select); }, @@ -135,7 +147,7 @@ pub const Notebook = union(enum) { pub fn getTabPosition(self: Notebook, tab: *Tab) ?c_int { return switch (self) { .adw_tab_view => |tab_view| page_idx: { - if (!build_options.libadwaita) unreachable; + if (comptime !adwaita.comptimeEnabled()) unreachable; const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return null; break :page_idx c.adw_tab_view_get_page_position(tab_view, page); }, @@ -174,7 +186,7 @@ pub const Notebook = union(enum) { pub fn setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void { switch (self) { .adw_tab_view => |tab_view| { - if (!build_options.libadwaita) unreachable; + if (comptime !adwaita.comptimeEnabled()) unreachable; const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)); c.adw_tab_page_set_title(page, title.ptr); }, @@ -186,7 +198,7 @@ pub const Notebook = union(enum) { const box_widget: *c.GtkWidget = @ptrCast(tab.box); switch (self) { .adw_tab_view => |tab_view| { - if (!build_options.libadwaita) unreachable; + if (comptime !adwaita.comptimeEnabled()) unreachable; const page = c.adw_tab_view_append(tab_view, box_widget); c.adw_tab_page_set_title(page, title.ptr); @@ -257,7 +269,7 @@ pub const Notebook = union(enum) { const window = tab.window; switch (self) { .adw_tab_view => |tab_view| { - if (!build_options.libadwaita) unreachable; + if (comptime !adwaita.comptimeEnabled()) unreachable; const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return; c.adw_tab_view_close_page(tab_view, page); From c27f427e028ea6a6cb0a90c87cba11180a38bb3a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Sep 2024 09:50:31 -0700 Subject: [PATCH 13/16] apprt/gtk: the comptimeEnabled check is redundant --- src/apprt/gtk/App.zig | 2 +- src/apprt/gtk/Window.zig | 10 ++++------ src/apprt/gtk/adwaita.zig | 8 +++----- src/apprt/gtk/notebook.zig | 18 +++++++++--------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 79e8846e2..2ed178c2f 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -149,7 +149,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { }); // If not libadwaita, create a standard GTK application. - if ((comptime adwaita.comptimeEnabled()) and + if ((comptime adwaita.versionAtLeast(0, 0, 0)) and !adwaita.enabled(&config)) { break :app @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new( diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 0d5a2b27b..68b1197f7 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -60,10 +60,10 @@ pub fn init(self: *Window, app: *App) !void { }; // Create the window - const adw_window = (comptime adwaita.comptimeEnabled()) and + const adw_window = + (comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&app.config) and app.config.@"gtk-titlebar" and - (comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0); const window: *c.GtkWidget = if (adw_window) c.adw_application_window_new(app.app) @@ -127,8 +127,7 @@ pub fn init(self: *Window, app: *App) !void { // This is a really common issue where people build from source in debug and performance is really bad. if (comptime std.debug.runtime_safety) { const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded."; - if ((comptime adwaita.comptimeEnabled()) and - (comptime adwaita.versionAtLeast(1, 3, 0)) and + if ((comptime adwaita.versionAtLeast(1, 3, 0)) and adwaita.enabled(&app.config) and adwaita.versionAtLeast(1, 3, 0)) { @@ -161,8 +160,7 @@ pub fn init(self: *Window, app: *App) !void { // Our actions for the menu initActions(self); - if ((comptime adwaita.comptimeEnabled()) and - (comptime adwaita.versionAtLeast(1, 4, 0)) and + if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&app.config) and adwaita.versionAtLeast(1, 4, 0) and app.config.@"gtk-titlebar" and diff --git a/src/apprt/gtk/adwaita.zig b/src/apprt/gtk/adwaita.zig index db509db3a..0543bdfa7 100644 --- a/src/apprt/gtk/adwaita.zig +++ b/src/apprt/gtk/adwaita.zig @@ -3,13 +3,11 @@ const c = @import("c.zig").c; const build_options = @import("build_options"); const Config = @import("../../config.zig").Config; -/// Returns true if Ghostty is configured to build with libadwaita. -pub fn comptimeEnabled() bool { - return build_options.libadwaita; -} - /// Returns true if Ghostty is configured to build with libadwaita and /// the configuration has enabled adwaita. +/// +/// For a comptime version of this function, use `versionAtLeast` in +/// a comptime context with all the version numbers set to 0. pub fn enabled(config: *const Config) bool { return build_options.libadwaita and config.@"gtk-adwaita"; diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index a3d672f24..71f20aa4b 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -8,7 +8,7 @@ const adwaita = @import("adwaita.zig"); const log = std.log.scoped(.gtk); -const AdwTabView = if (adwaita.comptimeEnabled()) c.AdwTabView else anyopaque; +const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque; /// An abstraction over the GTK notebook and Adwaita tab view to manage /// all the terminal tabs in a window. @@ -94,7 +94,7 @@ pub const Notebook = union(enum) { pub fn nPages(self: Notebook) c_int { return switch (self) { .gtk_notebook => |notebook| c.gtk_notebook_get_n_pages(notebook), - .adw_tab_view => |tab_view| if (comptime adwaita.comptimeEnabled()) + .adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) c.adw_tab_view_get_n_pages(tab_view) else unreachable, @@ -104,7 +104,7 @@ pub const Notebook = union(enum) { pub fn currentPage(self: Notebook) c_int { switch (self) { .adw_tab_view => |tab_view| { - if (comptime !adwaita.comptimeEnabled()) unreachable; + if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; const page = c.adw_tab_view_get_selected_page(tab_view); return c.adw_tab_view_get_page_position(tab_view, page); }, @@ -116,7 +116,7 @@ pub const Notebook = union(enum) { pub fn currentTab(self: Notebook) ?*Tab { const child = switch (self) { .adw_tab_view => |tab_view| child: { - if (comptime !adwaita.comptimeEnabled()) unreachable; + if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; const page = c.adw_tab_view_get_selected_page(tab_view) orelse return null; const child = c.adw_tab_page_get_child(page); break :child child; @@ -136,7 +136,7 @@ pub const Notebook = union(enum) { pub fn gotoNthTab(self: Notebook, position: c_int) void { switch (self) { .adw_tab_view => |tab_view| { - if (comptime !adwaita.comptimeEnabled()) unreachable; + if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; const page_to_select = c.adw_tab_view_get_nth_page(tab_view, position); c.adw_tab_view_set_selected_page(tab_view, page_to_select); }, @@ -147,7 +147,7 @@ pub const Notebook = union(enum) { pub fn getTabPosition(self: Notebook, tab: *Tab) ?c_int { return switch (self) { .adw_tab_view => |tab_view| page_idx: { - if (comptime !adwaita.comptimeEnabled()) unreachable; + if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return null; break :page_idx c.adw_tab_view_get_page_position(tab_view, page); }, @@ -186,7 +186,7 @@ pub const Notebook = union(enum) { pub fn setTabLabel(self: Notebook, tab: *Tab, title: [:0]const u8) void { switch (self) { .adw_tab_view => |tab_view| { - if (comptime !adwaita.comptimeEnabled()) unreachable; + if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)); c.adw_tab_page_set_title(page, title.ptr); }, @@ -198,7 +198,7 @@ pub const Notebook = union(enum) { const box_widget: *c.GtkWidget = @ptrCast(tab.box); switch (self) { .adw_tab_view => |tab_view| { - if (comptime !adwaita.comptimeEnabled()) unreachable; + if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; const page = c.adw_tab_view_append(tab_view, box_widget); c.adw_tab_page_set_title(page, title.ptr); @@ -269,7 +269,7 @@ pub const Notebook = union(enum) { const window = tab.window; switch (self) { .adw_tab_view => |tab_view| { - if (comptime !adwaita.comptimeEnabled()) unreachable; + if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; const page = c.adw_tab_view_get_page(tab_view, @ptrCast(tab.box)) orelse return; c.adw_tab_view_close_page(tab_view, page); From 8186a8835b8e186d6d9257e66464d4574effddba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Sep 2024 10:17:36 -0700 Subject: [PATCH 14/16] apprt/gtk: scope comptime to only one --- src/apprt/gtk/notebook.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 71f20aa4b..a2c25c2ce 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -63,7 +63,7 @@ pub const Notebook = union(enum) { const tab_view: *c.AdwTabView = c.adw_tab_view_new().?; c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(tab_view))); - if (comptime !adwaita.versionAtLeast(1, 4, 0) or + if ((comptime !adwaita.versionAtLeast(1, 4, 0)) or !adwaita.versionAtLeast(1, 4, 0) or !app.config.@"gtk-titlebar") { From 2b5d436792b02c5d00aea57d239bcadb743d76f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Sep 2024 10:27:28 -0700 Subject: [PATCH 15/16] apprt/gtk: log the libadwaita version at startup if we use it --- src/apprt/gtk/App.zig | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 2ed178c2f..a1843b9ac 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -107,6 +107,18 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { } } + // If we're using libadwaita, log the version + if ((comptime adwaita.versionAtLeast(0, 0, 0)) and + adwaita.enabled(&config)) + { + log.info("libadwaita version build={s} runtime={}.{}.{}", .{ + c.ADW_VERSION_S, + c.adw_get_major_version(), + c.adw_get_minor_version(), + c.adw_get_micro_version(), + }); + } + // The "none" cursor is used for hiding the cursor const cursor_none = c.gdk_cursor_new_from_name("none", null); errdefer if (cursor_none) |cursor| c.g_object_unref(cursor); From 37ba0529135e4def7d749c534893b777711b5845 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Sep 2024 10:42:36 -0700 Subject: [PATCH 16/16] apprt/gtk: cleaup final notebook page on libadw 1.3.x --- src/apprt/gtk/adwaita.zig | 12 +++++++++--- src/apprt/gtk/notebook.zig | 14 +++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/adwaita.zig b/src/apprt/gtk/adwaita.zig index 0543bdfa7..f2d6bcfdd 100644 --- a/src/apprt/gtk/adwaita.zig +++ b/src/apprt/gtk/adwaita.zig @@ -43,7 +43,13 @@ pub fn versionAtLeast( // We use the functions instead of the constants such as // c.ADW_MINOR_VERSION because the function gets the actual // runtime version. - return c.adw_get_major_version() >= major and - c.adw_get_minor_version() >= minor and - c.adw_get_micro_version() >= micro; + if (c.adw_get_major_version() >= major) { + if (c.adw_get_major_version() > major) return true; + if (c.adw_get_minor_version() >= minor) { + if (c.adw_get_minor_version() > minor) return true; + return c.adw_get_micro_version() >= micro; + } + } + + return false; } diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index a2c25c2ce..8dd3a8690 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -275,8 +275,20 @@ pub const Notebook = union(enum) { c.adw_tab_view_close_page(tab_view, page); // If we have no more tabs we close the window - if (self.nPages() == 0) + if (self.nPages() == 0) { + // libadw versions <= 1.3.x leak the final page view + // which causes our surface to not properly cleanup. We + // unref to force the cleanup. This will trigger a critical + // warning from GTK, but I don't know any other workaround. + // Note: I'm not actually sure if 1.4.0 contains the fix, + // I just know that 1.3.x is broken and 1.5.1 is fixed. + // If we know that 1.4.0 is fixed, we can change this. + if (!adwaita.versionAtLeast(1, 4, 0)) { + c.g_object_unref(tab.box); + } + c.gtk_window_destroy(tab.window.window); + } }, .gtk_notebook => |notebook| { const page = c.gtk_notebook_get_page(notebook, @ptrCast(tab.box)) orelse return;