From f35671417c7e3bb6863d9f50bd95c0a1cd897945 Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Thu, 12 Sep 2024 23:17:14 +0100 Subject: [PATCH 1/2] apprt/gtk: use AdwTabOverview An additional way to manage tabs. Signed-off-by: Tristan Partin --- src/apprt/gtk/Tab.zig | 7 ++++++- src/apprt/gtk/Window.zig | 35 +++++++++++++++++++++++++++++++---- src/apprt/gtk/notebook.zig | 7 ++++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 3595bb977..e3134903d 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -37,6 +37,10 @@ elem: Surface.Container.Elem, // can easily re-focus that terminal. focus_child: *Surface, +/// If the notebook implementation is AdwTabView, then this is the tab's +/// AdwTabPage. +tab_page: *c.GObject, + pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab { var tab = try alloc.create(Tab); errdefer alloc.destroy(tab); @@ -53,6 +57,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { .box = undefined, .elem = undefined, .focus_child = undefined, + .tab_page = undefined, }; // Create a Box in which we'll later keep either Surface or Split. @@ -76,7 +81,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // 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"); + self.tab_page = try window.notebook.addTab(self, "Ghostty"); // Attach all events _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index b4aad87fb..235b7c053 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -97,6 +97,16 @@ pub fn init(self: *Window, app: *App) !void { const display = c.gdk_display_get_default(); c.gtk_style_context_add_provider_for_display(display, @ptrCast(app.css_provider), c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + // Create our box which will hold our widgets. + const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); + + var tab_overview: *c.GtkWidget = undefined; + if (self.isAdwWindow()) { + tab_overview = c.adw_tab_overview_new(); + c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1); + _ = c.g_signal_connect_data(tab_overview, "create-tab", c.G_CALLBACK(&newTabFromOverview), self, null, c.G_CONNECT_DEFAULT); + } + // gtk-titlebar can be used to disable the header bar (but keep // the window manager's decorations). We create this no matter if we // are decorated or not because we can have a keybind to toggle the @@ -117,6 +127,14 @@ pub fn init(self: *Window, app: *App) !void { else c.gtk_header_bar_pack_end(@ptrCast(header), btn); } + if (self.isAdwWindow()) { + const btn = c.gtk_toggle_button_new(); + c.gtk_widget_set_tooltip_text(btn, "Show Open Tabs"); + c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic"); + c.gtk_widget_set_focus_on_click(btn, c.FALSE); + c.adw_header_bar_pack_end(@ptrCast(header), btn); + _ = c.g_object_bind_property(btn, "active", tab_overview, "open", c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE); + } { const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic"); c.gtk_widget_set_tooltip_text(btn, "New Tab"); @@ -135,9 +153,6 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_window_set_decorated(gtk_window, 0); } - // 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 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) { @@ -180,6 +195,7 @@ pub fn init(self: *Window, app: *App) !void { initActions(self); if (self.hasAdwToolbar()) { + c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view); const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?)); @@ -210,7 +226,8 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_widget_set_visible(header_widget, 0); } - c.adw_application_window_set_content(@ptrCast(gtk_window), @ptrCast(@alignCast(toolbar_view))); + c.adw_tab_overview_set_child(@ptrCast(tab_overview), @ptrCast(@alignCast(toolbar_view))); + c.adw_application_window_set_content(@ptrCast(gtk_window), @ptrCast(@alignCast(tab_overview))); } else { // The box is our main child c.gtk_window_set_child(gtk_window, box); @@ -379,6 +396,16 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { }; } +/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick +/// because we need to return an AdwTabPage from this function. +fn newTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.GObject { + const self: *Window = @ptrCast(@alignCast(ud orelse return null)); + const alloc = self.app.core_app.alloc; + const surface = self.actionSurface() orelse return null; + const tab = Tab.create(alloc, self, surface) catch return null; + return tab.tab_page; +} + fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { _ = v; log.debug("refocus term request", .{}); diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index bd03399f9..7f840e321 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -198,8 +198,9 @@ pub const Notebook = union(enum) { } } - pub fn addTab(self: Notebook, tab: *Tab, title: [:0]const u8) !void { + pub fn addTab(self: Notebook, tab: *Tab, title: [:0]const u8) !*c.GObject { const box_widget: *c.GtkWidget = @ptrCast(tab.box); + var tab_page: *c.GObject = undefined; switch (self) { .adw_tab_view => |tab_view| { if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; @@ -209,6 +210,8 @@ pub const Notebook = union(enum) { // Switch to the new tab c.adw_tab_view_set_selected_page(tab_view, page); + + tab_page = @ptrCast(@alignCast(page)); }, .gtk_notebook => |notebook| { // Build the tab label @@ -267,6 +270,8 @@ pub const Notebook = union(enum) { c.gtk_notebook_set_current_page(notebook, page_idx); }, } + + return tab_page; } pub fn closeTab(self: Notebook, tab: *Tab) void { From fced4370d3e7b664f3e1252c562cfcb03376a4a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Sep 2024 16:06:14 -0700 Subject: [PATCH 2/2] apprt/gtk: some tweaks, avoid uninitialized memory access, use optionals --- src/apprt/gtk/Tab.zig | 11 +++--- src/apprt/gtk/Window.zig | 69 +++++++++++++++++++++++++++++++------- src/apprt/gtk/notebook.zig | 12 ++++--- 3 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index e3134903d..415e5fb49 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -38,8 +38,9 @@ elem: Surface.Container.Elem, focus_child: *Surface, /// If the notebook implementation is AdwTabView, then this is the tab's -/// AdwTabPage. -tab_page: *c.GObject, +/// AdwTabPage. If we have libadwaita disabled then this will be undefined +/// memory and is unsafe to use. +adw_tab_page: *c.GObject, pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab { var tab = try alloc.create(Tab); @@ -57,7 +58,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { .box = undefined, .elem = undefined, .focus_child = undefined, - .tab_page = undefined, + .adw_tab_page = undefined, }; // Create a Box in which we'll later keep either Surface or Split. @@ -81,7 +82,9 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // Set the userdata of the box to point to this tab. c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); - self.tab_page = try window.notebook.addTab(self, "Ghostty"); + if (try window.notebook.addTab(self, "Ghostty")) |page| { + self.adw_tab_page = page; + } // Attach all events _ = c.g_signal_connect_data(box_widget, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 235b7c053..7241a92ff 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -97,15 +97,24 @@ pub fn init(self: *Window, app: *App) !void { const display = c.gdk_display_get_default(); c.gtk_style_context_add_provider_for_display(display, @ptrCast(app.css_provider), c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - // Create our box which will hold our widgets. + // Create our box which will hold our widgets in the main content area. const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); - var tab_overview: *c.GtkWidget = undefined; - if (self.isAdwWindow()) { - tab_overview = c.adw_tab_overview_new(); + // If we are using an AdwWindow then we can support the tab overview. + const tab_overview_: ?*c.GtkWidget = if (self.isAdwWindow()) overview: { + const tab_overview = c.adw_tab_overview_new(); c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1); - _ = c.g_signal_connect_data(tab_overview, "create-tab", c.G_CALLBACK(&newTabFromOverview), self, null, c.G_CONNECT_DEFAULT); - } + _ = c.g_signal_connect_data( + tab_overview, + "create-tab", + c.G_CALLBACK(>kNewTabFromOverview), + self, + null, + c.G_CONNECT_DEFAULT, + ); + + break :overview tab_overview; + } else null; // gtk-titlebar can be used to disable the header bar (but keep // the window manager's decorations). We create this no matter if we @@ -127,14 +136,25 @@ pub fn init(self: *Window, app: *App) !void { else c.gtk_header_bar_pack_end(@ptrCast(header), btn); } - if (self.isAdwWindow()) { + + // If we're using an AdwWindow then we can support the tab overview. + if (tab_overview_) |tab_overview| { + assert(self.isAdwWindow()); + const btn = c.gtk_toggle_button_new(); c.gtk_widget_set_tooltip_text(btn, "Show Open Tabs"); c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic"); c.gtk_widget_set_focus_on_click(btn, c.FALSE); c.adw_header_bar_pack_end(@ptrCast(header), btn); - _ = c.g_object_bind_property(btn, "active", tab_overview, "open", c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE); + _ = c.g_object_bind_property( + btn, + "active", + tab_overview, + "open", + c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE, + ); } + { const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic"); c.gtk_widget_set_tooltip_text(btn, "New Tab"); @@ -176,8 +196,15 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_box_append(@ptrCast(box), warning_box); } + // Setup our notebook self.notebook = Notebook.create(self, box); + // If we have a tab overview then we can set it on our notebook. + if (tab_overview_) |tab_overview| { + assert(self.notebook == .adw_tab_view); + c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view); + } + 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); c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), c.False); @@ -195,7 +222,6 @@ pub fn init(self: *Window, app: *App) !void { initActions(self); if (self.hasAdwToolbar()) { - c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view); const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?)); @@ -226,8 +252,23 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_widget_set_visible(header_widget, 0); } - c.adw_tab_overview_set_child(@ptrCast(tab_overview), @ptrCast(@alignCast(toolbar_view))); - c.adw_application_window_set_content(@ptrCast(gtk_window), @ptrCast(@alignCast(tab_overview))); + // Set our application window content. The content depends on if + // we're using an AdwTabOverview or not. + if (tab_overview_) |tab_overview| { + c.adw_tab_overview_set_child( + @ptrCast(tab_overview), + @ptrCast(@alignCast(toolbar_view)), + ); + c.adw_application_window_set_content( + @ptrCast(gtk_window), + @ptrCast(@alignCast(tab_overview)), + ); + } else { + 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); @@ -398,12 +439,14 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { /// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick /// because we need to return an AdwTabPage from this function. -fn newTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.GObject { +fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.GObject { const self: *Window = @ptrCast(@alignCast(ud orelse return null)); + assert(self.isAdwWindow()); + const alloc = self.app.core_app.alloc; const surface = self.actionSurface() orelse return null; const tab = Tab.create(alloc, self, surface) catch return null; - return tab.tab_page; + return tab.adw_tab_page; } fn gtkRefocusTerm(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { diff --git a/src/apprt/gtk/notebook.zig b/src/apprt/gtk/notebook.zig index 7f840e321..4b826f8a0 100644 --- a/src/apprt/gtk/notebook.zig +++ b/src/apprt/gtk/notebook.zig @@ -198,9 +198,11 @@ pub const Notebook = union(enum) { } } - pub fn addTab(self: Notebook, tab: *Tab, title: [:0]const u8) !*c.GObject { + /// Adds a new tab with the given title to the notebook. If the notebook + /// is an adwaita tab view, this will return an AdwTabPage. If the notebook + /// is a GTK notebook, this will return null. + pub fn addTab(self: Notebook, tab: *Tab, title: [:0]const u8) !?*c.GObject { const box_widget: *c.GtkWidget = @ptrCast(tab.box); - var tab_page: *c.GObject = undefined; switch (self) { .adw_tab_view => |tab_view| { if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; @@ -211,7 +213,7 @@ pub const Notebook = union(enum) { // Switch to the new tab c.adw_tab_view_set_selected_page(tab_view, page); - tab_page = @ptrCast(@alignCast(page)); + return @ptrCast(@alignCast(page)); }, .gtk_notebook => |notebook| { // Build the tab label @@ -268,10 +270,10 @@ pub const Notebook = union(enum) { // Switch to the new tab c.gtk_notebook_set_current_page(notebook, page_idx); + + return null; }, } - - return tab_page; } pub fn closeTab(self: Notebook, tab: *Tab) void {