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); +}