diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 029161a43..1a41d23af 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -56,7 +56,8 @@ pub fn init(self: *Paned, sibling: *Surface, direction: input.SplitDirection) !v const gtk_paned: *c.GtkPaned = @ptrCast(paned); self.paned = gtk_paned; - const surface = try sibling.tab.newSurface(&sibling.core_surface); + const tab = sibling.container.tab().?; // TODO + const surface = try tab.newSurface(&sibling.core_surface); surface.setParent(.{ .paned = .{ self, .end } }); self.addChild1(.{ .surface = sibling }); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index ad25abd56..19ee08b7d 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -55,6 +55,38 @@ pub const Options = struct { parentSurface: bool = false, }; +/// The container that this surface is directly attached to. +pub const Container = union(enum) { + /// The surface is not currently attached to anything. This means + /// that the GLArea has been created and potentially initialized + /// but the widget is currently floating and not part of any parent. + none: void, + + /// Directly attached to a tab. (i.e. no splits) + tab_: *Tab, + + /// A split within a split hierarchy. + paned: *Paned, + + /// Returns the window that this surface is attached to. + pub fn window(self: Container) ?*Window { + return switch (self) { + .none => null, + .tab_ => |v| v.window, + else => @panic("TODO"), + }; + } + + /// Returns the tab container if it exists. + pub fn tab(self: Container) ?*Tab { + return switch (self) { + .none => null, + .tab_ => |v| v, + else => @panic("TODO"), + }; + } +}; + /// Where the title of this surface will go. const Title = union(enum) { none: void, @@ -69,15 +101,16 @@ realized: bool = false, /// See Options.parentSurface parentSurface: bool = false, +/// The GUI container that this surface has been attached to. This +/// dictates some behaviors such as new splits, etc. +container: Container = .{ .none = {} }, + /// The app we're part of app: *App, /// The window we're part of window: *Window, -/// The tab we're part of -tab: *Tab, - /// The parent we belong to parent: Parent, @@ -177,7 +210,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { self.* = .{ .app = app, .window = opts.window, - .tab = opts.tab, + .container = .{ .tab_ = opts.tab }, .parent = opts.parent, .gl_area = opts.gl_area, .title = if (opts.title_label) |label| .{ @@ -456,8 +489,9 @@ pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.Surfac } pub fn grabFocus(self: *Surface) void { + if (self.container.tab()) |tab| tab.focus_child = self; + self.updateTitleLabels(); - self.tab.focus_child = self; const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); _ = c.gtk_widget_grab_focus(widget); } @@ -859,7 +893,7 @@ fn gtkMouseDown( // If we don't have focus, grab it. const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); if (c.gtk_widget_has_focus(gl_widget) == 0) { - self.tab.focus_child = self; + if (self.container.tab()) |tab| tab.focus_child = self; _ = c.gtk_widget_grab_focus(gl_widget); // If we have siblings, we also update the title, since it means diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 2945f42a0..185a0268f 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -1,3 +1,6 @@ +/// The state associated with a single tab in the window. +/// +/// A tab can contain one or more terminals due to splits. const Tab = @This(); const std = @import("std"); @@ -20,7 +23,6 @@ pub const GHOSTTY_TAB = "ghostty_tab"; window: *Window, label_text: *c.GtkLabel, -close_button: *c.GtkButton, // We'll put our children into this box instead of packing them directly, so // that we can send the box into `c.g_signal_connect_data` for the close button box: *c.GtkBox, @@ -38,11 +40,12 @@ pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab { return tab; } +/// Initialize the tab, create a surface, and add it to the window. "self" +/// needs to be a stable pointer, since it is used for GTK events. pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { self.* = .{ .window = window, .label_text = undefined, - .close_button = undefined, .box = undefined, .child = undefined, .focus_child = undefined, @@ -56,13 +59,11 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { 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"); 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); - self.close_button = label_close; - - _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT); // Wide style GTK tabs if (window.app.config.@"gtk-wide-tabs") { @@ -88,11 +89,10 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { c.gtk_widget_set_vexpand(box_widget, 1); self.box = @ptrCast(box_widget); - // Create the Surface + // Create the initial surface since all tabs start as a single non-split const surface = try self.newSurface(parent_); errdefer surface.deinit(); - - self.child = Child{ .surface = surface }; + self.child = .{ .surface = surface }; // Add Surface to the Tab const gl_area_widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); @@ -123,6 +123,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); + // Attach all events + _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT); + // Switch to the new tab c.gtk_notebook_set_current_page(window.notebook, page_idx); @@ -131,6 +134,17 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { surface.grabFocus(); } +/// Deinits tab by deiniting child if child is Paned. +pub fn deinit(self: *Tab) void { + switch (self.child) { + .none, .surface => return, + .paned => |paned| { + paned.deinit(self.window.app.core_app.alloc); + self.window.app.core_app.alloc.destroy(paned); + }, + } +} + /// Allocates and initializes a new Surface, but doesn't add it to the Tab yet. /// Can also be added to a Paned. pub fn newSurface(self: *Tab, parent_: ?*CoreSurface) !*Surface { @@ -206,17 +220,6 @@ pub fn setChild(self: *Tab, child: Child) void { self.child = child; } -/// Deinits tab by deiniting child if child is Paned. -pub fn deinit(self: *Tab) void { - switch (self.child) { - .none, .surface => return, - .paned => |paned| { - paned.deinit(self.window.app.core_app.alloc); - self.window.app.core_app.alloc.destroy(paned); - }, - } -} - fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { const tab: *Tab = @ptrCast(@alignCast(ud)); const window = tab.window; diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index eadb1ed8f..e3a38b3a3 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -1,4 +1,8 @@ -/// A Window is a single, real GTK window. +/// A Window is a single, real GTK window that holds terminal surfaces. +/// +/// A Window always contains a notebook (what GTK calls a tabbed container) +/// even while no tabs are in use, because a notebook without a tab bar has +/// no visible UI chrome. const Window = @This(); const std = @import("std"); @@ -327,7 +331,12 @@ pub fn hasTabs(self: *const Window) bool { /// Go to the previous tab for a surface. pub fn gotoPreviousTab(self: *Window, surface: *Surface) void { - const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.tab.box)) orelse return; + const tab = surface.container.tab() orelse { + log.info("surface is not attached to a tab bar, cannot navigate", .{}); + return; + }; + + const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return; const page_idx = getNotebookPageIndex(page); // The next index is the previous or we wrap around. @@ -345,7 +354,12 @@ pub fn gotoPreviousTab(self: *Window, surface: *Surface) void { /// Go to the next tab for a surface. pub fn gotoNextTab(self: *Window, surface: *Surface) void { - const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.tab.box)) orelse return; + const tab = surface.container.tab() orelse { + log.info("surface is not attached to a tab bar, cannot navigate", .{}); + return; + }; + + const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return; const page_idx = getNotebookPageIndex(page); const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1; const next_idx = if (page_idx < max) page_idx + 1 else 0;