From d88898fc61176c36fbf928a97a21068b076d2be5 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 19 Oct 2023 13:04:15 +0200 Subject: [PATCH] gtk: get 1st version of GTK splits working --- src/apprt/gtk/Paned.zig | 115 +++++++----------- src/apprt/gtk/Surface.zig | 92 ++++++++++++--- src/apprt/gtk/Tab.zig | 117 +++++++++++++++---- src/apprt/gtk/Window.zig | 237 ++++++++++++++++++++------------------ 4 files changed, 337 insertions(+), 224 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 88cd21158..8cf3d79ce 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -13,23 +13,8 @@ const Position = @import("parent.zig").Position; const Parent = @import("parent.zig").Parent; const c = @import("c.zig"); -const Child = union(enum) { - surface: *Surface, - paned: *Paned, - empty: void, - - const Self = @This(); - - fn is_empty(self: Self) bool { - switch (self) { - Child.empty => return true, - else => return false, - } - } -}; - /// We'll need to keep a reference to the Window this belongs to for various reasons -Window: *c.GtkWindow, +window: *Window, // We keep track of the tab label's text so that if a child widget of this pane // gets focus (and is a Surface) we can reset the tab label appropriately @@ -40,8 +25,8 @@ paned: *c.GtkPaned, // We have two children, each of which can be either a Surface, another pane, // or empty. We're going to keep track of which each child is here. -child1: Child, -child2: Child, +child1: Tab.Child, +child2: Tab.Child, // We also hold a reference to our parent widget, so that when we close we can either // maximize the parent pane, or close the tab. @@ -59,8 +44,8 @@ pub fn init(self: *Paned, window: *Window, label_text: *c.GtkWidget) !void { .window = window, .label_text = label_text, .paned = undefined, - .child1 = Child{.empty}, - .child2 = Child{.empty}, + .child1 = .empty, + .child2 = .empty, .parent = undefined, }; @@ -69,16 +54,16 @@ pub fn init(self: *Paned, window: *Window, label_text: *c.GtkWidget) !void { errdefer c.gtk_widget_destroy(paned); self.paned = gtk_paned; - const surface = try self.newSurface(self.window.actionSurface()); - // We know that both panels are currently empty, so we maximize the 1st - c.gtk_paned_set_position(self.paned, 100); - const child_widget: *c.GtkWidget = @ptrCast(surface.gl_area); - const child = Child{ .surface = surface }; - c.gtk_paned_pack1(self.paned, child_widget, 1, 1); - self.child1 = child; + // const surface = try self.newSurface(self.window.actionSurface()); + // // We know that both panels are currently empty, so we maximize the 1st + // c.gtk_paned_set_position(self.paned, 100); + // const child_widget: *c.GtkWidget = @ptrCast(surface.gl_area); + // const child = Child{ .surface = surface }; + // c.gtk_paned_pack1(self.paned, child_widget, 1, 1); + // self.child1 = child; } -pub fn newSurface(self: *Paned, parent_: ?*CoreSurface) !*Surface { +pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { // Grab a surface allocation we'll need it later. var surface = try self.window.app.core_app.alloc.create(Surface); errdefer self.window.app.core_app.alloc.destroy(surface); @@ -95,8 +80,15 @@ pub fn newSurface(self: *Paned, parent_: ?*CoreSurface) !*Surface { // wait for the "realize" callback from GTK to know that the OpenGL // context is ready. See Surface docs for more info. const gl_area = c.gtk_gl_area_new(); + c.gtk_widget_set_hexpand(gl_area, 1); + c.gtk_widget_set_vexpand(gl_area, 1); try surface.init(self.window.app, .{ - .window = self, + .window = self.window, + .tab = tab, + .parent = .{ .paned = .{ + self, + Position.end, + } }, .gl_area = @ptrCast(gl_area), .title_label = @ptrCast(self.label_text), .font_size = font_size, @@ -104,62 +96,35 @@ pub fn newSurface(self: *Paned, parent_: ?*CoreSurface) !*Surface { return surface; } -fn addChild1Surface(self: *Paned, surface: *Surface) void { - assert(self.child1.is_empty()); - self.child1 = Child{ .surface = surface }; - surface.parent = Surface.Parent{ .paned = .{ self, Position.start } }; - c.gtk_paned_pack1(@ptrCast(self.paned), @ptrCast(surface.gl_area), 1, 0); - if (self.child2.is_empty()) { - c.gtk_paned_set_position(self.paned, 100); - } else { - c.gtk_paned_set_position(self.paned, 50); - } +pub fn removeChildren(self: *Paned) void { + c.gtk_paned_set_start_child(@ptrCast(self.paned), null); + c.gtk_paned_set_end_child(@ptrCast(self.paned), null); } -fn addChild2Surface(self: *Paned, surface: *Surface) void { +pub fn addChild1Surface(self: *Paned, surface: *Surface) void { assert(self.child1.is_empty()); - self.child2 = Child{ .surface = surface }; - surface.parent = Surface.Parent{ .paned = .{ self, Position.end } }; - c.gtk_paned_pack2(@ptrCast(self.paned), @ptrCast(surface.gl_area), 1, 0); - if (self.child2.is_empty()) { - c.gtk_paned_set_position(self.paned, 0); - } else { - c.gtk_paned_set_position(self.paned, 50); - } + self.child1 = Tab.Child{ .surface = surface }; + surface.setParent(Parent{ .paned = .{ self, Position.start } }); + c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); } -fn addChild1Paned(self: *Paned, child: *Paned) void { - assert(self.child1.is_empty()); - self.child1 = Child{ .paned = child }; - child.parent = Parent{ .paned = .{ self, Position.start } }; - c.gtk_paned_pack1(@ptrCast(self.paned), @ptrCast(child.paned), 1, 0); - if (self.child2.is_empty()) { - c.gtk_paned_set_position(self.paned, 100); - } else { - c.gtk_paned_set_position(self.paned, 50); - } +pub fn addChild2Surface(self: *Paned, surface: *Surface) void { + assert(self.child2.is_empty()); + self.child2 = Tab.Child{ .surface = surface }; + surface.setParent(Parent{ .paned = .{ self, Position.end } }); + c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); } -fn addChild2Paned(self: *Paned, child: *Paned) void { - assert(self.child1.is_empty()); - self.child2 = Child{ .paned = child }; - child.parent = Parent{ .paned = .{ self, Position.end } }; - c.gtk_paned_pack2(@ptrCast(self.paned), @ptrCast(child.paned), 1, 0); - if (self.child2.is_empty()) { - c.gtk_paned_set_position(self.paned, 0); - } else { - c.gtk_paned_set_position(self.paned, 50); - } -} - -fn removeChild1(self: *Paned) void { +pub fn removeChild1(self: *Paned) void { assert(!self.child1.is_empty()); - // todo + self.child1 = .empty; + c.gtk_paned_set_start_child(@ptrCast(self.paned), null); } -fn removeChild2(self: *Paned) void { - assert(!self.child1.is_empty()); - // todo +pub fn removeChild2(self: *Paned) void { + assert(!self.child2.is_empty()); + self.child2 = .empty; + c.gtk_paned_set_end_child(@ptrCast(self.paned), null); } pub fn splitStartPosition(self: *Paned, orientation: c.GtkOrientation) !void { diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index b5572607b..78fd4fad3 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -32,6 +32,12 @@ pub const Options = struct { /// The window that this surface is attached to. window: *Window, + /// The tab that this surface is attached to. + tab: *Tab, + + /// The parent this surface is created under. + parent: Parent, + /// The GL area that this surface should draw to. gl_area: *c.GtkGLArea, @@ -41,13 +47,6 @@ pub const Options = struct { /// A font size to set on the surface once it is initialized. font_size: ?font.face.DesiredSize = null, - - /// True if this surface has a parent. This is a bit of a hack currently - /// to work around newConfig unconditinally inheriting the working - /// directory. The proper long term fix is to have the working directory - /// inherited upstream likely at the point where this field would be set, - /// then remove this field. - parent: bool = false, }; /// Where the title of this surface will go. @@ -61,16 +60,16 @@ const Title = union(enum) { /// surface has been initialized. realized: bool = false, -/// See Options.parent -parent: bool = false, - /// The app we're part of app: *App, /// The window we're part of window: *Window, -/// Our parent widget +/// The tab we're part of +tab: *Tab, + +/// The parent we belong to parent: Parent, /// Our GTK area @@ -161,14 +160,14 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { self.* = .{ .app = app, .window = opts.window, - .parent = Parent.none, + .tab = opts.tab, + .parent = opts.parent, .gl_area = opts.gl_area, .title = if (opts.title_label) |label| .{ .label = label, } else .{ .none = {} }, .core_surface = undefined, .font_size = opts.font_size, - .parent = opts.parent, .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .im_context = im_context, @@ -347,6 +346,51 @@ pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFulls self.window.toggleFullscreen(mac_non_native); } +pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { + log.info("surface.newSplit. direction={}", .{direction}); + + switch (self.parent) { + .none => { + log.info("no parent\n", .{}); + }, + .paned => { + log.info("parent is paned \n", .{}); + }, + .tab => |tab| { + const tab_idx = for (self.window.tabs.items, 0..) |t, i| { + if (t == tab) break i; + } else null; + + const label_text: ?*c.GtkWidget = switch (self.title) { + .none => null, + .label => |label| l: { + const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(label))); + break :l widget; + }, + }; + + if (label_text) |text| { + tab.removeChild(); + + const paned = try Paned.create(self.app.core_app.alloc, self.window, text); + + const new_surface = try paned.newSurface(tab, &self.core_surface); + // // This sets .parent on each surface + paned.addChild1Surface(self); + paned.addChild2Surface(new_surface); + + tab.setChild(.{ .paned = paned }); + + // FOCUS ON NEW SURFACE + const widget = @as(*c.GtkWidget, @ptrCast(new_surface.gl_area)); + _ = c.gtk_widget_grab_focus(widget); + } else { + log.info("no label text: {?}\n", .{tab_idx}); + } + }, + } +} + pub fn newTab(self: *Surface) !void { try self.window.newTab(&self.core_surface); } @@ -429,6 +473,10 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { // )); } +pub fn setParent(self: *Surface, parent: Parent) void { + self.parent = parent; +} + pub fn setMouseShape( self: *Surface, shape: terminal.MouseShape, @@ -755,9 +803,20 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { log.debug("gl destroy", .{}); const self = userdataSelf(ud.?); - const alloc = self.app.core_app.alloc; - self.deinit(); - alloc.destroy(self); + switch (self.parent) { + .none, .tab => { + const alloc = self.app.core_app.alloc; + self.deinit(); + alloc.destroy(self); + }, + else => { + + // const alloc = self.app.core_app.alloc; + // self.deinit(); + // alloc.destroy(self); + log.debug("TODO: no destroy", .{}); + }, + } } /// Scale x/y by the GDK device scale. @@ -1226,7 +1285,6 @@ fn gtkInputCommit( fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void { const self = userdataSelf(ud.?); - // Notify our IM context c.gtk_im_context_focus_in(self.im_context); diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 7cfad8e63..3855a60d5 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -2,20 +2,33 @@ const Tab = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; +const assert = std.debug.assert; const font = @import("../../font/main.zig"); const CoreSurface = @import("../../Surface.zig"); const Paned = @import("Paned.zig"); +const Parent = @import("parent.zig").Parent; const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const c = @import("c.zig"); const log = std.log.scoped(.gtk); -const GHOSTTY_TAB = "ghostty_tab"; +pub const GHOSTTY_TAB = "ghostty_tab"; -const Child = union(enum) { +pub const Child = union(enum) { surface: *Surface, paned: *Paned, + + empty, + + const Self = @This(); + + pub fn is_empty(self: Self) bool { + switch (self) { + Child.empty => return true, + else => return false, + } + } }; window: *Window, @@ -35,6 +48,7 @@ pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab { var tab = try alloc.create(Tab); errdefer alloc.destroy(tab); try tab.init(window, parent_); + return tab; } pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { @@ -48,8 +62,11 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { }; // Grab a surface allocation we'll need it later. - var surface = try self.app.core_app.alloc.create(Surface); - errdefer self.app.core_app.alloc.destroy(surface); + var surface = try window.app.core_app.alloc.create(Surface); + errdefer window.app.core_app.alloc.destroy(surface); + self.child = Child{ .surface = surface }; + // TODO: this needs to change + self.focus_child = surface; // Inherit the parent's font size if we are configured to. const font_size: ?font.face.DesiredSize = font_size: { @@ -60,25 +77,35 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // Build the tab label const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0); - const label_box: *c.GtkBox = @ptrCast(label_box_widget); + 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); self.label_text = label_text; c.gtk_box_append(label_box, label_text_widget); 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_has_frame(label_close, 0); + 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), surface, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), self, null, c.G_CONNECT_DEFAULT); // Wide style GTK tabs - if (self.app.config.@"gtk-wide-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, 1); - c.gtk_widget_set_halign(label_text, 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(@ptrCast(label_text), 1); + c.gtk_label_set_ellipsize(@ptrCast(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(@ptrCast(label_text), 100, 1); } const box_widget = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); @@ -93,42 +120,86 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { const gl_area = c.gtk_gl_area_new(); c.gtk_widget_set_hexpand(gl_area, 1); c.gtk_widget_set_vexpand(gl_area, 1); - try surface.init(self.app, .{ - .window = self, + try surface.init(window.app, .{ + .window = window, + .tab = self, + .parent = .{ .tab = self }, .gl_area = @ptrCast(gl_area), .title_label = @ptrCast(label_text), .font_size = font_size, }); errdefer surface.deinit(); - c.gtk_box_pack_start(self.box, gl_area); - const page_idx = c.gtk_notebook_append_page(self.notebook, box_widget, label_box_widget); + c.gtk_box_append(self.box, gl_area); + const page_idx = c.gtk_notebook_append_page(window.notebook, box_widget, label_box_widget); if (page_idx < 0) { log.warn("failed to add page to notebook", .{}); return error.GtkAppendPageFailed; } // Tab settings - c.gtk_notebook_set_tab_reorderable(self.notebook, gl_area, 1); - c.gtk_notebook_set_tab_detachable(self.notebook, gl_area, 1); + c.gtk_notebook_set_tab_reorderable(window.notebook, gl_area, 1); + c.gtk_notebook_set_tab_detachable(window.notebook, gl_area, 1); // If we have multiple tabs, show the tab bar. - if (c.gtk_notebook_get_n_pages(self.notebook) > 1) { - c.gtk_notebook_set_show_tabs(self.notebook, 1); + if (c.gtk_notebook_get_n_pages(window.notebook) > 1) { + c.gtk_notebook_set_show_tabs(window.notebook, 1); } - // Set the userdata of the close button so it points to this page. + // Set the userdata of the box to point to this tab. c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); // Switch to the new tab - c.gtk_notebook_set_current_page(self.notebook, page_idx); + c.gtk_notebook_set_current_page(window.notebook, page_idx); // We need to grab focus after it is added to the window. When // creating a window we want to always focus on the widget. - _ = c.gtk_widget_grab_focus(box_widget); + const widget = @as(*c.GtkWidget, @ptrCast(gl_area)); + _ = c.gtk_widget_grab_focus(widget); +} + +pub fn removeChild(self: *Tab) void { + // Remove old child from box. + const widget = switch (self.child) { + .surface => |surface| @as(*c.GtkWidget, @ptrCast(surface.gl_area)), + .paned => |paned| @as(*c.GtkWidget, @ptrCast(@alignCast(paned.paned))), + .empty => return, + }; + c.gtk_box_remove(self.box, widget); + self.child = .empty; +} + +pub fn setChild(self: *Tab, newChild: Child) void { + const parent = Parent{ .tab = self }; + + switch (newChild) { + .surface => |surface| { + surface.setParent(parent); + const widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); + c.gtk_box_append(self.box, widget); + }, + .paned => |paned| { + paned.parent = parent; + const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(paned.paned))); + c.gtk_box_append(self.box, widget); + }, + .empty => return, + } + + self.child = newChild; +} + +pub fn setChildSurface(self: *Tab, surface: *Surface, gl_area: *c.GtkWidget) !void { + c.gtk_box_append(self.box, gl_area); + + const parent = Parent{ .tab = self }; + surface.setParent(parent); + + self.child = .{ .surface = surface }; } fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { - _ = ud; - // todo + const tab: *Tab = @ptrCast(@alignCast(ud)); + _ = tab; + log.info("tab close click\n", .{}); } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 81ee174ae..c6a91bd90 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -8,18 +8,18 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const configpkg = @import("../../config.zig"); const font = @import("../../font/main.zig"); +const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); const Paned = @import("Paned.zig"); const Surface = @import("Surface.zig"); +const Tab = @import("Tab.zig"); const icon = @import("icon.zig"); const c = @import("c.zig"); const log = std.log.scoped(.gtk); -const GL_AREA_SURFACE = "gl_area_surface"; - app: *App, /// Our window @@ -32,6 +32,8 @@ notebook: *c.GtkNotebook, /// pointer to this because GTK can use it at any time. icon: icon.Icon, +tabs: std.ArrayListUnmanaged(*Tab), + pub fn create(alloc: Allocator, app: *App) !*Window { // Allocate a fixed pointer for our window. We try to minimize // allocations but windows and other GUI requirements are so minimal @@ -53,8 +55,13 @@ pub fn init(self: *Window, app: *App) !void { .icon = undefined, .window = undefined, .notebook = undefined, + .tabs = undefined, }; + var tabs: std.ArrayListUnmanaged(*Tab) = .{}; + errdefer tabs.deinit(app.core_app.alloc); + self.tabs = tabs; + // Create the window const window = c.gtk_application_window_new(app.app); const gtk_window: *c.GtkWindow = @ptrCast(window); @@ -182,105 +189,48 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { self.icon.deinit(self.app); + for (self.tabs.items) |tab| self.app.core_app.alloc.destroy(tab); + self.tabs.deinit(self.app.core_app.alloc); } /// Add a new tab to this window. -pub fn newTab(self: *Window, parent_: ?*CoreSurface) !void { - // Grab a surface allocation we'll need it later. - var surface = try self.app.core_app.alloc.create(Surface); - errdefer self.app.core_app.alloc.destroy(surface); +pub fn newTab(self: *Window, parentSurface: ?*CoreSurface) !void { + const tab = try Tab.create(self.app.core_app.alloc, self, parentSurface); + try self.tabs.append(self.app.core_app.alloc, tab); - // Inherit the parent's font size if we are configured to. - const font_size: ?font.face.DesiredSize = font_size: { - if (!self.app.config.@"window-inherit-font-size") break :font_size null; - const parent = parent_ orelse break :font_size null; - break :font_size parent.font_size; - }; - - // Build our 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 = c.gtk_label_new("Ghostty"); - c.gtk_box_append(label_box, label_text); - - // Wide style GTK tabs - if (self.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, 1); - c.gtk_widget_set_halign(label_text, 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(@ptrCast(label_text), 1); - c.gtk_label_set_ellipsize(@ptrCast(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(@ptrCast(label_text), 100, 1); - } - - const label_close_widget = c.gtk_button_new_from_icon_name("window-close"); - const label_close = @as(*c.GtkButton, @ptrCast(label_close_widget)); - c.gtk_button_set_has_frame(label_close, 0); - c.gtk_box_append(label_box, label_close_widget); - _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), surface, null, c.G_CONNECT_DEFAULT); - - // Initialize the GtkGLArea and attach it to our surface. - // The surface starts in the "unrealized" state because we have to - // wait for the "realize" callback from GTK to know that the OpenGL - // context is ready. See Surface docs for more info. - const gl_area = c.gtk_gl_area_new(); - c.gtk_widget_set_cursor_from_name(gl_area, "text"); - try surface.init(self.app, .{ - .window = self, - .gl_area = @ptrCast(gl_area), - .title_label = @ptrCast(label_text), - .font_size = font_size, - .parent = parent_ != null, - }); - errdefer surface.deinit(); - - // Add the notebook page (create tab). We create the tab after our - // current selected tab if we have one. - const page_idx = c.gtk_notebook_insert_page( - self.notebook, - gl_area, - label_box_widget, - c.gtk_notebook_get_current_page(self.notebook) + 1, - ); - if (page_idx < 0) { - log.warn("failed to add surface to notebook", .{}); - return error.GtkAppendPageFailed; - } - - // Tab settings - c.gtk_notebook_set_tab_reorderable(self.notebook, gl_area, 1); - c.gtk_notebook_set_tab_detachable(self.notebook, gl_area, 1); - - // If we have multiple tabs, show the tab bar. - if (c.gtk_notebook_get_n_pages(self.notebook) > 1) { - c.gtk_notebook_set_show_tabs(self.notebook, 1); - } - - // Set the userdata of the close button so it points to this page. - c.g_object_set_data(@ptrCast(gl_area), GL_AREA_SURFACE, surface); - - // Switch to the new tab - c.gtk_notebook_set_current_page(self.notebook, page_idx); - - // We need to grab focus after it is added to the window. When - // creating a window we want to always focus on the widget. - const widget = @as(*c.GtkWidget, @ptrCast(gl_area)); - _ = c.gtk_widget_grab_focus(widget); + log.info("\n\n\nnewTab. New tabs len={}\n", .{self.tabs.items.len}); + // TODO: When this is triggered through a GTK action, the new surface + // redraws correctly. When it's triggered through keyboard shortcuts, it + // does not (cursor doesn't blink). } /// Close the tab for the given notebook page. This will automatically /// handle closing the window if there are no more tabs. fn closeTab(self: *Window, page: *c.GtkNotebookPage) void { - // Remove the page + // Find page and tab which we're closing const page_idx = getNotebookPageIndex(page); + const page_widget = c.gtk_notebook_get_nth_page(self.notebook, page_idx); + const tab: *Tab = @ptrCast(@alignCast( + c.g_object_get_data(@ptrCast(page_widget), Tab.GHOSTTY_TAB) orelse return, + )); + + // Remove the tab from our stored tabs. + const tab_idx = for (self.tabs.items, 0..) |t, i| { + if (t == tab) break i; + } else null; + // TODO: Shrink capacity? + if (tab_idx) |idx| { + _ = self.tabs.orderedRemove(idx); + } else { + log.info("tab of page {} not found in managed tabs list\n", .{page_idx}); + return; + } + // Deallocate the tab + self.app.core_app.alloc.destroy(tab); + + log.info("\n\n\ncloseTab. New tabs len={}\n", .{self.tabs.items.len}); + + // Now remove the page c.gtk_notebook_remove_page(self.notebook, page_idx); const remaining = c.gtk_notebook_get_n_pages(self.notebook); @@ -302,8 +252,75 @@ fn closeTab(self: *Window, page: *c.GtkNotebookPage) void { pub fn closeSurface(self: *Window, surface: *Surface) void { assert(surface.window == self); - const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.gl_area)) orelse return; - self.closeTab(page); + const alloc = surface.app.core_app.alloc; + + switch (surface.parent) { + .tab => { + const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.tab.box)) orelse return; + self.closeTab(page); + }, + .paned => |paned_tuple| { + const paned = paned_tuple[0]; + const position = paned_tuple[1]; + + // TODO: Do we need this? + surface.setParent(.none); + + const sibling = switch (position) { + .start => .{ + switch (paned.child2) { + .surface => |s| s, + else => return, + }, + c.gtk_paned_get_end_child(paned.paned), + }, + .end => .{ + switch (paned.child1) { + .surface => |s| s, + else => return, + }, + c.gtk_paned_get_start_child(paned.paned), + }, + }; + // TODO: Use destructuring syntax once it doesn't break ZLS + const sibling_surface = sibling[0]; + const sibling_widget = sibling[1]; + + // Keep explicit reference to sibling's gl_area, so it's not + // destroyed when we remove it from GtkPaned. + const sibling_object: *c.GObject = @ptrCast(sibling_widget); + _ = c.g_object_ref(sibling_object); + defer c.g_object_unref(sibling_object); + + // Remove children and kill Paned. + paned.removeChild1(); + paned.removeChild2(); + defer alloc.destroy(paned); + + // Remove children from Paned we were part of. + switch (paned.parent) { + .tab => |tab| { + // If parent of Paned we belong to is a tab, we can + // replace the child with the other surface + tab.removeChild(); + + tab.setChild(.{ .surface = sibling_surface }); + // try tab.setChildSurface(sibling_surface, sibling_widget); + }, + .paned => |paned_paned| { + log.info("paned is nested, parent is paned. position={}", .{paned_paned[1]}); + }, + .none => { + log.info("paned has no parent", .{}); + }, + } + + // alloc.destroy(paned); + }, + .none => { + log.info("no parent, dude?!", .{}); + }, + } } /// Returns true if this window has any tabs. @@ -313,7 +330,7 @@ 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.gl_area)) orelse return; + const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.tab.box)) orelse return; const page_idx = getNotebookPageIndex(page); // The next index is the previous or we wrap around. @@ -331,7 +348,7 @@ 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.gl_area)) orelse return; + const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.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; @@ -365,13 +382,12 @@ pub fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) 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 widget = c.gtk_notebook_get_nth_page(self.notebook, page_idx); - _ = c.gtk_widget_grab_focus(widget); -} - -fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { - const surface: *Surface = @ptrCast(@alignCast(ud)); - surface.core_surface.close(); + 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 gl_area = @as(*c.GtkWidget, @ptrCast(tab.focus_child.gl_area)); + _ = c.gtk_widget_grab_focus(gl_area); } // Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab @@ -424,10 +440,11 @@ fn gtkNotebookCreateWindow( page: *c.GtkWidget, ud: ?*anyopaque, ) callconv(.C) ?*c.GtkNotebook { - // The surface for the page is stored in the widget data. - const surface: *Surface = @ptrCast(@alignCast( - c.g_object_get_data(@ptrCast(page), GL_AREA_SURFACE) orelse return null, + // 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 surface: *Surface = tab.focus_child; const self = userdataSelf(ud.?); const alloc = self.app.core_app.alloc; @@ -438,9 +455,10 @@ fn gtkNotebookCreateWindow( return null; }; - // We need to update our surface to point to the new window so that + // We need to update our surface to point to the new window and tab so that // events such as new tab go to the right window. surface.window = window; + surface.tab = window.tabs.items[window.tabs.items.len - 1]; return window.notebook; } @@ -460,6 +478,7 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { return true; } + log.debug("WE ARE HERE", .{}); // Setup our basic message const alert = c.gtk_message_dialog_new( self.window, @@ -603,10 +622,10 @@ fn gtkActionToggleInspector( 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 surface: *Surface = @ptrCast(@alignCast( - c.g_object_get_data(@ptrCast(page), GL_AREA_SURFACE) orelse return null, + const tab: *Tab = @ptrCast(@alignCast( + c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null, )); - return &surface.core_surface; + return &tab.focus_child.core_surface; } fn userdataSelf(ud: *anyopaque) *Window {