From 5c0b668517214e062bfcdfee7dc4046b35cf1edc Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Tue, 26 Sep 2023 17:06:45 -0400 Subject: [PATCH 01/74] Begin working on a skeleton for splits in Linux/Gtk --- src/apprt/gtk/Paned.zig | 98 ++++++++++++++++++++++++++++++++++++++++ src/apprt/gtk/Tab.zig | 35 ++++++++++++++ src/apprt/gtk/Window.zig | 1 + 3 files changed, 134 insertions(+) create mode 100644 src/apprt/gtk/Paned.zig create mode 100644 src/apprt/gtk/Tab.zig diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig new file mode 100644 index 000000000..173605337 --- /dev/null +++ b/src/apprt/gtk/Paned.zig @@ -0,0 +1,98 @@ +const Paned = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const font = @import("../../font/main.zig"); +const CoreSurface = @import("../../Surface.zig"); + +const Window = @import("Window.zig"); +const Surface = @import("Surface.zig"); +const Tab = @import("Tab.zig"); +const c = @import("c.zig"); + +const Child = union(enum) { + surface: *Surface, + paned: *Paned, + empty: void, +}; + +const Parent = union(enum) { + tab: *Tab, + paned: *Paned, +} + +// We'll need to keep a reference to the Window this belongs to for various reasons +window: *c.GtkWindow, + +// 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 +label_text: *c.GtkWidget, + +// Our actual GtkPaned widget +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, + +// 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. +parent: Parent, + +pub fn create(alloc: Allocator, window: *Window, label_text: *c.GtkWidget) !*Paned { + var paned = try alloc.create(Paned); + errdefer alloc.destroy(paned); + try paned.init(window, label_text); + return paned; +} + +pub fn init(self: *Paned, window: *Window, label_text: *c.GtkWidget) !void { + self.* = .{ + .window = window, + .label_text = label_text, + .paned = undefined, + .child1 = Child{.empty}, + .child2 = Child{.empty}, + .parent = undefined, + }; + + const paned = c.gtk_paned_new(c.GTK_ORIENTATION_HORIZONTAL); + const gtk_paned: *c.GtkPaned = @ptrCast(paned); + 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; +} + +pub fn newSurface(self: *Paned, 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); + + // Inherit the parent's font size if we are configured to. + const font_size: ?font.face.DesiredSize = font_size: { + if (!self.window.app.config.@"window-inherit-font-size") break :font_size null; + const parent = parent_ orelse break :font_size null; + break :font_size parent.font_size; + }; + + // 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(); + try surface.init(self.window.app, .{ + .window = self, + .gl_area = @ptrCast(gl_area), + .title_label = @ptrCast(label_text), + .font_size = font_size, + }); + return surface; +} diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig new file mode 100644 index 000000000..ac33631f9 --- /dev/null +++ b/src/apprt/gtk/Tab.zig @@ -0,0 +1,35 @@ +const Tab = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const Paned = @import("Paned.zig"); +const Surface = @import("Surface.zig"); +const Window = @import("Window.zig"); +const c = import("c.zig"); + +const Child = union(enum) { + surface: *Surface, + paned: *Paned, +} + +window: *Window, +label_text: *c.GtkLabel, +close_button: *c.GtkButton, +child: Child, + +pub fn create(alloc: Allocator, window: *Window) !*Tab { + var tab = try alloc.create(Tab); + errdefer alloc.destroy(paned); + try tab.init(window); +} + +pub fn init(self: *Tab, window) !void { + self.* = .{ + window = Window, + label_text = undefined, + close_button = undefined, + child = undefined, + }; + // todo - write the function and initialize everything +} \ No newline at end of file diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index e06ea5fbd..81ee174ae 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -11,6 +11,7 @@ const font = @import("../../font/main.zig"); const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); +const Paned = @import("Paned.zig"); const Surface = @import("Surface.zig"); const icon = @import("icon.zig"); const c = @import("c.zig"); From 1b16c2dd232d8defbaa992ebf490baf1d0418b62 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Tue, 26 Sep 2023 19:14:30 -0400 Subject: [PATCH 02/74] Add to Tab.init and fix compilation errors --- src/apprt/gtk/Paned.zig | 4 ++-- src/apprt/gtk/Tab.zig | 46 ++++++++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 173605337..c6f6a600d 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -19,7 +19,7 @@ const Child = union(enum) { const Parent = union(enum) { tab: *Tab, paned: *Paned, -} +}; // We'll need to keep a reference to the Window this belongs to for various reasons window: *c.GtkWindow, @@ -91,7 +91,7 @@ pub fn newSurface(self: *Paned, parent_: ?*CoreSurface) !*Surface { try surface.init(self.window.app, .{ .window = self, .gl_area = @ptrCast(gl_area), - .title_label = @ptrCast(label_text), + .title_label = @ptrCast(self.label_text), .font_size = font_size, }); return surface; diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index ac33631f9..b01212945 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -6,30 +6,56 @@ const Allocator = std.mem.Allocator; const Paned = @import("Paned.zig"); const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); -const c = import("c.zig"); +const c = @import("c.zig"); const Child = union(enum) { surface: *Surface, paned: *Paned, -} +}; 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, +// The child can be either a Surface if the tab is not split or a Paned child: Child, +// We'll update this every time a Surface gains focus, so that we have it +// when we switch to another Tab. Then when we switch back to this tab, we +// can easily re-focus that terminal. +focus_child: *Surface, pub fn create(alloc: Allocator, window: *Window) !*Tab { var tab = try alloc.create(Tab); - errdefer alloc.destroy(paned); + errdefer alloc.destroy(tab); try tab.init(window); } -pub fn init(self: *Tab, window) !void { +pub fn init(self: *Tab, window: *Window) !void { self.* = .{ - window = Window, - label_text = undefined, - close_button = undefined, - child = undefined, + .window = window, + .label_text = undefined, + .close_button = undefined, + .box = undefined, + .child = undefined, + .focus_child = undefined, }; - // todo - write the function and initialize everything -} \ No newline at end of file + + // 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_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_box_append(label_box, label_close_widget); + self.close_button = label_close; + const box_widget = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); + const box: *c.GtkBox = @ptrCast(box_widget); + self.box = box; + // todo - write the rest of function and initialize a new Surface +} From 5b5c2c9ab27b7b7e1791144ed54e74f03ab14e67 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Thu, 28 Sep 2023 19:09:13 -0400 Subject: [PATCH 03/74] Fill out the rest of `Tab.init()` --- src/apprt/gtk/Tab.zig | 78 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index b01212945..d0cce3650 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -2,7 +2,8 @@ const Tab = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; - +const font = @import("../../font/main.zig"); +const CoreSurface = @import("../../Surface.zig"); const Paned = @import("Paned.zig"); const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); @@ -26,13 +27,13 @@ child: Child, // can easily re-focus that terminal. focus_child: *Surface, -pub fn create(alloc: Allocator, window: *Window) !*Tab { +pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab { var tab = try alloc.create(Tab); errdefer alloc.destroy(tab); - try tab.init(window); + try tab.init(window, _parent); } -pub fn init(self: *Tab, window: *Window) !void { +pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { self.* = .{ .window = window, .label_text = undefined, @@ -42,6 +43,17 @@ pub fn init(self: *Tab, window: *Window) !void { .focus_child = undefined, }; + // 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); + + // Inherit the parent's font size if we are configured to. + const font_size: ?font.face.DesiredSize = font_size: { + if (!window.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 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); @@ -54,8 +66,60 @@ pub fn init(self: *Tab, window: *Window) !void { c.gtk_button_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); + + // 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); + } + const box_widget = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); - const box: *c.GtkBox = @ptrCast(box_widget); - self.box = box; - // todo - write the rest of function and initialize a new Surface + c.gtk_widget_set_hexpand(box_widget, 1); + c.gtk_widget_set_vexpand(box_widget, 1); + self.box = ptrCast(box_widget); + + // 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_hexpand(gl_area, 1); + c.gtk_widget_set_vexpand(gl_area, 1); + try surface.init(self.app, .{ + .window = 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); + 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); + + // 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(box), GHOSTTY_TAB, self); + + // 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. + _ = c.gtk_widget_grab_focus(box_widget); } From aba1b85503b07d1a898c7739dc0ca72c7d914b6b Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Sat, 30 Sep 2023 00:43:17 -0400 Subject: [PATCH 04/74] Add `Parent` and `Child` types for tracking splits; Add methods for adding start and end children in `Paned` widget; --- src/apprt/gtk/Paned.zig | 93 +++++++++++++++++++++++++++++++++++---- src/apprt/gtk/Surface.zig | 8 ++++ src/apprt/gtk/Tab.zig | 15 +++++-- src/apprt/gtk/parent.zig | 16 +++++++ 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 src/apprt/gtk/parent.zig diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index c6f6a600d..88cd21158 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -2,33 +2,40 @@ const Paned = @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 Window = @import("Window.zig"); const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); +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, + } + } }; -const Parent = union(enum) { - tab: *Tab, - paned: *Paned, -}; - -// We'll need to keep a reference to the Window this belongs to for various reasons -window: *c.GtkWindow, +/// We'll need to keep a reference to the Window this belongs to for various reasons +Window: *c.GtkWindow, // 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 label_text: *c.GtkWidget, -// Our actual GtkPaned widget +/// Our actual GtkPaned widget paned: *c.GtkPaned, // We have two children, each of which can be either a Surface, another pane, @@ -96,3 +103,73 @@ 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); + } +} + +fn addChild2Surface(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); + } +} + +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); + } +} + +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 { + assert(!self.child1.is_empty()); + // todo +} + +fn removeChild2(self: *Paned) void { + assert(!self.child1.is_empty()); + // todo +} + +pub fn splitStartPosition(self: *Paned, orientation: c.GtkOrientation) !void { + _ = orientation; + _ = self; + // todo +} + +pub fn splitEndPosition(self: *Paned, orientation: c.GtkOrientation) !void { + _ = orientation; + _ = self; + // todo +} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 66ccb2047..b5572607b 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -12,8 +12,12 @@ const terminal = @import("../../terminal/main.zig"); const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); +const Paned = @import("Paned.zig"); +const Tab = @import("Tab.zig"); const Window = @import("Window.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); +const Position = @import("parent.zig").Position; +const Parent = @import("parent.zig").Parent; const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); @@ -66,6 +70,9 @@ app: *App, /// The window we're part of window: *Window, +/// Our parent widget +parent: Parent, + /// Our GTK area gl_area: *c.GtkGLArea, @@ -154,6 +161,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { self.* = .{ .app = app, .window = opts.window, + .parent = Parent.none, .gl_area = opts.gl_area, .title = if (opts.title_label) |label| .{ .label = label, diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index d0cce3650..7cfad8e63 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -9,6 +9,10 @@ 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"; + const Child = union(enum) { surface: *Surface, paned: *Paned, @@ -30,7 +34,7 @@ focus_child: *Surface, 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); + try tab.init(window, parent_); } pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { @@ -80,7 +84,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { const box_widget = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); c.gtk_widget_set_hexpand(box_widget, 1); c.gtk_widget_set_vexpand(box_widget, 1); - self.box = ptrCast(box_widget); + self.box = @ptrCast(box_widget); // Initialize the GtkGLArea and attach it to our surface. // The surface starts in the "unrealized" state because we have to @@ -114,7 +118,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { } // Set the userdata of the close button so it points to this page. - c.g_object_set_data(@ptrCast(box), GHOSTTY_TAB, self); + 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); @@ -123,3 +127,8 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // creating a window we want to always focus on the widget. _ = c.gtk_widget_grab_focus(box_widget); } + +fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { + _ = ud; + // todo +} diff --git a/src/apprt/gtk/parent.zig b/src/apprt/gtk/parent.zig new file mode 100644 index 000000000..7c4539fa0 --- /dev/null +++ b/src/apprt/gtk/parent.zig @@ -0,0 +1,16 @@ +const Paned = @import("Paned.zig"); +const Tab = @import("Tab.zig"); + +pub const Position = enum { + start, + end, +}; + +pub const Parent = union(enum) { + none: void, + tab: *Tab, + paned: struct { + *Paned, + Position, + }, +}; From d88898fc61176c36fbf928a97a21068b076d2be5 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 19 Oct 2023 13:04:15 +0200 Subject: [PATCH 05/74] 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 { From 2d7a81c0dbb76c050fcff5a51f71f0ec074cd820 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 19 Oct 2023 16:15:57 +0200 Subject: [PATCH 06/74] gtk: cleanup code in *Paned --- src/apprt/gtk/Paned.zig | 38 +++++++++++-------------------------- src/apprt/gtk/Tab.zig | 28 +++++---------------------- src/apprt/gtk/Window.zig | 41 ++++++++++++++++++++-------------------- 3 files changed, 37 insertions(+), 70 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 8cf3d79ce..7d377031e 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -44,8 +44,8 @@ pub fn init(self: *Paned, window: *Window, label_text: *c.GtkWidget) !void { .window = window, .label_text = label_text, .paned = undefined, - .child1 = .empty, - .child2 = .empty, + .child1 = .none, + .child2 = .none, .parent = undefined, }; @@ -53,14 +53,6 @@ pub fn init(self: *Paned, window: *Window, label_text: *c.GtkWidget) !void { const gtk_paned: *c.GtkPaned = @ptrCast(paned); 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; } pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { @@ -87,7 +79,7 @@ pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { .tab = tab, .parent = .{ .paned = .{ self, - Position.end, + .end, } }, .gl_area = @ptrCast(gl_area), .title_label = @ptrCast(self.label_text), @@ -97,36 +89,28 @@ pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { } pub fn removeChildren(self: *Paned) void { + assert(self.child1 != .none); + assert(self.child2 != .none); + self.child1 = .none; + self.child2 = .none; c.gtk_paned_set_start_child(@ptrCast(self.paned), null); c.gtk_paned_set_end_child(@ptrCast(self.paned), null); } pub fn addChild1Surface(self: *Paned, surface: *Surface) void { - assert(self.child1.is_empty()); + assert(self.child1 == .none); self.child1 = Tab.Child{ .surface = surface }; - surface.setParent(Parent{ .paned = .{ self, Position.start } }); + surface.setParent(Parent{ .paned = .{ self, .start } }); c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); } pub fn addChild2Surface(self: *Paned, surface: *Surface) void { - assert(self.child2.is_empty()); + assert(self.child2 == .none); self.child2 = Tab.Child{ .surface = surface }; - surface.setParent(Parent{ .paned = .{ self, Position.end } }); + surface.setParent(Parent{ .paned = .{ self, .end } }); c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); } -pub fn removeChild1(self: *Paned) void { - assert(!self.child1.is_empty()); - self.child1 = .empty; - c.gtk_paned_set_start_child(@ptrCast(self.paned), null); -} - -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 { _ = orientation; _ = self; diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 3855a60d5..165eef38a 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -18,17 +18,7 @@ pub const GHOSTTY_TAB = "ghostty_tab"; 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, - } - } + none, }; window: *Window, @@ -163,10 +153,10 @@ pub fn removeChild(self: *Tab) void { 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, + .none => return, }; c.gtk_box_remove(self.box, widget); - self.child = .empty; + self.child = .none; } pub fn setChild(self: *Tab, newChild: Child) void { @@ -183,23 +173,15 @@ pub fn setChild(self: *Tab, newChild: Child) void { const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(paned.paned))); c.gtk_box_append(self.box, widget); }, - .empty => return, + .none => 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 { const tab: *Tab = @ptrCast(@alignCast(ud)); _ = tab; + // TODO: Fix tab closing logic log.info("tab close click\n", .{}); } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index c6a91bd90..5b4fab1af 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -195,13 +195,26 @@ pub fn deinit(self: *Window) void { /// Add a new tab to this window. 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); + const alloc = self.app.core_app.alloc; + const tab = try Tab.create(alloc, self, parentSurface); + try self.tabs.append(alloc, tab); - 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). + // does not (cursor doesn't blink) unless reactivated by refocusing. +} + +pub fn removeTab(self: *Window, tab: *Tab) !void { + // 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 return error.TabNotFound; + + // Deallocate the tab + self.app.core_app.alloc.destroy(tab); } /// Close the tab for the given notebook page. This will automatically @@ -215,20 +228,10 @@ fn closeTab(self: *Window, page: *c.GtkNotebookPage) void { )); // 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}); + self.removeTab(tab) catch |err| { + log.warn("tab {} not removable: {}", .{ page_idx, err }); 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); @@ -293,8 +296,7 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { defer c.g_object_unref(sibling_object); // Remove children and kill Paned. - paned.removeChild1(); - paned.removeChild2(); + paned.removeChildren(); defer alloc.destroy(paned); // Remove children from Paned we were part of. @@ -478,7 +480,6 @@ 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, From 601eed24c4808ed7d2dced37ac2466b795cd1fce Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 19 Oct 2023 16:17:34 +0200 Subject: [PATCH 07/74] gtk: remove code comments and dead code --- src/apprt/gtk/Window.zig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 5b4fab1af..c581816b2 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -299,15 +299,12 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { paned.removeChildren(); 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]}); @@ -316,8 +313,6 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { log.info("paned has no parent", .{}); }, } - - // alloc.destroy(paned); }, .none => { log.info("no parent, dude?!", .{}); From 98a5016598f0ce4eee2c0f65f224a40cf63374ff Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 20 Oct 2023 07:03:41 +0200 Subject: [PATCH 08/74] gtk: simplify code when creating new split --- src/apprt/gtk/Surface.zig | 42 +++++++++++++++------------------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 78fd4fad3..805378923 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -347,46 +347,36 @@ pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFulls } pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { - log.info("surface.newSplit. direction={}", .{direction}); + log.debug("new split, direction: {}", .{direction}); switch (self.parent) { - .none => { - log.info("no parent\n", .{}); - }, + .none => return, .paned => { - log.info("parent is paned \n", .{}); + // TODO: Implement this + log.info("parent is paned", .{}); }, .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, + const label_text: *c.GtkWidget = switch (self.title) { + .none => return, .label => |label| l: { const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(label))); break :l widget; }, }; - if (label_text) |text| { - tab.removeChild(); + tab.removeChild(); - const paned = try Paned.create(self.app.core_app.alloc, self.window, text); + const paned = try Paned.create(self.app.core_app.alloc, self.window, label_text); + const new_surface = try paned.newSurface(tab, &self.core_surface); + // This sets .parent on each surface + paned.addChild1Surface(self); + paned.addChild2Surface(new_surface); - 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 }); - 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}); - } + // FOCUS ON NEW SURFACE + const widget = @as(*c.GtkWidget, @ptrCast(new_surface.gl_area)); + _ = c.gtk_widget_grab_focus(widget); }, } } From 8afcce666a94de5691e43b5a77bc5b57a6a85133 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 20 Oct 2023 09:16:11 +0200 Subject: [PATCH 09/74] gtk: wire up close-tab button --- src/apprt/gtk/Tab.zig | 5 ++--- src/apprt/gtk/Window.zig | 20 +++++++------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 165eef38a..2fde5b373 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -181,7 +181,6 @@ pub fn setChild(self: *Tab, newChild: Child) void { fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { const tab: *Tab = @ptrCast(@alignCast(ud)); - _ = tab; - // TODO: Fix tab closing logic - log.info("tab close click\n", .{}); + const window = tab.window; + window.closeTab(tab); } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index c581816b2..321880914 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -219,13 +219,11 @@ pub fn removeTab(self: *Window, tab: *Tab) !void { /// 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 { +pub fn closeTab(self: *Window, tab: *Tab) void { + const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return; + // 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. self.removeTab(tab) catch |err| { @@ -233,7 +231,6 @@ fn closeTab(self: *Window, page: *c.GtkNotebookPage) void { return; }; - // Now remove the page c.gtk_notebook_remove_page(self.notebook, page_idx); const remaining = c.gtk_notebook_get_n_pages(self.notebook); @@ -258,17 +255,11 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { 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); - }, + .tab => |tab| self.closeTab(tab), .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) { @@ -295,6 +286,9 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { _ = c.g_object_ref(sibling_object); defer c.g_object_unref(sibling_object); + // Remove reference on the surface we're closing + surface.setParent(.none); + // Remove children and kill Paned. paned.removeChildren(); defer alloc.destroy(paned); From a50fc7bc50b38686c38ea8731ef727e35ed4e990 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 20 Oct 2023 12:31:06 +0200 Subject: [PATCH 10/74] gtk: use `unreachable` in switch statements --- src/apprt/gtk/Window.zig | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 321880914..30f037593 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -255,6 +255,7 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { const alloc = surface.app.core_app.alloc; switch (surface.parent) { + .none => unreachable, .tab => |tab| self.closeTab(tab), .paned => |paned_tuple| { const paned = paned_tuple[0]; @@ -294,6 +295,7 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { defer alloc.destroy(paned); switch (paned.parent) { + .none => unreachable, .tab => |tab| { // If parent of Paned we belong to is a tab, we can // replace the child with the other surface @@ -303,14 +305,8 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { .paned => |paned_paned| { log.info("paned is nested, parent is paned. position={}", .{paned_paned[1]}); }, - .none => { - log.info("paned has no parent", .{}); - }, } }, - .none => { - log.info("no parent, dude?!", .{}); - }, } } From 2ed841145b2359fbf5fbd8eb5de0869b3675db4f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sun, 22 Oct 2023 15:32:00 +0200 Subject: [PATCH 11/74] gtk: take direction into account when creating a new split --- src/apprt/gtk/Paned.zig | 14 ++++++++++---- src/apprt/gtk/Surface.zig | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 7d377031e..af548b349 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -4,6 +4,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const font = @import("../../font/main.zig"); +const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); const Window = @import("Window.zig"); @@ -32,14 +33,14 @@ child2: Tab.Child, // maximize the parent pane, or close the tab. parent: Parent, -pub fn create(alloc: Allocator, window: *Window, label_text: *c.GtkWidget) !*Paned { +pub fn create(alloc: Allocator, window: *Window, direction: input.SplitDirection, label_text: *c.GtkWidget) !*Paned { var paned = try alloc.create(Paned); errdefer alloc.destroy(paned); - try paned.init(window, label_text); + try paned.init(window, direction, label_text); return paned; } -pub fn init(self: *Paned, window: *Window, label_text: *c.GtkWidget) !void { +pub fn init(self: *Paned, window: *Window, direction: input.SplitDirection, label_text: *c.GtkWidget) !void { self.* = .{ .window = window, .label_text = label_text, @@ -49,7 +50,12 @@ pub fn init(self: *Paned, window: *Window, label_text: *c.GtkWidget) !void { .parent = undefined, }; - const paned = c.gtk_paned_new(c.GTK_ORIENTATION_HORIZONTAL); + const orientation: c_uint = switch (direction) { + .right => c.GTK_ORIENTATION_HORIZONTAL, + .down => c.GTK_ORIENTATION_VERTICAL, + }; + + const paned = c.gtk_paned_new(orientation); const gtk_paned: *c.GtkPaned = @ptrCast(paned); errdefer c.gtk_widget_destroy(paned); self.paned = gtk_paned; diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 805378923..98f49f6c7 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -366,7 +366,7 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { tab.removeChild(); - const paned = try Paned.create(self.app.core_app.alloc, self.window, label_text); + const paned = try Paned.create(self.app.core_app.alloc, self.window, direction, label_text); const new_surface = try paned.newSurface(tab, &self.core_surface); // This sets .parent on each surface paned.addChild1Surface(self); From 5e789bf1522f90a24883e5ae2a3961f44c31916a Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 24 Oct 2023 06:33:04 +0200 Subject: [PATCH 12/74] gtk: allow splitting when already split --- src/apprt/gtk/Paned.zig | 37 ++++++++++++++++++++++- src/apprt/gtk/Surface.zig | 63 ++++++++++++++++++++++++++++----------- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index af548b349..1c51f7cd8 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -56,8 +56,9 @@ pub fn init(self: *Paned, window: *Window, direction: input.SplitDirection, labe }; const paned = c.gtk_paned_new(orientation); - const gtk_paned: *c.GtkPaned = @ptrCast(paned); errdefer c.gtk_widget_destroy(paned); + + const gtk_paned: *c.GtkPaned = @ptrCast(paned); self.paned = gtk_paned; } @@ -80,6 +81,7 @@ pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { 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, .tab = tab, @@ -91,9 +93,13 @@ pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { .title_label = @ptrCast(self.label_text), .font_size = font_size, }); + return surface; } +pub fn setParent(self: *Paned, parent: Parent) void { + self.parent = parent; +} pub fn removeChildren(self: *Paned) void { assert(self.child1 != .none); assert(self.child2 != .none); @@ -103,6 +109,21 @@ pub fn removeChildren(self: *Paned) void { c.gtk_paned_set_end_child(@ptrCast(self.paned), null); } +pub fn removeChildInPosition(self: *Paned, position: Position) void { + switch (position) { + .start => { + assert(self.child1 != .none); + self.child1 = .none; + c.gtk_paned_set_start_child(@ptrCast(self.paned), null); + }, + .end => { + assert(self.child2 != .none); + self.child2 = .none; + c.gtk_paned_set_end_child(@ptrCast(self.paned), null); + }, + } +} + pub fn addChild1Surface(self: *Paned, surface: *Surface) void { assert(self.child1 == .none); self.child1 = Tab.Child{ .surface = surface }; @@ -117,6 +138,20 @@ pub fn addChild2Surface(self: *Paned, surface: *Surface) void { c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); } +pub fn addChild1Paned(self: *Paned, paned: *Paned) void { + assert(self.child1 == .none); + self.child1 = Tab.Child{ .paned = paned }; + paned.setParent(Parent{ .paned = .{ self, .start } }); + c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); +} + +pub fn addChild2Paned(self: *Paned, paned: *Paned) void { + assert(self.child2 == .none); + self.child2 = Tab.Child{ .paned = paned }; + paned.setParent(Parent{ .paned = .{ self, .end } }); + c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); +} + pub fn splitStartPosition(self: *Paned, orientation: c.GtkOrientation) !void { _ = orientation; _ = self; diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 98f49f6c7..e63e6855f 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -349,11 +349,49 @@ pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFulls pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { log.debug("new split, direction: {}", .{direction}); + // TODO: Refactor all of this! + switch (self.parent) { .none => return, - .paned => { - // TODO: Implement this - log.info("parent is paned", .{}); + .paned => |parent_paned_tuple| { + const label_text: *c.GtkWidget = switch (self.title) { + .none => return, + .label => |label| l: { + const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(label))); + break :l widget; + }, + }; + + // Keep explicit reference to our gl_area before we remove ourselves. + const sibling_object: *c.GObject = @ptrCast(self.gl_area); + _ = c.g_object_ref(sibling_object); + defer c.g_object_unref(sibling_object); + + const parent_paned = parent_paned_tuple[0]; + const parent_position = parent_paned_tuple[1]; + + // Keep position of divider + const parent_paned_position_before = c.gtk_paned_get_position(parent_paned.paned); + // Now remove ourselves from parent + parent_paned.removeChildInPosition(parent_position); + + // Create new sibling + const paned = try Paned.create(self.app.core_app.alloc, self.window, direction, label_text); + const new_surface = try paned.newSurface(self.tab, &self.core_surface); + // This sets .parent on each surface + paned.addChild1Surface(self); + paned.addChild2Surface(new_surface); + + // Add new split-paned + switch (parent_position) { + .start => parent_paned.addChild1Paned(paned), + .end => parent_paned.addChild2Paned(paned), + } + // Restore position + c.gtk_paned_set_position(parent_paned.paned, parent_paned_position_before); + // Focus on new surface + const widget = @as(*c.GtkWidget, @ptrCast(new_surface.gl_area)); + _ = c.gtk_widget_grab_focus(widget); }, .tab => |tab| { const label_text: *c.GtkWidget = switch (self.title) { @@ -374,7 +412,7 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { tab.setChild(.{ .paned = paned }); - // FOCUS ON NEW SURFACE + // Focus on new surface const widget = @as(*c.GtkWidget, @ptrCast(new_surface.gl_area)); _ = c.gtk_widget_grab_focus(widget); }, @@ -793,20 +831,9 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { log.debug("gl destroy", .{}); const self = userdataSelf(ud.?); - 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", .{}); - }, - } + const alloc = self.app.core_app.alloc; + self.deinit(); + alloc.destroy(self); } /// Scale x/y by the GDK device scale. From be836bc777fd76724e6ed204fe6bda8b5175fdd3 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 24 Oct 2023 06:45:06 +0200 Subject: [PATCH 13/74] gtk: allow closing split panes that have been split --- src/apprt/gtk/Window.zig | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 30f037593..1c97fb774 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -302,10 +302,27 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { tab.removeChild(); tab.setChild(.{ .surface = sibling_surface }); }, - .paned => |paned_paned| { - log.info("paned is nested, parent is paned. position={}", .{paned_paned[1]}); + .paned => |parent_paned_tuple| { + const parent_paned = parent_paned_tuple[0]; + const parent_paned_position = parent_paned_tuple[1]; + + // Keep position of divider + const parent_paned_position_before = c.gtk_paned_get_position(parent_paned.paned); + + parent_paned.removeChildInPosition(parent_paned_position); + + switch (parent_paned_position) { + .start => parent_paned.addChild1Surface(sibling_surface), + .end => parent_paned.addChild2Surface(sibling_surface), + } + + // Restore position + c.gtk_paned_set_position(parent_paned.paned, parent_paned_position_before); }, } + + const widget = @as(*c.GtkWidget, @ptrCast(sibling_widget)); + _ = c.gtk_widget_grab_focus(widget); }, } } From 19f7b37bb393c122a4a66bf40a19b7b23a1bfd69 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 24 Oct 2023 06:54:41 +0200 Subject: [PATCH 14/74] gtk: move Child/Parent/Position into same file --- src/apprt/gtk/Paned.zig | 17 +++++++++-------- src/apprt/gtk/Surface.zig | 3 +-- src/apprt/gtk/Tab.zig | 9 ++------- src/apprt/gtk/{parent.zig => relation.zig} | 9 ++++++++- 4 files changed, 20 insertions(+), 18 deletions(-) rename src/apprt/gtk/{parent.zig => relation.zig} (62%) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 1c51f7cd8..70905d225 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -10,8 +10,9 @@ const CoreSurface = @import("../../Surface.zig"); const Window = @import("Window.zig"); const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); -const Position = @import("parent.zig").Position; -const Parent = @import("parent.zig").Parent; +const Position = @import("relation.zig").Position; +const Parent = @import("relation.zig").Parent; +const Child = @import("relation.zig").Child; const c = @import("c.zig"); /// We'll need to keep a reference to the Window this belongs to for various reasons @@ -26,8 +27,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: Tab.Child, -child2: Tab.Child, +child1: Child, +child2: 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. @@ -126,28 +127,28 @@ pub fn removeChildInPosition(self: *Paned, position: Position) void { pub fn addChild1Surface(self: *Paned, surface: *Surface) void { assert(self.child1 == .none); - self.child1 = Tab.Child{ .surface = surface }; + self.child1 = Child{ .surface = surface }; surface.setParent(Parent{ .paned = .{ self, .start } }); c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); } pub fn addChild2Surface(self: *Paned, surface: *Surface) void { assert(self.child2 == .none); - self.child2 = Tab.Child{ .surface = surface }; + self.child2 = Child{ .surface = surface }; surface.setParent(Parent{ .paned = .{ self, .end } }); c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); } pub fn addChild1Paned(self: *Paned, paned: *Paned) void { assert(self.child1 == .none); - self.child1 = Tab.Child{ .paned = paned }; + self.child1 = Child{ .paned = paned }; paned.setParent(Parent{ .paned = .{ self, .start } }); c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); } pub fn addChild2Paned(self: *Paned, paned: *Paned) void { assert(self.child2 == .none); - self.child2 = Tab.Child{ .paned = paned }; + self.child2 = Child{ .paned = paned }; paned.setParent(Parent{ .paned = .{ self, .end } }); c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index e63e6855f..532bba67c 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -16,8 +16,7 @@ const Paned = @import("Paned.zig"); const Tab = @import("Tab.zig"); const Window = @import("Window.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); -const Position = @import("parent.zig").Position; -const Parent = @import("parent.zig").Parent; +const Parent = @import("relation.zig").Parent; const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 2fde5b373..e076ebee2 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -6,7 +6,8 @@ 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 Parent = @import("relation.zig").Parent; +const Child = @import("relation.zig").Child; const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const c = @import("c.zig"); @@ -15,12 +16,6 @@ const log = std.log.scoped(.gtk); pub const GHOSTTY_TAB = "ghostty_tab"; -pub const Child = union(enum) { - surface: *Surface, - paned: *Paned, - none, -}; - window: *Window, label_text: *c.GtkLabel, close_button: *c.GtkButton, diff --git a/src/apprt/gtk/parent.zig b/src/apprt/gtk/relation.zig similarity index 62% rename from src/apprt/gtk/parent.zig rename to src/apprt/gtk/relation.zig index 7c4539fa0..8d0e8b5cf 100644 --- a/src/apprt/gtk/parent.zig +++ b/src/apprt/gtk/relation.zig @@ -1,3 +1,4 @@ +const Surface = @import("Surface.zig"); const Paned = @import("Paned.zig"); const Tab = @import("Tab.zig"); @@ -7,10 +8,16 @@ pub const Position = enum { }; pub const Parent = union(enum) { - none: void, + none, tab: *Tab, paned: struct { *Paned, Position, }, }; + +pub const Child = union(enum) { + none, + surface: *Surface, + paned: *Paned, +}; From 142a2f4cb026684f53373816aca6510460a8167e Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 24 Oct 2023 07:13:46 +0200 Subject: [PATCH 15/74] gtk: refactor how Paned is created --- src/apprt/gtk/Paned.zig | 59 ++++++++++++++++++++++++--------------- src/apprt/gtk/Surface.zig | 48 ++++++++++--------------------- 2 files changed, 50 insertions(+), 57 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 70905d225..7d978217a 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -15,6 +15,8 @@ const Parent = @import("relation.zig").Parent; const Child = @import("relation.zig").Child; const c = @import("c.zig"); +const log = std.log.scoped(.gtk); + /// We'll need to keep a reference to the Window this belongs to for various reasons window: *Window, @@ -34,22 +36,28 @@ child2: Child, // maximize the parent pane, or close the tab. parent: Parent, -pub fn create(alloc: Allocator, window: *Window, direction: input.SplitDirection, label_text: *c.GtkWidget) !*Paned { +pub fn create(alloc: Allocator, window: *Window, sibling: *Surface, direction: input.SplitDirection) !*Paned { var paned = try alloc.create(Paned); errdefer alloc.destroy(paned); - try paned.init(window, direction, label_text); + try paned.init(window, sibling, direction); return paned; } -pub fn init(self: *Paned, window: *Window, direction: input.SplitDirection, label_text: *c.GtkWidget) !void { +pub fn init(self: *Paned, window: *Window, sibling: *Surface, direction: input.SplitDirection) !void { self.* = .{ .window = window, - .label_text = label_text, + .label_text = undefined, .paned = undefined, .child1 = .none, .child2 = .none, .parent = undefined, }; + errdefer self.* = undefined; + + self.label_text = sibling.getTitleLabel() orelse { + log.warn("sibling surface has no title label", .{}); + return; + }; const orientation: c_uint = switch (direction) { .right => c.GTK_ORIENTATION_HORIZONTAL, @@ -57,10 +65,15 @@ pub fn init(self: *Paned, window: *Window, direction: input.SplitDirection, labe }; const paned = c.gtk_paned_new(orientation); - errdefer c.gtk_widget_destroy(paned); + errdefer c.g_object_unref(paned); const gtk_paned: *c.GtkPaned = @ptrCast(paned); self.paned = gtk_paned; + + const new_surface = try self.newSurface(sibling.tab, &sibling.core_surface); + // This sets .parent on each surface + self.addChild1Surface(sibling); + self.addChild2Surface(new_surface); } pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { @@ -98,16 +111,28 @@ pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { return surface; } +pub fn focusSurfaceInPosition(self: *Paned, position: Position) void { + const child = switch (position) { + .start => self.child1, + .end => self.child2, + }; + + const surface = switch (child) { + .surface => |surface| surface, + else => return, + }; + + const widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); + _ = c.gtk_widget_grab_focus(widget); +} + pub fn setParent(self: *Paned, parent: Parent) void { self.parent = parent; } + pub fn removeChildren(self: *Paned) void { - assert(self.child1 != .none); - assert(self.child2 != .none); - self.child1 = .none; - self.child2 = .none; - c.gtk_paned_set_start_child(@ptrCast(self.paned), null); - c.gtk_paned_set_end_child(@ptrCast(self.paned), null); + self.removeChildInPosition(.start); + self.removeChildInPosition(.end); } pub fn removeChildInPosition(self: *Paned, position: Position) void { @@ -152,15 +177,3 @@ pub fn addChild2Paned(self: *Paned, paned: *Paned) void { paned.setParent(Parent{ .paned = .{ self, .end } }); c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); } - -pub fn splitStartPosition(self: *Paned, orientation: c.GtkOrientation) !void { - _ = orientation; - _ = self; - // todo -} - -pub fn splitEndPosition(self: *Paned, orientation: c.GtkOrientation) !void { - _ = orientation; - _ = self; - // todo -} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 532bba67c..26f866a1b 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -345,22 +345,22 @@ pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFulls self.window.toggleFullscreen(mac_non_native); } +pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { + switch (self.title) { + .none => return null, + .label => |label| { + const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(label))); + return widget; + }, + } +} + pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { log.debug("new split, direction: {}", .{direction}); - // TODO: Refactor all of this! - switch (self.parent) { .none => return, .paned => |parent_paned_tuple| { - const label_text: *c.GtkWidget = switch (self.title) { - .none => return, - .label => |label| l: { - const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(label))); - break :l widget; - }, - }; - // Keep explicit reference to our gl_area before we remove ourselves. const sibling_object: *c.GObject = @ptrCast(self.gl_area); _ = c.g_object_ref(sibling_object); @@ -374,12 +374,7 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { // Now remove ourselves from parent parent_paned.removeChildInPosition(parent_position); - // Create new sibling - const paned = try Paned.create(self.app.core_app.alloc, self.window, direction, label_text); - const new_surface = try paned.newSurface(self.tab, &self.core_surface); - // This sets .parent on each surface - paned.addChild1Surface(self); - paned.addChild2Surface(new_surface); + const paned = try Paned.create(self.app.core_app.alloc, self.window, self, direction); // Add new split-paned switch (parent_position) { @@ -389,31 +384,16 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { // Restore position c.gtk_paned_set_position(parent_paned.paned, parent_paned_position_before); // Focus on new surface - const widget = @as(*c.GtkWidget, @ptrCast(new_surface.gl_area)); - _ = c.gtk_widget_grab_focus(widget); + paned.focusSurfaceInPosition(.end); }, .tab => |tab| { - const label_text: *c.GtkWidget = switch (self.title) { - .none => return, - .label => |label| l: { - const widget = @as(*c.GtkWidget, @ptrCast(@alignCast(label))); - break :l widget; - }, - }; - tab.removeChild(); - const paned = try Paned.create(self.app.core_app.alloc, self.window, direction, label_text); - const new_surface = try paned.newSurface(tab, &self.core_surface); - // This sets .parent on each surface - paned.addChild1Surface(self); - paned.addChild2Surface(new_surface); - + const paned = try Paned.create(self.app.core_app.alloc, self.window, self, direction); tab.setChild(.{ .paned = paned }); // Focus on new surface - const widget = @as(*c.GtkWidget, @ptrCast(new_surface.gl_area)); - _ = c.gtk_widget_grab_focus(widget); + paned.focusSurfaceInPosition(.end); }, } } From 0add9de0eba7151357943d763b6b1427cddf612d Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 25 Oct 2023 07:02:47 +0200 Subject: [PATCH 16/74] gtk: handle closing of surfaces with sibling being a Paned --- src/apprt/gtk/Paned.zig | 54 +++++++++++++++++++++++---------------- src/apprt/gtk/Surface.zig | 4 +-- src/apprt/gtk/Window.zig | 18 +++++-------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 7d978217a..35beb2a88 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -72,8 +72,8 @@ pub fn init(self: *Paned, window: *Window, sibling: *Surface, direction: input.S const new_surface = try self.newSurface(sibling.tab, &sibling.core_surface); // This sets .parent on each surface - self.addChild1Surface(sibling); - self.addChild2Surface(new_surface); + self.addChild1(.{ .surface = sibling }); + self.addChild2(.{ .surface = new_surface }); } pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { @@ -150,30 +150,40 @@ pub fn removeChildInPosition(self: *Paned, position: Position) void { } } -pub fn addChild1Surface(self: *Paned, surface: *Surface) void { +pub fn addChild1(self: *Paned, child: Child) void { assert(self.child1 == .none); - self.child1 = Child{ .surface = surface }; - surface.setParent(Parent{ .paned = .{ self, .start } }); - c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); + + const parent = Parent{ .paned = .{ self, .start } }; + self.child1 = child; + + switch (child) { + .none => return, + .paned => |paned| { + paned.setParent(parent); + c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); + }, + .surface => |surface| { + surface.setParent(parent); + c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); + }, + } } -pub fn addChild2Surface(self: *Paned, surface: *Surface) void { +pub fn addChild2(self: *Paned, child: Child) void { assert(self.child2 == .none); - self.child2 = Child{ .surface = surface }; - surface.setParent(Parent{ .paned = .{ self, .end } }); - c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); -} -pub fn addChild1Paned(self: *Paned, paned: *Paned) void { - assert(self.child1 == .none); - self.child1 = Child{ .paned = paned }; - paned.setParent(Parent{ .paned = .{ self, .start } }); - c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); -} + const parent = Parent{ .paned = .{ self, .end } }; + self.child2 = child; -pub fn addChild2Paned(self: *Paned, paned: *Paned) void { - assert(self.child2 == .none); - self.child2 = Child{ .paned = paned }; - paned.setParent(Parent{ .paned = .{ self, .end } }); - c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); + switch (child) { + .none => return, + .paned => |paned| { + paned.setParent(parent); + c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); + }, + .surface => |surface| { + surface.setParent(parent); + c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); + }, + } } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 26f866a1b..ba7b99275 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -378,8 +378,8 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { // Add new split-paned switch (parent_position) { - .start => parent_paned.addChild1Paned(paned), - .end => parent_paned.addChild2Paned(paned), + .start => parent_paned.addChild1(.{ .paned = paned }), + .end => parent_paned.addChild2(.{ .paned = paned }), } // Restore position c.gtk_paned_set_position(parent_paned.paned, parent_paned_position_before); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 1c97fb774..22bf3abf0 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -263,22 +263,16 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { const sibling = switch (position) { .start => .{ - switch (paned.child2) { - .surface => |s| s, - else => return, - }, + paned.child2, c.gtk_paned_get_end_child(paned.paned), }, .end => .{ - switch (paned.child1) { - .surface => |s| s, - else => return, - }, + paned.child1, 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_child = sibling[0]; const sibling_widget = sibling[1]; // Keep explicit reference to sibling's gl_area, so it's not @@ -300,7 +294,7 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { // 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 }); + tab.setChild(sibling_child); }, .paned => |parent_paned_tuple| { const parent_paned = parent_paned_tuple[0]; @@ -312,8 +306,8 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { parent_paned.removeChildInPosition(parent_paned_position); switch (parent_paned_position) { - .start => parent_paned.addChild1Surface(sibling_surface), - .end => parent_paned.addChild2Surface(sibling_surface), + .start => parent_paned.addChild1(sibling_child), + .end => parent_paned.addChild2(sibling_child), } // Restore position From de7cbb602f0bcf46735de23e8618ec8cc5a57e5f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 25 Oct 2023 07:10:41 +0200 Subject: [PATCH 17/74] gtk: refactor replacing child of Paned --- src/apprt/gtk/Paned.zig | 15 +++++++++++++++ src/apprt/gtk/Window.zig | 13 +------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 35beb2a88..1f10b1cc4 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -130,6 +130,21 @@ pub fn setParent(self: *Paned, parent: Parent) void { self.parent = parent; } +pub fn replaceChildInPosition(self: *Paned, child: Child, position: Position) void { + // Keep position of divider + const parent_paned_position_before = c.gtk_paned_get_position(self.paned); + + self.removeChildInPosition(position); + + switch (position) { + .start => self.addChild1(child), + .end => self.addChild2(child), + } + + // Restore position + c.gtk_paned_set_position(self.paned, parent_paned_position_before); +} + pub fn removeChildren(self: *Paned) void { self.removeChildInPosition(.start); self.removeChildInPosition(.end); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 22bf3abf0..2f496e1c4 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -300,18 +300,7 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { const parent_paned = parent_paned_tuple[0]; const parent_paned_position = parent_paned_tuple[1]; - // Keep position of divider - const parent_paned_position_before = c.gtk_paned_get_position(parent_paned.paned); - - parent_paned.removeChildInPosition(parent_paned_position); - - switch (parent_paned_position) { - .start => parent_paned.addChild1(sibling_child), - .end => parent_paned.addChild2(sibling_child), - } - - // Restore position - c.gtk_paned_set_position(parent_paned.paned, parent_paned_position_before); + parent_paned.replaceChildInPosition(sibling_child, parent_paned_position); }, } From 9ddf097a03352b679584f872dbe7939dbce3796f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 26 Oct 2023 06:31:27 +0200 Subject: [PATCH 18/74] gtk: Refactor how a new split is created --- src/apprt/gtk/Paned.zig | 41 ++++++++++++++++++++++++++++++++++++--- src/apprt/gtk/Surface.zig | 26 +++---------------------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 1f10b1cc4..5bd9a1159 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -70,10 +70,9 @@ pub fn init(self: *Paned, window: *Window, sibling: *Surface, direction: input.S const gtk_paned: *c.GtkPaned = @ptrCast(paned); self.paned = gtk_paned; - const new_surface = try self.newSurface(sibling.tab, &sibling.core_surface); - // This sets .parent on each surface + const surface = try self.newSurface(sibling.tab, &sibling.core_surface); self.addChild1(.{ .surface = sibling }); - self.addChild2(.{ .surface = new_surface }); + self.addChild2(.{ .surface = surface }); } pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { @@ -130,6 +129,42 @@ pub fn setParent(self: *Paned, parent: Parent) void { self.parent = parent; } +pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input.SplitDirection) !void { + const child = switch (position) { + .start => self.child1, + .end => self.child2, + }; + + const surface: *Surface = switch (child) { + .surface => |surface| surface, + else => return, + }; + + // Keep explicit reference to surface gl_area before we remove it. + const object: *c.GObject = @ptrCast(surface.gl_area); + _ = c.g_object_ref(object); + defer c.g_object_unref(object); + + // Keep position of divider + const parent_paned_position_before = c.gtk_paned_get_position(self.paned); + // Now remove it + self.removeChildInPosition(position); + + // Create new Paned + // NOTE: We cannot use `replaceChildInPosition` here because we need to + // first remove the surface before we create a new pane. + const paned = try Paned.create(self.window.app.core_app.alloc, self.window, surface, direction); + switch (position) { + .start => self.addChild1(.{ .paned = paned }), + .end => self.addChild2(.{ .paned = paned }), + } + // Restore position + c.gtk_paned_set_position(self.paned, parent_paned_position_before); + + // Focus on new surface + paned.focusSurfaceInPosition(.end); +} + pub fn replaceChildInPosition(self: *Paned, child: Child, position: Position) void { // Keep position of divider const parent_paned_position_before = c.gtk_paned_get_position(self.paned); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index ba7b99275..b84ddc898 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -361,30 +361,10 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { switch (self.parent) { .none => return, .paned => |parent_paned_tuple| { - // Keep explicit reference to our gl_area before we remove ourselves. - const sibling_object: *c.GObject = @ptrCast(self.gl_area); - _ = c.g_object_ref(sibling_object); - defer c.g_object_unref(sibling_object); + const paned = parent_paned_tuple[0]; + const position = parent_paned_tuple[1]; - const parent_paned = parent_paned_tuple[0]; - const parent_position = parent_paned_tuple[1]; - - // Keep position of divider - const parent_paned_position_before = c.gtk_paned_get_position(parent_paned.paned); - // Now remove ourselves from parent - parent_paned.removeChildInPosition(parent_position); - - const paned = try Paned.create(self.app.core_app.alloc, self.window, self, direction); - - // Add new split-paned - switch (parent_position) { - .start => parent_paned.addChild1(.{ .paned = paned }), - .end => parent_paned.addChild2(.{ .paned = paned }), - } - // Restore position - c.gtk_paned_set_position(parent_paned.paned, parent_paned_position_before); - // Focus on new surface - paned.focusSurfaceInPosition(.end); + try paned.splitSurfaceInPosition(position, direction); }, .tab => |tab| { tab.removeChild(); From 790cd84203c33f7898bcc109a0a566ab497875fa Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 26 Oct 2023 06:47:48 +0200 Subject: [PATCH 19/74] gtk: refator Parent/Tab/Paned and how they interact --- src/apprt/gtk/Paned.zig | 40 +++++++++++--------------------------- src/apprt/gtk/Tab.zig | 30 +++++++--------------------- src/apprt/gtk/relation.zig | 17 ++++++++++++++++ 3 files changed, 35 insertions(+), 52 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 5bd9a1159..29d9a0cd5 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -185,7 +185,7 @@ pub fn removeChildren(self: *Paned) void { self.removeChildInPosition(.end); } -pub fn removeChildInPosition(self: *Paned, position: Position) void { +fn removeChildInPosition(self: *Paned, position: Position) void { switch (position) { .start => { assert(self.child1 != .none); @@ -200,40 +200,22 @@ pub fn removeChildInPosition(self: *Paned, position: Position) void { } } -pub fn addChild1(self: *Paned, child: Child) void { +fn addChild1(self: *Paned, child: Child) void { assert(self.child1 == .none); - const parent = Parent{ .paned = .{ self, .start } }; - self.child1 = child; + const widget = child.widget() orelse return; + c.gtk_paned_set_start_child(@ptrCast(self.paned), widget); - switch (child) { - .none => return, - .paned => |paned| { - paned.setParent(parent); - c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); - }, - .surface => |surface| { - surface.setParent(parent); - c.gtk_paned_set_start_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); - }, - } + self.child1 = child; + child.setParent(.{ .paned = .{ self, .start } }); } -pub fn addChild2(self: *Paned, child: Child) void { +fn addChild2(self: *Paned, child: Child) void { assert(self.child2 == .none); - const parent = Parent{ .paned = .{ self, .end } }; - self.child2 = child; + const widget = child.widget() orelse return; + c.gtk_paned_set_end_child(@ptrCast(self.paned), widget); - switch (child) { - .none => return, - .paned => |paned| { - paned.setParent(parent); - c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(@alignCast(paned.paned))); - }, - .surface => |surface| { - surface.setParent(parent); - c.gtk_paned_set_end_child(@ptrCast(self.paned), @ptrCast(surface.gl_area)); - }, - } + self.child2 = child; + child.setParent(.{ .paned = .{ self, .end } }); } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index e076ebee2..c77819805 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -144,34 +144,18 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { } 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))), - .none => return, - }; + const widget = self.child.widget() orelse return; c.gtk_box_remove(self.box, widget); + self.child = .none; } -pub fn setChild(self: *Tab, newChild: Child) void { - const parent = Parent{ .tab = self }; +pub fn setChild(self: *Tab, child: Child) void { + const widget = child.widget() orelse return; + c.gtk_box_append(self.box, widget); - 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); - }, - .none => return, - } - - self.child = newChild; + child.setParent(.{ .tab = self }); + self.child = child; } fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { diff --git a/src/apprt/gtk/relation.zig b/src/apprt/gtk/relation.zig index 8d0e8b5cf..b14b0b86e 100644 --- a/src/apprt/gtk/relation.zig +++ b/src/apprt/gtk/relation.zig @@ -1,6 +1,7 @@ const Surface = @import("Surface.zig"); const Paned = @import("Paned.zig"); const Tab = @import("Tab.zig"); +const c = @import("c.zig"); pub const Position = enum { start, @@ -20,4 +21,20 @@ pub const Child = union(enum) { none, surface: *Surface, paned: *Paned, + + pub fn setParent(self: Child, parent: Parent) void { + switch (self) { + .none => return, + .surface => |surface| surface.setParent(parent), + .paned => |paned| paned.setParent(parent), + } + } + + pub fn widget(self: Child) ?*c.GtkWidget { + return switch (self) { + .none => null, + .paned => |paned| @ptrCast(@alignCast(paned.paned)), + .surface => |surface| @ptrCast(surface.gl_area), + }; + } }; From a7717289eb181b03d439226969a5f8d79a799f3b Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 26 Oct 2023 20:22:38 +0200 Subject: [PATCH 20/74] gtk: fix tab settings after introducing *Tab --- src/apprt/gtk/Tab.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index c77819805..a1a554359 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -123,8 +123,8 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { } // Tab settings - c.gtk_notebook_set_tab_reorderable(window.notebook, gl_area, 1); - c.gtk_notebook_set_tab_detachable(window.notebook, gl_area, 1); + c.gtk_notebook_set_tab_reorderable(window.notebook, box_widget, 1); + c.gtk_notebook_set_tab_detachable(window.notebook, box_widget, 1); // If we have multiple tabs, show the tab bar. if (c.gtk_notebook_get_n_pages(window.notebook) > 1) { From dc0f6e3a5b54f6750f475594c2d805b8795096cd Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 26 Oct 2023 20:24:32 +0200 Subject: [PATCH 21/74] WIP: gtk: handle split surfaces when closing tab/window --- src/apprt/gtk/Paned.zig | 18 ++++++++++++++++++ src/apprt/gtk/Tab.zig | 14 ++++++++++++++ src/apprt/gtk/Window.zig | 6 +++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 29d9a0cd5..65a592043 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -219,3 +219,21 @@ fn addChild2(self: *Paned, child: Child) void { self.child2 = child; child.setParent(.{ .paned = .{ self, .end } }); } + +pub fn deinit(self: *Paned, alloc: Allocator) void { + switch (self.child1) { + .none, .surface => {}, + .paned => |paned| { + paned.deinit(alloc); + alloc.destroy(paned); + }, + } + + switch (self.child2) { + .none, .surface => {}, + .paned => |paned| { + paned.deinit(alloc); + alloc.destroy(paned); + }, + } +} diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index a1a554359..b4b752370 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -163,3 +163,17 @@ fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { const window = tab.window; window.closeTab(tab); } + +pub fn close(self: *Tab) void { + switch (self.child) { + .none => return, + .surface => { + // TODO: I'm not 100% but I don't think we have to do something + return; + }, + .paned => |paned| { + paned.deinit(self.window.app.core_app.alloc); + self.window.app.core_app.alloc.destroy(paned); + }, + } +} diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 2f496e1c4..94cfe1186 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -189,7 +189,10 @@ 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); + for (self.tabs.items) |tab| { + tab.close(); + self.app.core_app.alloc.destroy(tab); + } self.tabs.deinit(self.app.core_app.alloc); } @@ -214,6 +217,7 @@ pub fn removeTab(self: *Window, tab: *Tab) !void { if (tab_idx) |idx| _ = self.tabs.orderedRemove(idx) else return error.TabNotFound; // Deallocate the tab + tab.close(); self.app.core_app.alloc.destroy(tab); } From aaa6ff8884a3e0494dedb3225c78f5a54b7f5cbd Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 26 Oct 2023 06:52:47 +0200 Subject: [PATCH 22/74] gtk: introduce helper method on Paned --- src/apprt/gtk/Paned.zig | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 65a592043..5e8ccec39 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -110,35 +110,18 @@ pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { return surface; } -pub fn focusSurfaceInPosition(self: *Paned, position: Position) void { - const child = switch (position) { - .start => self.child1, - .end => self.child2, - }; - - const surface = switch (child) { - .surface => |surface| surface, - else => return, - }; - - const widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); - _ = c.gtk_widget_grab_focus(widget); -} - pub fn setParent(self: *Paned, parent: Parent) void { self.parent = parent; } -pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input.SplitDirection) !void { - const child = switch (position) { - .start => self.child1, - .end => self.child2, - }; +pub fn focusSurfaceInPosition(self: *Paned, position: Position) void { + const surface: *Surface = self.surfaceInPosition(position) orelse return; + const widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); + _ = c.gtk_widget_grab_focus(widget); +} - const surface: *Surface = switch (child) { - .surface => |surface| surface, - else => return, - }; +pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input.SplitDirection) !void { + const surface: *Surface = self.surfaceInPosition(position) orelse return; // Keep explicit reference to surface gl_area before we remove it. const object: *c.GObject = @ptrCast(surface.gl_area); @@ -237,3 +220,15 @@ pub fn deinit(self: *Paned, alloc: Allocator) void { }, } } + +fn surfaceInPosition(self: *Paned, position: Position) ?*Surface { + const child = switch (position) { + .start => self.child1, + .end => self.child2, + }; + + return switch (child) { + .surface => |surface| surface, + else => null, + }; +} From 77b8e3c95672bb68ac9035399d0ed12cd16c0c33 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 27 Oct 2023 06:08:49 +0200 Subject: [PATCH 23/74] gtk: move helper function around --- src/apprt/gtk/Paned.zig | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 5e8ccec39..731513e0d 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -203,6 +203,18 @@ fn addChild2(self: *Paned, child: Child) void { child.setParent(.{ .paned = .{ self, .end } }); } +fn surfaceInPosition(self: *Paned, position: Position) ?*Surface { + const child = switch (position) { + .start => self.child1, + .end => self.child2, + }; + + return switch (child) { + .surface => |surface| surface, + else => null, + }; +} + pub fn deinit(self: *Paned, alloc: Allocator) void { switch (self.child1) { .none, .surface => {}, @@ -220,15 +232,3 @@ pub fn deinit(self: *Paned, alloc: Allocator) void { }, } } - -fn surfaceInPosition(self: *Paned, position: Position) ?*Surface { - const child = switch (position) { - .start => self.child1, - .end => self.child2, - }; - - return switch (child) { - .surface => |surface| surface, - else => null, - }; -} From d88c29377b26e39b2f0a845b0e174e4c2060ac7f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 27 Oct 2023 06:09:05 +0200 Subject: [PATCH 24/74] gtk: use deinit method on Paned consistently --- 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 94cfe1186..5cffb46b7 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -256,7 +256,7 @@ pub fn closeTab(self: *Window, tab: *Tab) void { pub fn closeSurface(self: *Window, surface: *Surface) void { assert(surface.window == self); - const alloc = surface.app.core_app.alloc; + const alloc = self.app.core_app.alloc; switch (surface.parent) { .none => unreachable, @@ -288,8 +288,9 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { // Remove reference on the surface we're closing surface.setParent(.none); - // Remove children and kill Paned. + // Remove children. paned.removeChildren(); + // Don't need to call paned.deinit, because we already removed children. defer alloc.destroy(paned); switch (paned.parent) { From adba77c3f9d5375942f09ce2b7b7d68734cb2f4a Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 27 Oct 2023 06:34:57 +0200 Subject: [PATCH 25/74] gtk: fix warning when replacing children of Paned --- src/apprt/gtk/Paned.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 731513e0d..455158fdc 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -152,6 +152,10 @@ pub fn replaceChildInPosition(self: *Paned, child: Child, position: Position) vo // Keep position of divider const parent_paned_position_before = c.gtk_paned_get_position(self.paned); + // Focus on the sibling, otherwise we'll get a GTK warning + self.focusSurfaceInPosition(if (position == .start) .end else .start); + + // Now we can remove the other one self.removeChildInPosition(position); switch (position) { From 34e4261210e9dbe6bdb0fdfc45ef46e769e55c07 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 27 Oct 2023 06:56:31 +0200 Subject: [PATCH 26/74] gtk: refactor the Paned deinit method --- src/apprt/gtk/Paned.zig | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 455158fdc..e94a494fc 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -220,19 +220,13 @@ fn surfaceInPosition(self: *Paned, position: Position) ?*Surface { } pub fn deinit(self: *Paned, alloc: Allocator) void { - switch (self.child1) { - .none, .surface => {}, - .paned => |paned| { - paned.deinit(alloc); - alloc.destroy(paned); - }, - } - - switch (self.child2) { - .none, .surface => {}, - .paned => |paned| { - paned.deinit(alloc); - alloc.destroy(paned); - }, + for ([_]Child{ self.child1, self.child2 }) |child| { + switch (child) { + .none, .surface => continue, + .paned => |paned| { + paned.deinit(alloc); + alloc.destroy(paned); + }, + } } } From fde6289880036e802bec9de76363686411d8ce08 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 28 Oct 2023 07:34:30 +0200 Subject: [PATCH 27/74] gtk: rename and refactor Tab.deinit --- src/apprt/gtk/Tab.zig | 8 ++------ src/apprt/gtk/Window.zig | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index b4b752370..cf604c792 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -164,13 +164,9 @@ fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { window.closeTab(tab); } -pub fn close(self: *Tab) void { +pub fn deinit(self: *Tab) void { switch (self.child) { - .none => return, - .surface => { - // TODO: I'm not 100% but I don't think we have to do something - return; - }, + .none, .surface => return, .paned => |paned| { paned.deinit(self.window.app.core_app.alloc); self.window.app.core_app.alloc.destroy(paned); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 5cffb46b7..9e3a28937 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -190,7 +190,7 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { self.icon.deinit(self.app); for (self.tabs.items) |tab| { - tab.close(); + tab.deinit(); self.app.core_app.alloc.destroy(tab); } self.tabs.deinit(self.app.core_app.alloc); @@ -217,7 +217,7 @@ pub fn removeTab(self: *Window, tab: *Tab) !void { if (tab_idx) |idx| _ = self.tabs.orderedRemove(idx) else return error.TabNotFound; // Deallocate the tab - tab.close(); + tab.deinit(); self.app.core_app.alloc.destroy(tab); } From eed2bb30781b1593549a849bd4eeeab63b084a71 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 28 Oct 2023 07:51:30 +0200 Subject: [PATCH 28/74] gtk: refactor code and add comments to Paned --- src/apprt/gtk/Paned.zig | 32 ++++++++----- src/apprt/gtk/Tab.zig | 12 ++--- src/apprt/gtk/Window.zig | 100 ++++++++++++++++++++------------------- 3 files changed, 76 insertions(+), 68 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index e94a494fc..977ffc3dd 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -75,7 +75,7 @@ pub fn init(self: *Paned, window: *Window, sibling: *Surface, direction: input.S self.addChild2(.{ .surface = surface }); } -pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { +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); @@ -110,16 +110,19 @@ pub fn newSurface(self: *Paned, tab: *Tab, parent_: ?*CoreSurface) !*Surface { return surface; } +/// Set the parent of Paned. pub fn setParent(self: *Paned, parent: Parent) void { self.parent = parent; } +/// Focus on the Surface's gl_area in the given position. pub fn focusSurfaceInPosition(self: *Paned, position: Position) void { const surface: *Surface = self.surfaceInPosition(position) orelse return; const widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); _ = c.gtk_widget_grab_focus(widget); } +/// Split the Surface in the given position into a Paned with two surfaces. pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input.SplitDirection) !void { const surface: *Surface = self.surfaceInPosition(position) orelse return; @@ -148,6 +151,7 @@ pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input paned.focusSurfaceInPosition(.end); } +/// Replace the existing .start or .end Child with the given new Child. pub fn replaceChildInPosition(self: *Paned, child: Child, position: Position) void { // Keep position of divider const parent_paned_position_before = c.gtk_paned_get_position(self.paned); @@ -167,11 +171,25 @@ pub fn replaceChildInPosition(self: *Paned, child: Child, position: Position) vo c.gtk_paned_set_position(self.paned, parent_paned_position_before); } +/// Remove both children, setting *c.GtkPaned start/end children to null. pub fn removeChildren(self: *Paned) void { self.removeChildInPosition(.start); self.removeChildInPosition(.end); } +/// Deinit the Paned by deiniting its child Paneds, if they exist. +pub fn deinit(self: *Paned, alloc: Allocator) void { + for ([_]Child{ self.child1, self.child2 }) |child| { + switch (child) { + .none, .surface => continue, + .paned => |paned| { + paned.deinit(alloc); + alloc.destroy(paned); + }, + } + } +} + fn removeChildInPosition(self: *Paned, position: Position) void { switch (position) { .start => { @@ -218,15 +236,3 @@ fn surfaceInPosition(self: *Paned, position: Position) ?*Surface { else => null, }; } - -pub fn deinit(self: *Paned, alloc: Allocator) void { - for ([_]Child{ self.child1, self.child2 }) |child| { - switch (child) { - .none, .surface => continue, - .paned => |paned| { - paned.deinit(alloc); - alloc.destroy(paned); - }, - } - } -} diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index cf604c792..cdd5e2b45 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -158,12 +158,6 @@ pub fn setChild(self: *Tab, child: Child) void { self.child = child; } -fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { - const tab: *Tab = @ptrCast(@alignCast(ud)); - const window = tab.window; - window.closeTab(tab); -} - pub fn deinit(self: *Tab) void { switch (self.child) { .none, .surface => return, @@ -173,3 +167,9 @@ pub fn deinit(self: *Tab) void { }, } } + +fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { + const tab: *Tab = @ptrCast(@alignCast(ud)); + const window = tab.window; + window.closeTab(tab); +} diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 9e3a28937..ae5efda43 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -15,6 +15,7 @@ const App = @import("App.zig"); const Paned = @import("Paned.zig"); const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); +const Position = @import("relation.zig").Position; const icon = @import("icon.zig"); const c = @import("c.zig"); @@ -213,7 +214,6 @@ pub fn removeTab(self: *Window, tab: *Tab) !void { if (t == tab) break i; } else null; - // TODO: Shrink capacity? if (tab_idx) |idx| _ = self.tabs.orderedRemove(idx) else return error.TabNotFound; // Deallocate the tab @@ -256,8 +256,6 @@ pub fn closeTab(self: *Window, tab: *Tab) void { pub fn closeSurface(self: *Window, surface: *Surface) void { assert(surface.window == self); - const alloc = self.app.core_app.alloc; - switch (surface.parent) { .none => unreachable, .tab => |tab| self.closeTab(tab), @@ -265,56 +263,60 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { const paned = paned_tuple[0]; const position = paned_tuple[1]; - const sibling = switch (position) { - .start => .{ - paned.child2, - c.gtk_paned_get_end_child(paned.paned), - }, - .end => .{ - paned.child1, - c.gtk_paned_get_start_child(paned.paned), - }, - }; - // TODO: Use destructuring syntax once it doesn't break ZLS - const sibling_child = 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 reference on the surface we're closing - surface.setParent(.none); - - // Remove children. - paned.removeChildren(); - // Don't need to call paned.deinit, because we already removed children. - defer alloc.destroy(paned); - - switch (paned.parent) { - .none => unreachable, - .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(sibling_child); - }, - .paned => |parent_paned_tuple| { - const parent_paned = parent_paned_tuple[0]; - const parent_paned_position = parent_paned_tuple[1]; - - parent_paned.replaceChildInPosition(sibling_child, parent_paned_position); - }, - } - - const widget = @as(*c.GtkWidget, @ptrCast(sibling_widget)); - _ = c.gtk_widget_grab_focus(widget); + self.closeSurfaceInPaned(surface, paned, position); }, } } +fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position: Position) void { + const sibling = switch (position) { + .start => .{ + paned.child2, + c.gtk_paned_get_end_child(paned.paned), + }, + .end => .{ + paned.child1, + c.gtk_paned_get_start_child(paned.paned), + }, + }; + // TODO: Use destructuring syntax once it doesn't break ZLS + const sibling_child = 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 reference on the surface we're closing + surface.setParent(.none); + + // Remove children. + paned.removeChildren(); + // Don't need to call paned.deinit, because we already removed children. + defer self.app.core_app.alloc.destroy(paned); + + switch (paned.parent) { + .none => unreachable, + .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(sibling_child); + }, + .paned => |parent_paned_tuple| { + const parent_paned = parent_paned_tuple[0]; + const parent_paned_position = parent_paned_tuple[1]; + + parent_paned.replaceChildInPosition(sibling_child, parent_paned_position); + }, + } + + const widget = @as(*c.GtkWidget, @ptrCast(sibling_widget)); + _ = c.gtk_widget_grab_focus(widget); +} + /// 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; From 633a3e2e8551f5044a02e3d8ad1722ffaf752799 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 28 Oct 2023 08:09:43 +0200 Subject: [PATCH 29/74] gtk: remove duplication of new surface creation --- src/apprt/gtk/Paned.zig | 39 ++---------------- src/apprt/gtk/Tab.zig | 87 ++++++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 71 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 977ffc3dd..10e7dc355 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -70,46 +70,13 @@ pub fn init(self: *Paned, window: *Window, sibling: *Surface, direction: input.S const gtk_paned: *c.GtkPaned = @ptrCast(paned); self.paned = gtk_paned; - const surface = try self.newSurface(sibling.tab, &sibling.core_surface); + const surface = try sibling.tab.newSurface(&sibling.core_surface); + surface.setParent(.{ .paned = .{ self, .end } }); + self.addChild1(.{ .surface = sibling }); self.addChild2(.{ .surface = surface }); } -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); - - // Inherit the parent's font size if we are configured to. - const font_size: ?font.face.DesiredSize = font_size: { - if (!self.window.app.config.@"window-inherit-font-size") break :font_size null; - const parent = parent_ orelse break :font_size null; - break :font_size parent.font_size; - }; - - // 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_hexpand(gl_area, 1); - c.gtk_widget_set_vexpand(gl_area, 1); - - try surface.init(self.window.app, .{ - .window = self.window, - .tab = tab, - .parent = .{ .paned = .{ - self, - .end, - } }, - .gl_area = @ptrCast(gl_area), - .title_label = @ptrCast(self.label_text), - .font_size = font_size, - }); - - return surface; -} - /// Set the parent of Paned. pub fn setParent(self: *Paned, parent: Parent) void { self.parent = parent; diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index cdd5e2b45..86a7deadf 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -46,27 +46,14 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { .focus_child = undefined, }; - // Grab a surface allocation we'll need it later. - 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: { - if (!window.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 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); - self.label_text = label_text; c.gtk_box_append(label_box, label_text_widget); + self.label_text = label_text; + 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); @@ -93,29 +80,24 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { c.gtk_widget_set_size_request(@ptrCast(label_text), 100, 1); } + // Create a Box in which we'll later keep either Surface or Paned const box_widget = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); c.gtk_widget_set_hexpand(box_widget, 1); c.gtk_widget_set_vexpand(box_widget, 1); self.box = @ptrCast(box_widget); - // 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_hexpand(gl_area, 1); - c.gtk_widget_set_vexpand(gl_area, 1); - 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, - }); + // Create the Surface + const surface = try self.newSurface(parent_); errdefer surface.deinit(); - c.gtk_box_append(self.box, gl_area); + self.child = Child{ .surface = surface }; + // // TODO: this needs to change + self.focus_child = surface; + + // Add Surface to the Tab + const gl_area_widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); + c.gtk_box_append(self.box, gl_area_widget); + 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", .{}); @@ -137,10 +119,45 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // Switch to the new tab 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. - const widget = @as(*c.GtkWidget, @ptrCast(gl_area)); - _ = c.gtk_widget_grab_focus(widget); + // 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. + _ = c.gtk_widget_grab_focus(gl_area_widget); +} + +/// 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 { + // 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); + + // Inherit the parent's font size if we are configured to. + const font_size: ?font.face.DesiredSize = font_size: { + if (!self.window.app.config.@"window-inherit-font-size") break :font_size null; + const parent = parent_ orelse break :font_size null; + break :font_size parent.font_size; + }; + + // 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_hexpand(gl_area, 1); + c.gtk_widget_set_vexpand(gl_area, 1); + + try surface.init(self.window.app, .{ + .window = self.window, + .tab = self, + .parent = .{ + .tab = self, + }, + .gl_area = @ptrCast(gl_area), + .title_label = @ptrCast(self.label_text), + .font_size = font_size, + }); + + return surface; } pub fn removeChild(self: *Tab) void { From 8c58bf222d44da9c09d96006cdbb93e2282db600 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 28 Oct 2023 08:21:17 +0200 Subject: [PATCH 30/74] gtk: move logic of splitting surface in a tab to Tab --- src/apprt/gtk/Surface.zig | 10 ++-------- src/apprt/gtk/Tab.zig | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index b84ddc898..2e1e81d0e 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -356,7 +356,7 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { } pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { - log.debug("new split, direction: {}", .{direction}); + log.debug("splitting surface, direction: {}", .{direction}); switch (self.parent) { .none => return, @@ -367,13 +367,7 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { try paned.splitSurfaceInPosition(position, direction); }, .tab => |tab| { - tab.removeChild(); - - const paned = try Paned.create(self.app.core_app.alloc, self.window, self, direction); - tab.setChild(.{ .paned = paned }); - - // Focus on new surface - paned.focusSurfaceInPosition(.end); + try tab.splitSurface(direction); }, } } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 86a7deadf..315db2ffd 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -4,7 +4,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const font = @import("../../font/main.zig"); +const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); + const Paned = @import("Paned.zig"); const Parent = @import("relation.zig").Parent; const Child = @import("relation.zig").Child; @@ -160,6 +162,28 @@ pub fn newSurface(self: *Tab, parent_: ?*CoreSurface) !*Surface { return surface; } +/// Splits the current child surface into a Paned in given direction. Child of +/// Tab must be a Surface. +pub fn splitSurface(self: *Tab, direction: input.SplitDirection) !void { + assert(self.child == .surface); + + const surface = switch (self.child) { + .surface => |s| s, + else => unreachable, + }; + self.removeChild(); + + // Create a Paned with two Surfaces. + const paned = try Paned.create(self.window.app.core_app.alloc, self.window, surface, direction); + + // Add Paned to the Tab. + self.setChild(.{ .paned = paned }); + + // Focus on new surface + paned.focusSurfaceInPosition(.end); +} + +/// Remove the current child from the Tab. Noop if no child set. pub fn removeChild(self: *Tab) void { const widget = self.child.widget() orelse return; c.gtk_box_remove(self.box, widget); @@ -167,6 +191,7 @@ pub fn removeChild(self: *Tab) void { self.child = .none; } +/// Sets child to given child and sets parent on child. pub fn setChild(self: *Tab, child: Child) void { const widget = child.widget() orelse return; c.gtk_box_append(self.box, widget); @@ -175,6 +200,7 @@ 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, From ab93579bf6f8d0199c3b8d46d8e6ab695b783f63 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 31 Oct 2023 20:38:17 +0100 Subject: [PATCH 31/74] gtk: port new-tab behavior over after rebase and refactor This is the new behavior from #776. --- src/apprt/gtk/Tab.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 315db2ffd..5ded58f4c 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -100,7 +100,14 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { const gl_area_widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); c.gtk_box_append(self.box, gl_area_widget); - const page_idx = c.gtk_notebook_append_page(window.notebook, box_widget, label_box_widget); + // 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( + window.notebook, + box_widget, + label_box_widget, + c.gtk_notebook_get_current_page(window.notebook) + 1, + ); if (page_idx < 0) { log.warn("failed to add page to notebook", .{}); return error.GtkAppendPageFailed; From a1fb74d8aa3dccf2f03fdc3ffacf693cf92a6937 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 31 Oct 2023 20:57:01 +0100 Subject: [PATCH 32/74] gtk: rework naming conflict after rebase --- src/apprt/gtk/Surface.zig | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 2e1e81d0e..59a9df4bb 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -46,6 +46,13 @@ 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. + parentSurface: bool = false, }; /// Where the title of this surface will go. @@ -59,6 +66,9 @@ const Title = union(enum) { /// surface has been initialized. realized: bool = false, +/// See Options.parentSurface +parentSurface: bool = false, + /// The app we're part of app: *App, @@ -167,6 +177,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { } else .{ .none = {} }, .core_surface = undefined, .font_size = opts.font_size, + .parentSurface = opts.parentSurface, .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .im_context = im_context, @@ -215,8 +226,8 @@ fn realize(self: *Surface) !void { // Get our new surface config var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config); defer config.deinit(); - if (!self.parent) { - // A hack, see the "parent" field for more information. + if (!self.parentSurface) { + // A hack, see the "parentSurface" field for more information. config.@"working-directory" = self.app.config.@"working-directory"; } From f1e81563d96376b03e8e19163aa79aa801793baf Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 1 Nov 2023 06:28:50 +0100 Subject: [PATCH 33/74] gtk: always remember focused surface before grabbing focus --- src/apprt/gtk/Paned.zig | 1 + src/apprt/gtk/Surface.zig | 1 + src/apprt/gtk/Tab.zig | 5 +++-- src/apprt/gtk/Window.zig | 4 ++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 10e7dc355..852409e12 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -86,6 +86,7 @@ pub fn setParent(self: *Paned, parent: Parent) void { pub fn focusSurfaceInPosition(self: *Paned, position: Position) void { const surface: *Surface = self.surfaceInPosition(position) orelse return; const widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); + surface.tab.focus_child = surface; _ = c.gtk_widget_grab_focus(widget); } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 59a9df4bb..3969200f1 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -836,6 +836,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; _ = c.gtk_widget_grab_focus(gl_widget); } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 5ded58f4c..69093b65b 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -93,8 +93,6 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { errdefer surface.deinit(); self.child = Child{ .surface = surface }; - // // TODO: this needs to change - self.focus_child = surface; // Add Surface to the Tab const gl_area_widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); @@ -122,6 +120,9 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { c.gtk_notebook_set_show_tabs(window.notebook, 1); } + // TODO: This needs to happen before we show the page + self.focus_child = surface; + // Set the userdata of the box to point to this tab. c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index ae5efda43..b9f880707 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -313,6 +313,10 @@ fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position }, } + switch (sibling_child) { + .surface => |s| s.tab.focus_child = s, + else => {}, + } const widget = @as(*c.GtkWidget, @ptrCast(sibling_widget)); _ = c.gtk_widget_grab_focus(widget); } From b1e368566490382622682c29e570ef68c87354fb Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 1 Nov 2023 06:56:16 +0100 Subject: [PATCH 34/74] gtk: find first surface to focus on if sibling is Paned --- src/apprt/gtk/Paned.zig | 34 +++++++++++++++++++++++----------- src/apprt/gtk/Tab.zig | 2 +- src/apprt/gtk/Window.zig | 16 +++++++++++----- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 852409e12..241902be3 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -82,12 +82,22 @@ pub fn setParent(self: *Paned, parent: Parent) void { self.parent = parent; } -/// Focus on the Surface's gl_area in the given position. -pub fn focusSurfaceInPosition(self: *Paned, position: Position) void { - const surface: *Surface = self.surfaceInPosition(position) orelse return; - const widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); - surface.tab.focus_child = surface; - _ = c.gtk_widget_grab_focus(widget); +/// Focus on first Surface that can be found in given position. If there's a +/// Paned in the position, it will focus on the first surface in that position. +pub fn focusFirstSurfaceInPosition(self: *Paned, position: Position) void { + const child = self.childInPosition(position); + switch (child) { + .surface => |s| { + const widget = @as(*c.GtkWidget, @ptrCast(s.gl_area)); + s.tab.focus_child = s; + _ = c.gtk_widget_grab_focus(widget); + }, + .paned => |p| p.focusFirstSurfaceInPosition(position), + .none => { + log.warn("attempted to focus on first surface, found none", .{}); + return; + }, + } } /// Split the Surface in the given position into a Paned with two surfaces. @@ -116,7 +126,7 @@ pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input c.gtk_paned_set_position(self.paned, parent_paned_position_before); // Focus on new surface - paned.focusSurfaceInPosition(.end); + paned.focusFirstSurfaceInPosition(.end); } /// Replace the existing .start or .end Child with the given new Child. @@ -125,7 +135,7 @@ pub fn replaceChildInPosition(self: *Paned, child: Child, position: Position) vo const parent_paned_position_before = c.gtk_paned_get_position(self.paned); // Focus on the sibling, otherwise we'll get a GTK warning - self.focusSurfaceInPosition(if (position == .start) .end else .start); + self.focusFirstSurfaceInPosition(if (position == .start) .end else .start); // Now we can remove the other one self.removeChildInPosition(position); @@ -193,13 +203,15 @@ fn addChild2(self: *Paned, child: Child) void { child.setParent(.{ .paned = .{ self, .end } }); } -fn surfaceInPosition(self: *Paned, position: Position) ?*Surface { - const child = switch (position) { +fn childInPosition(self: *Paned, position: Position) Child { + return switch (position) { .start => self.child1, .end => self.child2, }; +} - return switch (child) { +fn surfaceInPosition(self: *Paned, position: Position) ?*Surface { + return switch (self.childInPosition(position)) { .surface => |surface| surface, else => null, }; diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 69093b65b..bdeda014d 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -188,7 +188,7 @@ pub fn splitSurface(self: *Tab, direction: input.SplitDirection) !void { self.setChild(.{ .paned = paned }); // Focus on new surface - paned.focusSurfaceInPosition(.end); + paned.focusFirstSurfaceInPosition(.end); } /// Remove the current child from the Tab. Noop if no child set. diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index b9f880707..cb51a8696 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -283,8 +283,8 @@ fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position const sibling_child = 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. + // Keep explicit reference to sibling's widget (gl_area, or Paned), 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); @@ -314,11 +314,17 @@ fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position } switch (sibling_child) { - .surface => |s| s.tab.focus_child = s, + .surface => |s| { + s.tab.focus_child = s; + const widget = @as(*c.GtkWidget, @ptrCast(s.gl_area)); + _ = c.gtk_widget_grab_focus(widget); + }, + .paned => |p| { + // Focus on first surface in sibling Paned + p.focusFirstSurfaceInPosition(position); + }, else => {}, } - const widget = @as(*c.GtkWidget, @ptrCast(sibling_widget)); - _ = c.gtk_widget_grab_focus(widget); } /// Returns true if this window has any tabs. From b60133fd34efa0f2a8540f16a1fbe3e67303565a Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 1 Nov 2023 07:52:24 +0100 Subject: [PATCH 35/74] gtk: keep buffer of surface title, update if focused --- src/apprt/gtk/Paned.zig | 6 +----- src/apprt/gtk/Surface.zig | 45 ++++++++++++++++++++++++++++++--------- src/apprt/gtk/Tab.zig | 5 +---- src/apprt/gtk/Window.zig | 6 +----- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 241902be3..a77e599c8 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -87,11 +87,7 @@ pub fn setParent(self: *Paned, parent: Parent) void { pub fn focusFirstSurfaceInPosition(self: *Paned, position: Position) void { const child = self.childInPosition(position); switch (child) { - .surface => |s| { - const widget = @as(*c.GtkWidget, @ptrCast(s.gl_area)); - s.tab.focus_child = s; - _ = c.gtk_widget_grab_focus(widget); - }, + .surface => |s| s.grabFocus(), .paned => |p| p.focusFirstSurfaceInPosition(position), .none => { log.warn("attempted to focus on first surface, found none", .{}); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 3969200f1..ef212b115 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -90,6 +90,11 @@ cursor: ?*c.GdkCursor = null, /// Our title label (if there is one). title: Title, +/// Our title. The raw value of the title. This will be kept up to date and +/// .title will be updated if we have focus. +/// TODO: what's a big enough value? +titleText: [4096]u8, + /// The core surface backing this surface core_surface: CoreSurface, @@ -175,6 +180,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .title = if (opts.title_label) |label| .{ .label = label, } else .{ .none = {} }, + .titleText = undefined, .core_surface = undefined, .font_size = opts.font_size, .parentSurface = opts.parentSurface, @@ -445,24 +451,38 @@ pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.Surfac _ = max_; } -pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { +pub fn grabFocus(self: *Surface) void { + self.updateTitleLabels(); + self.tab.focus_child = self; + const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); + _ = c.gtk_widget_grab_focus(widget); +} + +fn updateTitleLabels(self: *Surface) void { switch (self.title) { .none => {}, - .label => |label| { - c.gtk_label_set_text(label, slice.ptr); - - const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); - if (c.gtk_widget_is_focus(widget) == 1) { + const slice: []u8 = std.mem.sliceTo(&self.titleText, 0); + // Check whether there's even something in the buffer yet. + // TODO: This check is really bad. + if (slice.len != self.titleText.len) { + c.gtk_label_set_text(label, @as([*c]const u8, @ptrCast(slice))); c.gtk_window_set_title(self.window.window, c.gtk_label_get_text(label)); } }, } +} - // const root = c.gtk_widget_get_root(@ptrCast( - // *c.GtkWidget, - // self.gl_area, - // )); +pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { + // TODO: I'm sure there has to be a better way than this in Zig. + const len = @min(self.titleText.len - 1, slice.len); + @memcpy(self.titleText[0..len], slice[0..]); + self.titleText[len] = 0; + + const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); + if (c.gtk_widget_is_focus(widget) == 1) { + self.updateTitleLabels(); + } } pub fn setParent(self: *Surface, parent: Parent) void { @@ -838,6 +858,10 @@ fn gtkMouseDown( if (c.gtk_widget_has_focus(gl_widget) == 0) { self.tab.focus_child = self; _ = c.gtk_widget_grab_focus(gl_widget); + + // If we have siblings, we also update the title, since it means + // another sibling might have updated the title. + if (self.parent != Parent.tab) self.updateTitleLabels(); } self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| { @@ -1267,6 +1291,7 @@ 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 bdeda014d..6c38cc6bd 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -120,9 +120,6 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { c.gtk_notebook_set_show_tabs(window.notebook, 1); } - // TODO: This needs to happen before we show the page - self.focus_child = surface; - // Set the userdata of the box to point to this tab. c.g_object_set_data(@ptrCast(box_widget), GHOSTTY_TAB, self); @@ -131,7 +128,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // 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. - _ = c.gtk_widget_grab_focus(gl_area_widget); + surface.grabFocus(); } /// Allocates and initializes a new Surface, but doesn't add it to the Tab yet. diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index cb51a8696..58355bf5e 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -314,11 +314,7 @@ fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position } switch (sibling_child) { - .surface => |s| { - s.tab.focus_child = s; - const widget = @as(*c.GtkWidget, @ptrCast(s.gl_area)); - _ = c.gtk_widget_grab_focus(widget); - }, + .surface => |s| s.grabFocus(), .paned => |p| { // Focus on first surface in sibling Paned p.focusFirstSurfaceInPosition(position); From 290b79ad621c3db0d8fecc8e7260dc85616a9dbd Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 1 Nov 2023 08:18:01 +0100 Subject: [PATCH 36/74] gtk: restore "inherit working directory" behavior --- src/apprt/gtk/Tab.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 6c38cc6bd..d4504f96c 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -159,6 +159,7 @@ pub fn newSurface(self: *Tab, parent_: ?*CoreSurface) !*Surface { .parent = .{ .tab = self, }, + .parentSurface = parent_ != null, .gl_area = @ptrCast(gl_area), .title_label = @ptrCast(self.label_text), .font_size = font_size, From 7cad9a253eec04e819c009ac0a074c334b9ceb54 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 1 Nov 2023 08:23:11 +0100 Subject: [PATCH 37/74] gtk: use destructuring syntax --- src/apprt/gtk/Window.zig | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 58355bf5e..a0011ff9f 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -269,7 +269,7 @@ pub fn closeSurface(self: *Window, surface: *Surface) void { } fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position: Position) void { - const sibling = switch (position) { + const sibling_child, const sibling_widget = switch (position) { .start => .{ paned.child2, c.gtk_paned_get_end_child(paned.paned), @@ -279,9 +279,6 @@ fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position c.gtk_paned_get_start_child(paned.paned), }, }; - // TODO: Use destructuring syntax once it doesn't break ZLS - const sibling_child = sibling[0]; - const sibling_widget = sibling[1]; // Keep explicit reference to sibling's widget (gl_area, or Paned), so it's // not destroyed when we remove it from GtkPaned. From 09bbaa82b464b97af5c714972c70791831bbeafd Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 1 Nov 2023 08:23:20 +0100 Subject: [PATCH 38/74] gtk: comment out detach-tab feature after breaking it --- src/apprt/gtk/Window.zig | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index a0011ff9f..eadb1ed8f 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -437,27 +437,31 @@ fn gtkNotebookCreateWindow( page: *c.GtkWidget, ud: ?*anyopaque, ) callconv(.C) ?*c.GtkNotebook { + _ = ud; + _ = page; + log.warn("feature needs to be re-implemented after adding gtk splits", .{}); + 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 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; + // const self = userdataSelf(ud.?); + // const alloc = self.app.core_app.alloc; - // Create a new window - const window = Window.create(alloc, self.app) catch |err| { - log.warn("error creating new window error={}", .{err}); - return null; - }; + // // Create a new window + // const window = Window.create(alloc, self.app) catch |err| { + // log.warn("error creating new window error={}", .{err}); + // return null; + // }; - // 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]; + // // 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; + // return window.notebook; } fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { From ba2992d4d08c32035219f12458a0260d05d207c7 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 1 Nov 2023 09:21:15 +0100 Subject: [PATCH 39/74] gtk: remove fields on Paned we don't need --- src/apprt/gtk/Paned.zig | 22 ++++------------------ src/apprt/gtk/Tab.zig | 2 +- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index a77e599c8..029161a43 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -17,13 +17,6 @@ const c = @import("c.zig"); const log = std.log.scoped(.gtk); -/// We'll need to keep a reference to the Window this belongs to for various reasons -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 -label_text: *c.GtkWidget, - /// Our actual GtkPaned widget paned: *c.GtkPaned, @@ -36,17 +29,15 @@ child2: Child, // maximize the parent pane, or close the tab. parent: Parent, -pub fn create(alloc: Allocator, window: *Window, sibling: *Surface, direction: input.SplitDirection) !*Paned { +pub fn create(alloc: Allocator, sibling: *Surface, direction: input.SplitDirection) !*Paned { var paned = try alloc.create(Paned); errdefer alloc.destroy(paned); - try paned.init(window, sibling, direction); + try paned.init(sibling, direction); return paned; } -pub fn init(self: *Paned, window: *Window, sibling: *Surface, direction: input.SplitDirection) !void { +pub fn init(self: *Paned, sibling: *Surface, direction: input.SplitDirection) !void { self.* = .{ - .window = window, - .label_text = undefined, .paned = undefined, .child1 = .none, .child2 = .none, @@ -54,11 +45,6 @@ pub fn init(self: *Paned, window: *Window, sibling: *Surface, direction: input.S }; errdefer self.* = undefined; - self.label_text = sibling.getTitleLabel() orelse { - log.warn("sibling surface has no title label", .{}); - return; - }; - const orientation: c_uint = switch (direction) { .right => c.GTK_ORIENTATION_HORIZONTAL, .down => c.GTK_ORIENTATION_VERTICAL, @@ -113,7 +99,7 @@ pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input // Create new Paned // NOTE: We cannot use `replaceChildInPosition` here because we need to // first remove the surface before we create a new pane. - const paned = try Paned.create(self.window.app.core_app.alloc, self.window, surface, direction); + const paned = try Paned.create(surface.window.app.core_app.alloc, surface, direction); switch (position) { .start => self.addChild1(.{ .paned = paned }), .end => self.addChild2(.{ .paned = paned }), diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index d4504f96c..2945f42a0 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -180,7 +180,7 @@ pub fn splitSurface(self: *Tab, direction: input.SplitDirection) !void { self.removeChild(); // Create a Paned with two Surfaces. - const paned = try Paned.create(self.window.app.core_app.alloc, self.window, surface, direction); + const paned = try Paned.create(self.window.app.core_app.alloc, surface, direction); // Add Paned to the Tab. self.setChild(.{ .paned = paned }); From 8cfa9297bf73c7b1f1f4a8d8a6c91a45f1cdb0ea Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 1 Nov 2023 19:57:37 +0100 Subject: [PATCH 40/74] gtk: rework surface title buffer handling --- src/apprt/gtk/Surface.zig | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index ef212b115..ad25abd56 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -92,8 +92,11 @@ title: Title, /// Our title. The raw value of the title. This will be kept up to date and /// .title will be updated if we have focus. +/// When set the text in this buf will be null-terminated, because we need to +/// pass it to GTK. /// TODO: what's a big enough value? -titleText: [4096]u8, +title_text_buf: [4096]u8, +title_text_buf_len: u13, /// The core surface backing this surface core_surface: CoreSurface, @@ -180,7 +183,8 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .title = if (opts.title_label) |label| .{ .label = label, } else .{ .none = {} }, - .titleText = undefined, + .title_text_buf = undefined, + .title_text_buf_len = 0, .core_surface = undefined, .font_size = opts.font_size, .parentSurface = opts.parentSurface, @@ -462,22 +466,21 @@ fn updateTitleLabels(self: *Surface) void { switch (self.title) { .none => {}, .label => |label| { - const slice: []u8 = std.mem.sliceTo(&self.titleText, 0); - // Check whether there's even something in the buffer yet. - // TODO: This check is really bad. - if (slice.len != self.titleText.len) { - c.gtk_label_set_text(label, @as([*c]const u8, @ptrCast(slice))); - c.gtk_window_set_title(self.window.window, c.gtk_label_get_text(label)); - } + if (self.title_text_buf_len == 0) return; + + const slice: []u8 = self.title_text_buf[0..self.title_text_buf_len]; + c.gtk_label_set_text(label, @as([*c]const u8, @ptrCast(slice))); + c.gtk_window_set_title(self.window.window, c.gtk_label_get_text(label)); }, } } pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { - // TODO: I'm sure there has to be a better way than this in Zig. - const len = @min(self.titleText.len - 1, slice.len); - @memcpy(self.titleText[0..len], slice[0..]); - self.titleText[len] = 0; + const len = @min(self.title_text_buf.len - 1, slice.len); + @memcpy(self.title_text_buf[0..len], slice[0..]); + // Null-terminate this because we then need to pass it to GTK. + self.title_text_buf[len] = 0; + self.title_text_buf_len = len; const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); if (c.gtk_widget_is_focus(widget) == 1) { From d65a804eddac7a7386c8bb90d4214636eb235db7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Nov 2023 21:12:08 -0700 Subject: [PATCH 41/74] apprt/gtk: working on some reorg --- src/apprt/gtk/Paned.zig | 3 ++- src/apprt/gtk/Surface.zig | 46 ++++++++++++++++++++++++++++++++++----- src/apprt/gtk/Tab.zig | 41 ++++++++++++++++++---------------- src/apprt/gtk/Window.zig | 20 ++++++++++++++--- 4 files changed, 81 insertions(+), 29 deletions(-) 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; From 00c02e88d299604b040a3b83767e2aebf49f0177 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Nov 2023 21:49:24 -0700 Subject: [PATCH 42/74] apprt/gtk: remove Window from Surface, use container only --- src/apprt/gtk/Paned.zig | 2 +- src/apprt/gtk/Surface.zig | 86 +++++++++++++++++++++++++++++---------- src/apprt/gtk/Tab.zig | 1 - src/apprt/gtk/Window.zig | 7 ++-- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 1a41d23af..0905d85e0 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -100,7 +100,7 @@ pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input // Create new Paned // NOTE: We cannot use `replaceChildInPosition` here because we need to // first remove the surface before we create a new pane. - const paned = try Paned.create(surface.window.app.core_app.alloc, surface, direction); + const paned = try Paned.create(surface.app.core_app.alloc, surface, direction); switch (position) { .start => self.addChild1(.{ .paned = paned }), .end => self.addChild2(.{ .paned = paned }), diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 19ee08b7d..84ac81b20 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -21,16 +21,13 @@ const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); -const log = std.log.scoped(.gtk); +const log = std.log.scoped(.gtk_surface); /// This is detected by the OpenGL renderer to move to a single-threaded /// draw operation. This basically puts locks around our draw path. pub const opengl_single_threaded_draw = true; pub const Options = struct { - /// The window that this surface is attached to. - window: *Window, - /// The tab that this surface is attached to. tab: *Tab, @@ -108,9 +105,6 @@ container: Container = .{ .none = {} }, /// The app we're part of app: *App, -/// The window we're part of -window: *Window, - /// The parent we belong to parent: Parent, @@ -209,7 +203,6 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { // Build our result self.* = .{ .app = app, - .window = opts.window, .container = .{ .tab_ = opts.tab }, .parent = opts.parent, .gl_area = opts.gl_area, @@ -329,14 +322,20 @@ pub fn redraw(self: *Surface) void { /// Close this surface. pub fn close(self: *Surface, processActive: bool) void { + // If we are not currently in a window, then we don't need to do any + // cleanup. If we are in a window, we need to potentially confirm, + // remove ourselves from the view hierarchy, etc. + const window = self.container.window() orelse return; + if (!processActive) { - self.window.closeSurface(self); + // TODO: change to container doing this directly + window.closeSurface(self); return; } // Setup our basic message const alert = c.gtk_message_dialog_new( - self.window.window, + window.window, c.GTK_DIALOG_MODAL, c.GTK_MESSAGE_QUESTION, c.GTK_BUTTONS_YES_NO, @@ -396,7 +395,15 @@ pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void { } pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFullscreen) void { - self.window.toggleFullscreen(mac_non_native); + const window = self.container.window() orelse { + log.info( + "toggleFullscreen invalid for container={s}", + .{@tagName(self.container)}, + ); + return; + }; + + window.toggleFullscreen(mac_non_native); } pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { @@ -427,23 +434,53 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { } pub fn newTab(self: *Surface) !void { - try self.window.newTab(&self.core_surface); + const window = self.container.window() orelse { + log.info("surface cannot create new tab when not attached to a window", .{}); + return; + }; + + try window.newTab(&self.core_surface); } pub fn hasTabs(self: *const Surface) bool { - return self.window.hasTabs(); + const window = self.container.window() orelse return false; + return window.hasTabs(); } pub fn gotoPreviousTab(self: *Surface) void { - self.window.gotoPreviousTab(self); + const window = self.container.window() orelse { + log.info( + "gotoPreviousTab invalid for container={s}", + .{@tagName(self.container)}, + ); + return; + }; + + window.gotoPreviousTab(self); } pub fn gotoNextTab(self: *Surface) void { - self.window.gotoNextTab(self); + const window = self.container.window() orelse { + log.info( + "gotoNextTab invalid for container={s}", + .{@tagName(self.container)}, + ); + return; + }; + + window.gotoNextTab(self); } pub fn gotoTab(self: *Surface, n: usize) void { - self.window.gotoTab(n); + const window = self.container.window() orelse { + log.info( + "gotoTab invalid for container={s}", + .{@tagName(self.container)}, + ); + return; + }; + + window.gotoTab(n); } pub fn setShouldClose(self: *Surface) void { @@ -467,10 +504,13 @@ pub fn getSize(self: *const Surface) !apprt.SurfaceSize { } pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void { + // This operation only makes sense if we're within a window view hierarchy. + const window = self.container.window() orelse return; + // Note: this doesn't properly take into account the window decorations. // I'm not currently sure how to do that. c.gtk_window_set_default_size( - @ptrCast(self.window.window), + @ptrCast(window.window), @intCast(width), @intCast(height), ); @@ -504,7 +544,10 @@ fn updateTitleLabels(self: *Surface) void { const slice: []u8 = self.title_text_buf[0..self.title_text_buf_len]; c.gtk_label_set_text(label, @as([*c]const u8, @ptrCast(slice))); - c.gtk_window_set_title(self.window.window, c.gtk_label_get_text(label)); + c.gtk_window_set_title( + self.container.window().?.window, // TODO: messy + c.gtk_label_get_text(label), + ); }, } } @@ -810,8 +853,8 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) }; const window_scale_factor = scale: { - const window = @as(*c.GtkNative, @ptrCast(self.window.window)); - const gdk_surface = c.gtk_native_get_surface(window); + const window = self.container.window() orelse break :scale 0; + const gdk_surface = c.gtk_native_get_surface(@ptrCast(window.window)); break :scale c.gdk_surface_get_scale_factor(gdk_surface); }; @@ -1359,7 +1402,8 @@ fn gtkCloseConfirmation( c.gtk_window_destroy(@ptrCast(alert)); if (response == c.GTK_RESPONSE_YES) { const self = userdataSelf(ud.?); - self.window.closeSurface(self); + const window = self.container.window() orelse return; + window.closeSurface(self); } } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 185a0268f..8ac45a241 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -168,7 +168,6 @@ pub fn newSurface(self: *Tab, parent_: ?*CoreSurface) !*Surface { c.gtk_widget_set_vexpand(gl_area, 1); try surface.init(self.window.app, .{ - .window = self.window, .tab = self, .parent = .{ .tab = self, diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index e3a38b3a3..184628f03 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -258,7 +258,7 @@ pub fn closeTab(self: *Window, tab: *Tab) void { /// Close the surface. This surface must be definitely part of this window. pub fn closeSurface(self: *Window, surface: *Surface) void { - assert(surface.window == self); + assert(surface.container.window().? == self); switch (surface.parent) { .none => unreachable, @@ -485,8 +485,9 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { // If none of our surfaces need confirmation, we can just exit. for (self.app.core_app.surfaces.items) |surface| { - if (surface.window == self) { - if (surface.core_surface.needsConfirmQuit()) break; + if (surface.container.window()) |window| { + if (window == self and + surface.core_surface.needsConfirmQuit()) break; } } else { c.gtk_window_destroy(self.window); From 14570b8a6c51518487e5aaeb37b74cb4e0975abc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Nov 2023 22:05:51 -0700 Subject: [PATCH 43/74] apprt/gtk: get rid of title label option, we can infer it on container --- src/apprt/gtk/Surface.zig | 38 +++++++++++--------------------------- src/apprt/gtk/Tab.zig | 1 - 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 84ac81b20..36daae687 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -37,10 +37,6 @@ pub const Options = struct { /// The GL area that this surface should draw to. gl_area: *c.GtkGLArea, - /// The label to use as the title of this surface. This will be - /// modified with setTitle. - title_label: ?*c.GtkLabel = null, - /// A font size to set on the surface once it is initialized. font_size: ?font.face.DesiredSize = null, @@ -84,12 +80,6 @@ pub const Container = union(enum) { } }; -/// Where the title of this surface will go. -const Title = union(enum) { - none: void, - label: *c.GtkLabel, -}; - /// Whether the surface has been realized or not yet. When a surface is /// "realized" it means that the OpenGL context is ready and the core /// surface has been initialized. @@ -114,9 +104,6 @@ gl_area: *c.GtkGLArea, /// Any active cursor we may have cursor: ?*c.GdkCursor = null, -/// Our title label (if there is one). -title: Title, - /// Our title. The raw value of the title. This will be kept up to date and /// .title will be updated if we have focus. /// When set the text in this buf will be null-terminated, because we need to @@ -206,9 +193,6 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .container = .{ .tab_ = opts.tab }, .parent = opts.parent, .gl_area = opts.gl_area, - .title = if (opts.title_label) |label| .{ - .label = label, - } else .{ .none = {} }, .title_text_buf = undefined, .title_text_buf_len = 0, .core_surface = undefined, @@ -537,18 +521,18 @@ pub fn grabFocus(self: *Surface) void { } fn updateTitleLabels(self: *Surface) void { - switch (self.title) { - .none => {}, - .label => |label| { - if (self.title_text_buf_len == 0) return; + // If we have no title, then we have nothing to update. + if (self.title_text_buf_len == 0) return; + const slice: []u8 = self.title_text_buf[0..self.title_text_buf_len]; - const slice: []u8 = self.title_text_buf[0..self.title_text_buf_len]; - c.gtk_label_set_text(label, @as([*c]const u8, @ptrCast(slice))); - c.gtk_window_set_title( - self.container.window().?.window, // TODO: messy - c.gtk_label_get_text(label), - ); - }, + // If we have a tab, then we have to update the tab + if (self.container.tab()) |tab| { + c.gtk_label_set_text(tab.label_text, slice.ptr); + } + + // If we have a window, then we have to update the window title. + if (self.container.window()) |window| { + c.gtk_window_set_title(window.window, slice.ptr); } } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 8ac45a241..495ec8991 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -174,7 +174,6 @@ pub fn newSurface(self: *Tab, parent_: ?*CoreSurface) !*Surface { }, .parentSurface = parent_ != null, .gl_area = @ptrCast(gl_area), - .title_label = @ptrCast(self.label_text), .font_size = font_size, }); From 97b9708dd125ad3b55570f81dd2874db3a1f4ca2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Nov 2023 22:17:26 -0700 Subject: [PATCH 44/74] apprt/gtk: remove tab option from surface --- src/apprt/gtk/Surface.zig | 13 +++++++++---- src/apprt/gtk/Tab.zig | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 36daae687..ed485d06b 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -28,9 +28,6 @@ const log = std.log.scoped(.gtk_surface); pub const opengl_single_threaded_draw = true; pub const Options = struct { - /// The tab that this surface is attached to. - tab: *Tab, - /// The parent this surface is created under. parent: Parent, @@ -190,7 +187,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { // Build our result self.* = .{ .app = app, - .container = .{ .tab_ = opts.tab }, + .container = .{ .none = {} }, .parent = opts.parent, .gl_area = opts.gl_area, .title_text_buf = undefined, @@ -290,6 +287,14 @@ pub fn deinit(self: *Surface) void { if (self.cursor) |cursor| c.g_object_unref(cursor); } +// TODO: move this +/// Change the container for the surface to `container`. +pub fn setContainer(self: *Surface, container: Container) void { + self.container = container; + + // TODO: do we need to ever update our title or anything here? +} + fn render(self: *Surface) !void { try self.core_surface.renderer.drawFrame(self); } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 495ec8991..a16f9f061 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -168,7 +168,6 @@ pub fn newSurface(self: *Tab, parent_: ?*CoreSurface) !*Surface { c.gtk_widget_set_vexpand(gl_area, 1); try surface.init(self.window.app, .{ - .tab = self, .parent = .{ .tab = self, }, @@ -176,6 +175,7 @@ pub fn newSurface(self: *Tab, parent_: ?*CoreSurface) !*Surface { .gl_area = @ptrCast(gl_area), .font_size = font_size, }); + surface.setContainer(.{ .tab_ = self }); return surface; } From f1547465cc227f63aa1038da59b715af41b3fa58 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Nov 2023 22:38:22 -0700 Subject: [PATCH 45/74] apprt/gtk: new surface options down to just a couple --- src/apprt/gtk/Surface.zig | 68 ++++++++++++++++++++++++--------------- src/apprt/gtk/Tab.zig | 27 +++------------- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index ed485d06b..40167d1e1 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -4,6 +4,7 @@ const Surface = @This(); const std = @import("std"); +const Allocator = std.mem.Allocator; const configpkg = @import("../../config.zig"); const apprt = @import("../../apprt.zig"); const font = @import("../../font/main.zig"); @@ -28,21 +29,12 @@ const log = std.log.scoped(.gtk_surface); pub const opengl_single_threaded_draw = true; pub const Options = struct { + /// The parent surface to inherit settings such as font size, working + /// directory, etc. from. + parent2: ?*CoreSurface = null, + /// The parent this surface is created under. parent: Parent, - - /// The GL area that this surface should draw to. - gl_area: *c.GtkGLArea, - - /// 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. - parentSurface: bool = false, }; /// The container that this surface is directly attached to. @@ -129,12 +121,24 @@ im_composing: bool = false, im_buf: [128]u8 = undefined, im_len: u7 = 0, +pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface { + var surface = try alloc.create(Surface); + errdefer alloc.destroy(surface); + try surface.init(app, opts); + return surface; +} + pub fn init(self: *Surface, app: *App, opts: Options) !void { - const widget = @as(*c.GtkWidget, @ptrCast(opts.gl_area)); - c.gtk_gl_area_set_required_version(opts.gl_area, 3, 3); - c.gtk_gl_area_set_has_stencil_buffer(opts.gl_area, 0); - c.gtk_gl_area_set_has_depth_buffer(opts.gl_area, 0); - c.gtk_gl_area_set_use_es(opts.gl_area, 0); + const widget: *c.GtkWidget = c.gtk_gl_area_new(); + const gl_area: *c.GtkGLArea = @ptrCast(widget); + c.gtk_widget_set_hexpand(widget, 1); + c.gtk_widget_set_vexpand(widget, 1); + + c.gtk_widget_set_cursor_from_name(@ptrCast(gl_area), "text"); + c.gtk_gl_area_set_required_version(gl_area, 3, 3); + c.gtk_gl_area_set_has_stencil_buffer(gl_area, 0); + c.gtk_gl_area_set_has_depth_buffer(gl_area, 0); + c.gtk_gl_area_set_use_es(gl_area, 0); // Key event controller will tell us about raw keypress events. const ec_key = c.gtk_event_controller_key_new(); @@ -184,17 +188,24 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { c.gtk_widget_set_focusable(widget, 1); c.gtk_widget_set_focus_on_click(widget, 1); + // Inherit the parent's font size if we have a parent. + const font_size: ?font.face.DesiredSize = font_size: { + if (!app.config.@"window-inherit-font-size") break :font_size null; + const parent = opts.parent2 orelse break :font_size null; + break :font_size parent.font_size; + }; + // Build our result self.* = .{ .app = app, .container = .{ .none = {} }, .parent = opts.parent, - .gl_area = opts.gl_area, + .gl_area = gl_area, .title_text_buf = undefined, .title_text_buf_len = 0, .core_surface = undefined, - .font_size = opts.font_size, - .parentSurface = opts.parentSurface, + .font_size = font_size, + .parentSurface = opts.parent2 != null, .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .im_context = im_context, @@ -205,11 +216,11 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { try self.setMouseShape(.text); // GL events - _ = c.g_signal_connect_data(opts.gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(opts.gl_area, "unrealize", c.G_CALLBACK(>kUnrealize), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(opts.gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(opts.gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(opts.gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "unrealize", c.G_CALLBACK(>kUnrealize), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_key_press, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, c.G_CONNECT_DEFAULT); @@ -267,6 +278,11 @@ fn realize(self: *Surface) !void { self.realized = true; } +pub fn destroy(self: *Surface, alloc: Allocator) void { + self.deinit(); + alloc.free(self); +} + pub fn deinit(self: *Surface) void { // We don't allocate anything if we aren't realized. if (!self.realized) return; diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index a16f9f061..b1abbfbf2 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -148,33 +148,14 @@ pub fn deinit(self: *Tab) void { /// 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 { - // 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); - - // Inherit the parent's font size if we are configured to. - const font_size: ?font.face.DesiredSize = font_size: { - if (!self.window.app.config.@"window-inherit-font-size") break :font_size null; - const parent = parent_ orelse break :font_size null; - break :font_size parent.font_size; - }; - - // 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_hexpand(gl_area, 1); - c.gtk_widget_set_vexpand(gl_area, 1); - - try surface.init(self.window.app, .{ + const alloc = self.window.app.core_app.alloc; + var surface = try Surface.create(alloc, self.window.app, .{ + .parent2 = parent_, .parent = .{ .tab = self, }, - .parentSurface = parent_ != null, - .gl_area = @ptrCast(gl_area), - .font_size = font_size, }); + errdefer surface.destroy(alloc); surface.setContainer(.{ .tab_ = self }); return surface; From 83b0096dfcd7c50a1c80e54f98e9758451fba6d7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Nov 2023 22:43:49 -0700 Subject: [PATCH 46/74] apprt/gtk: get rid of one newsurface call --- src/apprt/gtk/Surface.zig | 2 +- src/apprt/gtk/Tab.zig | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 40167d1e1..5640ff5a2 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -280,7 +280,7 @@ fn realize(self: *Surface) !void { pub fn destroy(self: *Surface, alloc: Allocator) void { self.deinit(); - alloc.free(self); + alloc.destroy(self); } pub fn deinit(self: *Surface) void { diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index b1abbfbf2..d480abbff 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -90,8 +90,12 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { self.box = @ptrCast(box_widget); // Create the initial surface since all tabs start as a single non-split - const surface = try self.newSurface(parent_); - errdefer surface.deinit(); + var surface = try Surface.create(window.app.core_app.alloc, window.app, .{ + .parent2 = parent_, + .parent = .{ .tab = self }, + }); + errdefer surface.destroy(window.app.core_app.alloc); + surface.setContainer(.{ .tab_ = self }); self.child = .{ .surface = surface }; // Add Surface to the Tab From 79a9d417d17efa596a307edc89c21fc236b9886b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Nov 2023 09:38:06 -0700 Subject: [PATCH 47/74] apprt/gtk: working on new Split --- src/apprt/gtk/Paned.zig | 10 +- src/apprt/gtk/Split.zig | 260 ++++++++++++++++++++++++++++++++++++++ src/apprt/gtk/Surface.zig | 39 +++++- src/apprt/gtk/Tab.zig | 16 --- 4 files changed, 300 insertions(+), 25 deletions(-) create mode 100644 src/apprt/gtk/Split.zig diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig index 0905d85e0..ac699f960 100644 --- a/src/apprt/gtk/Paned.zig +++ b/src/apprt/gtk/Paned.zig @@ -56,9 +56,13 @@ pub fn init(self: *Paned, sibling: *Surface, direction: input.SplitDirection) !v const gtk_paned: *c.GtkPaned = @ptrCast(paned); self.paned = gtk_paned; - const tab = sibling.container.tab().?; // TODO - const surface = try tab.newSurface(&sibling.core_surface); - surface.setParent(.{ .paned = .{ self, .end } }); + const alloc = sibling.app.core_app.alloc; + var surface = try Surface.create(alloc, sibling.app, .{ + .parent2 = &sibling.core_surface, + .parent = .{ .paned = .{ self, .end } }, + }); + errdefer surface.destroy(alloc); + surface.container = sibling.container; // TODO self.addChild1(.{ .surface = sibling }); self.addChild2(.{ .surface = surface }); diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig new file mode 100644 index 000000000..a0589e88a --- /dev/null +++ b/src/apprt/gtk/Split.zig @@ -0,0 +1,260 @@ +/// Split represents a surface split where two surfaces are shown side-by-side +/// within the same window either vertically or horizontally. +const Split = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const font = @import("../../font/main.zig"); +const input = @import("../../input.zig"); +const CoreSurface = @import("../../Surface.zig"); + +const Surface = @import("Surface.zig"); +const Tab = @import("Tab.zig"); +const Position = @import("relation.zig").Position; +const Parent = @import("relation.zig").Parent; +const Child = @import("relation.zig").Child; +const c = @import("c.zig"); + +const log = std.log.scoped(.gtk); + +/// Our actual GtkPaned widget +paned: *c.GtkPaned, + +/// The container for this split panel. +container: Surface.Container, + +/// The elements of this split panel. +top_left: Elem, +bottom_right: Elem, + +/// Elem is the possible element of the split. +pub const Elem = union(enum) { + /// A surface is a leaf element of the split -- a terminal surface. + surface: *Surface, + + /// A split is a nested split within a split. This lets you for example + /// have a horizontal split with a vertical split on the left side + /// (amongst all other possible combinations). + split: *Split, + + /// Returns the GTK widget to add to the paned for the given element + pub fn widget(self: Child) *c.GtkWidget { + return switch (self) { + .surface => |surface| @ptrCast(surface.gl_area), + .split => |split| @ptrCast(@alignCast(split.paned)), + }; + } +}; + +/// Create a new split panel with the given sibling surface in the given +/// direction. The direction is where the new surface will be initialized. +/// +/// The sibling surface can be in a split already or it can be within a +/// tab. This properly handles updating the surface container so that +/// it represents the new split. +pub fn create( + alloc: Allocator, + sibling: *Surface, + direction: input.SplitDirection, +) !*Split { + var split = try alloc.create(Split); + errdefer alloc.destroy(split); + try split.init(sibling, direction); + return split; +} + +pub fn init( + self: *Split, + sibling: *Surface, + direction: input.SplitDirection, +) !void { + // Create the new child surface + const alloc = sibling.app.core_app.alloc; + var surface = try Surface.create(alloc, sibling.app, .{ + .parent2 = &sibling.core_surface, + .parent = .{ .paned = .{ self, .end } }, + }); + errdefer surface.destroy(alloc); + + // Create the actual GTKPaned, attach the proper children. + const orientation: c_uint = switch (direction) { + .right => c.GTK_ORIENTATION_HORIZONTAL, + .down => c.GTK_ORIENTATION_VERTICAL, + }; + const paned = c.gtk_paned_new(orientation); + errdefer c.g_object_unref(paned); + + // Update all of our containers to point to the right place. + // The split has to point to where the sibling pointed to because + // we're inheriting its parent. The sibling points to its location + // in the split, and the surface points to the other location. + const container = sibling.container; + sibling.container = .{ .split_tl = &self.top_left }; + surface.container = .{ .split_br = &self.bottom_right }; + + // If the sibling is already in a split, then we need to + // nest them properly. This gets the pointer to the split element + // that the original split was in, then updates it to point to this + // split. This split then contains the surface as an element. + if (container.splitElem()) |parent_elem| { + parent_elem.* = .{ .split = self }; + } + + self.* = .{ + .paned = @ptrCast(paned), + .container = container, + .top_left = .{ .surface = sibling }, + .bottom_right = .{ .surface = surface }, + }; +} + +/// Set the parent of Split. +pub fn setParent(self: *Split, parent: Parent) void { + self.parent = parent; +} + +/// Focus on first Surface that can be found in given position. If there's a +/// Split in the position, it will focus on the first surface in that position. +pub fn focusFirstSurfaceInPosition(self: *Split, position: Position) void { + const child = self.childInPosition(position); + switch (child) { + .surface => |s| s.grabFocus(), + .paned => |p| p.focusFirstSurfaceInPosition(position), + .none => { + log.warn("attempted to focus on first surface, found none", .{}); + return; + }, + } +} + +/// Split the Surface in the given position into a Split with two surfaces. +pub fn splitSurfaceInPosition(self: *Split, position: Position, direction: input.SplitDirection) !void { + const surface: *Surface = self.surfaceInPosition(position) orelse return; + + // Keep explicit reference to surface gl_area before we remove it. + const object: *c.GObject = @ptrCast(surface.gl_area); + _ = c.g_object_ref(object); + defer c.g_object_unref(object); + + // Keep position of divider + const parent_paned_position_before = c.gtk_paned_get_position(self.paned); + // Now remove it + self.removeChildInPosition(position); + + // Create new Split + // NOTE: We cannot use `replaceChildInPosition` here because we need to + // first remove the surface before we create a new pane. + const paned = try Split.create(surface.app.core_app.alloc, surface, direction); + switch (position) { + .start => self.addChild1(.{ .paned = paned }), + .end => self.addChild2(.{ .paned = paned }), + } + // Restore position + c.gtk_paned_set_position(self.paned, parent_paned_position_before); + + // Focus on new surface + paned.focusFirstSurfaceInPosition(.end); +} + +/// Replace the existing .start or .end Child with the given new Child. +pub fn replaceChildInPosition(self: *Split, child: Child, position: Position) void { + // Keep position of divider + const parent_paned_position_before = c.gtk_paned_get_position(self.paned); + + // Focus on the sibling, otherwise we'll get a GTK warning + self.focusFirstSurfaceInPosition(if (position == .start) .end else .start); + + // Now we can remove the other one + self.removeChildInPosition(position); + + switch (position) { + .start => self.addChild1(child), + .end => self.addChild2(child), + } + + // Restore position + c.gtk_paned_set_position(self.paned, parent_paned_position_before); +} + +/// Remove both children, setting *c.GtkSplit start/end children to null. +pub fn removeChildren(self: *Split) void { + self.removeChildInPosition(.start); + self.removeChildInPosition(.end); +} + +/// Deinit the Split by deiniting its child Split, if they exist. +pub fn deinit(self: *Split, alloc: Allocator) void { + for ([_]Child{ self.child1, self.child2 }) |child| { + switch (child) { + .none, .surface => continue, + .paned => |paned| { + paned.deinit(alloc); + alloc.destroy(paned); + }, + } + } +} + +fn removeChildInPosition(self: *Split, position: Position) void { + switch (position) { + .start => { + assert(self.child1 != .none); + self.child1 = .none; + c.gtk_paned_set_start_child(@ptrCast(self.paned), null); + }, + .end => { + assert(self.child2 != .none); + self.child2 = .none; + c.gtk_paned_set_end_child(@ptrCast(self.paned), null); + }, + } +} + +/// Update the paned children to represent the current state. +/// This should be called anytime the top/left or bottom/right +/// element is changed. +fn updateChildren(self: *const Split) void { + c.gtk_paned_set_start_child( + @ptrCast(self.paned), + self.top_left.widget(), + ); + c.gtk_paned_set_end_child( + @ptrCast(self.paned), + self.bottom_right.widget(), + ); +} + +fn addChild1(self: *Split, child: Child) void { + assert(self.child1 == .none); + + const widget = child.widget() orelse return; + c.gtk_paned_set_start_child(@ptrCast(self.paned), widget); + + self.child1 = child; + child.setParent(.{ .paned = .{ self, .start } }); +} + +fn addChild2(self: *Split, child: Child) void { + assert(self.child2 == .none); + + const widget = child.widget() orelse return; + c.gtk_paned_set_end_child(@ptrCast(self.paned), widget); + + self.child2 = child; + child.setParent(.{ .paned = .{ self, .end } }); +} + +fn childInPosition(self: *Split, position: Position) Child { + return switch (position) { + .start => self.child1, + .end => self.child2, + }; +} + +fn surfaceInPosition(self: *Split, position: Position) ?*Surface { + return switch (self.childInPosition(position)) { + .surface => |surface| surface, + else => null, + }; +} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 5640ff5a2..5f79c001f 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -13,7 +13,7 @@ const terminal = @import("../../terminal/main.zig"); const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); -const Paned = @import("Paned.zig"); +const Split = @import("Split.zig"); const Tab = @import("Tab.zig"); const Window = @import("Window.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); @@ -47,15 +47,20 @@ pub const Container = union(enum) { /// Directly attached to a tab. (i.e. no splits) tab_: *Tab, - /// A split within a split hierarchy. - paned: *Paned, + /// A split within a split hierarchy. The key determines the + /// position of the split within the parent split. + split_tl: *Split.Elem, + split_br: *Split.Elem, /// 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"), + .split_tl, .split_br => split: { + const s = self.split() orelse break :split null; + break :split s.container.window(); + }, }; } @@ -64,7 +69,29 @@ pub const Container = union(enum) { return switch (self) { .none => null, .tab_ => |v| v, - else => @panic("TODO"), + .split_tl, .split_br => split: { + const s = self.split() orelse break :split null; + break :split s.container.tab(); + }, + }; + } + + /// Returns the split containing this surface (if any). + pub fn split(self: Container) ?*Split { + return switch (self) { + .none, .tab_ => null, + .split_tl => |ptr| @fieldParentPtr(Split, "top_left", ptr), + .split_br => |ptr| @fieldParentPtr(Split, "bottom_right", ptr), + }; + } + + /// Returns the element of the split that this container + /// is attached to. + pub fn splitElem(self: Container) ?*Split.Elem { + return switch (self) { + .none, .tab_ => null, + .split_tl => |ptr| ptr, + .split_br => |ptr| ptr, }; } }; @@ -422,7 +449,7 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { } pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { - log.debug("splitting surface, direction: {}", .{direction}); + log.debug("splitting direction={}", .{direction}); switch (self.parent) { .none => return, diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index d480abbff..474871da4 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -149,22 +149,6 @@ pub fn deinit(self: *Tab) void { } } -/// 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 { - const alloc = self.window.app.core_app.alloc; - var surface = try Surface.create(alloc, self.window.app, .{ - .parent2 = parent_, - .parent = .{ - .tab = self, - }, - }); - errdefer surface.destroy(alloc); - surface.setContainer(.{ .tab_ = self }); - - return surface; -} - /// Splits the current child surface into a Paned in given direction. Child of /// Tab must be a Surface. pub fn splitSurface(self: *Tab, direction: input.SplitDirection) !void { From 4c1300ab691f540aacde7c67f05a7295d42b6ab7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Nov 2023 10:48:57 -0700 Subject: [PATCH 48/74] apprt/gkt: a lot of things are broken --- src/apprt/gtk/Split.zig | 62 +++++++++++-------------- src/apprt/gtk/Surface.zig | 98 +++++++++++++++++++++++++++------------ src/apprt/gtk/Tab.zig | 27 +++++++++-- src/apprt/gtk/Window.zig | 21 +++++---- 4 files changed, 130 insertions(+), 78 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index a0589e88a..458d22765 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -12,7 +12,6 @@ const CoreSurface = @import("../../Surface.zig"); const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); const Position = @import("relation.zig").Position; -const Parent = @import("relation.zig").Parent; const Child = @import("relation.zig").Child; const c = @import("c.zig"); @@ -25,27 +24,8 @@ paned: *c.GtkPaned, container: Surface.Container, /// The elements of this split panel. -top_left: Elem, -bottom_right: Elem, - -/// Elem is the possible element of the split. -pub const Elem = union(enum) { - /// A surface is a leaf element of the split -- a terminal surface. - surface: *Surface, - - /// A split is a nested split within a split. This lets you for example - /// have a horizontal split with a vertical split on the left side - /// (amongst all other possible combinations). - split: *Split, - - /// Returns the GTK widget to add to the paned for the given element - pub fn widget(self: Child) *c.GtkWidget { - return switch (self) { - .surface => |surface| @ptrCast(surface.gl_area), - .split => |split| @ptrCast(@alignCast(split.paned)), - }; - } -}; +top_left: Surface.Container.Elem, +bottom_right: Surface.Container.Elem, /// Create a new split panel with the given sibling surface in the given /// direction. The direction is where the new surface will be initialized. @@ -73,7 +53,6 @@ pub fn init( const alloc = sibling.app.core_app.alloc; var surface = try Surface.create(alloc, sibling.app, .{ .parent2 = &sibling.core_surface, - .parent = .{ .paned = .{ self, .end } }, }); errdefer surface.destroy(alloc); @@ -93,25 +72,21 @@ pub fn init( sibling.container = .{ .split_tl = &self.top_left }; surface.container = .{ .split_br = &self.bottom_right }; - // If the sibling is already in a split, then we need to - // nest them properly. This gets the pointer to the split element - // that the original split was in, then updates it to point to this - // split. This split then contains the surface as an element. - if (container.splitElem()) |parent_elem| { - parent_elem.* = .{ .split = self }; - } - self.* = .{ .paned = @ptrCast(paned), .container = container, .top_left = .{ .surface = sibling }, .bottom_right = .{ .surface = surface }, }; -} -/// Set the parent of Split. -pub fn setParent(self: *Split, parent: Parent) void { - self.parent = parent; + // Replace the previous containers element with our split. + // This allows a non-split to become a split, a split to + // become a nested split, etc. + container.replace(.{ .split = self }); + + // Update our children so that our GL area is properly + // added to the paned. + self.updateChildren(); } /// Focus on first Surface that can be found in given position. If there's a @@ -211,6 +186,23 @@ fn removeChildInPosition(self: *Split, position: Position) void { } } +// TODO: ehhhhhh +pub fn replace( + self: *Split, + ptr: *Surface.Container.Elem, + new: Surface.Container.Elem, +) void { + // We can write our element directly. There's nothing special. + assert(&self.top_left == ptr or &self.bottom_right == ptr); + ptr.* = new; + + // Update our paned children. This will reset the divider + // position but we want to keep it in place so save and restore it. + const pos = c.gtk_paned_get_position(self.paned); + self.updateChildren(); + c.gtk_paned_set_position(self.paned, pos); +} + /// Update the paned children to represent the current state. /// This should be called anytime the top/left or bottom/right /// element is changed. diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 5f79c001f..2891c0788 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -32,9 +32,6 @@ pub const Options = struct { /// The parent surface to inherit settings such as font size, working /// directory, etc. from. parent2: ?*CoreSurface = null, - - /// The parent this surface is created under. - parent: Parent, }; /// The container that this surface is directly attached to. @@ -49,8 +46,33 @@ pub const Container = union(enum) { /// A split within a split hierarchy. The key determines the /// position of the split within the parent split. - split_tl: *Split.Elem, - split_br: *Split.Elem, + split_tl: *Elem, + split_br: *Elem, + + /// Elem is the possible element of any container. A container can + /// hold both a surface and a split. Any valid container should + /// have an Elem value so that it can be properly used with + /// splits. + pub const Elem = union(enum) { + /// A surface is a leaf element of the split -- a terminal + /// surface. + surface: *Surface, + + /// A split is a nested split within a split. This lets you + /// for example have a horizontal split with a vertical split + /// on the left side (amongst all other possible + /// combinations). + split: *Split, + + /// Returns the GTK widget to add to the paned for the given + /// element + pub fn widget(self: Elem) *c.GtkWidget { + return switch (self) { + .surface => |s| @ptrCast(s.gl_area), + .split => |s| @ptrCast(@alignCast(s.paned)), + }; + } + }; /// Returns the window that this surface is attached to. pub fn window(self: Container) ?*Window { @@ -85,14 +107,19 @@ pub const Container = union(enum) { }; } - /// Returns the element of the split that this container - /// is attached to. - pub fn splitElem(self: Container) ?*Split.Elem { - return switch (self) { - .none, .tab_ => null, - .split_tl => |ptr| ptr, - .split_br => |ptr| ptr, - }; + /// Replace the container's element with this element. This is + /// used by children to modify their parents to for example change + /// from a surface to a split or a split back to a surface or + /// a split to a nested split and so on. + pub fn replace(self: Container, elem: Elem) void { + switch (self) { + .none => {}, + .tab_ => |t| t.replaceElem(elem), + inline .split_tl, .split_br => |ptr| { + const s = self.split().?; + s.replace(ptr, elem); + }, + } } }; @@ -111,9 +138,6 @@ container: Container = .{ .none = {} }, /// The app we're part of app: *App, -/// The parent we belong to -parent: Parent, - /// Our GTK area gl_area: *c.GtkGLArea, @@ -158,9 +182,20 @@ pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface { pub fn init(self: *Surface, app: *App, opts: Options) !void { const widget: *c.GtkWidget = c.gtk_gl_area_new(); const gl_area: *c.GtkGLArea = @ptrCast(widget); + + // We grab the floating reference to GL area. This lets the + // GL area be moved around i.e. between a split, a tab, etc. + // without having to be really careful about ordering to + // prevent a destroy. + // TODO: unref in deinit + _ = c.g_object_ref_sink(@ptrCast(gl_area)); + errdefer c.g_object_unref(@ptrCast(gl_area)); + + // We want the gl area to expand to fill the parent container. c.gtk_widget_set_hexpand(widget, 1); c.gtk_widget_set_vexpand(widget, 1); + // Various other GL properties c.gtk_widget_set_cursor_from_name(@ptrCast(gl_area), "text"); c.gtk_gl_area_set_required_version(gl_area, 3, 3); c.gtk_gl_area_set_has_stencil_buffer(gl_area, 0); @@ -226,7 +261,6 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { self.* = .{ .app = app, .container = .{ .none = {} }, - .parent = opts.parent, .gl_area = gl_area, .title_text_buf = undefined, .title_text_buf_len = 0, @@ -450,19 +484,21 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { log.debug("splitting direction={}", .{direction}); + const alloc = self.app.core_app.alloc; + _ = try Split.create(alloc, self, direction); - switch (self.parent) { - .none => return, - .paned => |parent_paned_tuple| { - const paned = parent_paned_tuple[0]; - const position = parent_paned_tuple[1]; - - try paned.splitSurfaceInPosition(position, direction); - }, - .tab => |tab| { - try tab.splitSurface(direction); - }, - } + // switch (self.parent) { + // .none => return, + // .paned => |parent_paned_tuple| { + // const paned = parent_paned_tuple[0]; + // const position = parent_paned_tuple[1]; + // + // try paned.splitSurfaceInPosition(position, direction); + // }, + // .tab => |tab| { + // try tab.splitSurface(direction); + // }, + // } } pub fn newTab(self: *Surface) !void { @@ -852,6 +888,7 @@ fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { fn gtkUnrealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { _ = area; + log.debug("gl surface unrealized", .{}); const self = userdataSelf(ud.?); self.core_surface.renderer.displayUnrealized(); @@ -973,7 +1010,8 @@ fn gtkMouseDown( // If we have siblings, we also update the title, since it means // another sibling might have updated the title. - if (self.parent != Parent.tab) self.updateTitleLabels(); + // TODO: fixme + //if (self.parent != Parent.tab) self.updateTitleLabels(); } self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| { diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 474871da4..9417d0052 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -22,12 +22,21 @@ const log = std.log.scoped(.gtk); pub const GHOSTTY_TAB = "ghostty_tab"; window: *Window, + +/// The tab label. label_text: *c.GtkLabel, -// 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 + +/// 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, + +/// The element of this tab so that we can handle splits and so on. +elem: Surface.Container.Elem, + // The child can be either a Surface if the tab is not split or a Paned child: Child, + // We'll update this every time a Surface gains focus, so that we have it // when we switch to another Tab. Then when we switch back to this tab, we // can easily re-focus that terminal. @@ -47,6 +56,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { .window = window, .label_text = undefined, .box = undefined, + .elem = undefined, .child = undefined, .focus_child = undefined, }; @@ -92,11 +102,11 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // Create the initial surface since all tabs start as a single non-split var surface = try Surface.create(window.app.core_app.alloc, window.app, .{ .parent2 = parent_, - .parent = .{ .tab = self }, }); errdefer surface.destroy(window.app.core_app.alloc); surface.setContainer(.{ .tab_ = self }); self.child = .{ .surface = surface }; + self.elem = .{ .surface = surface }; // Add Surface to the Tab const gl_area_widget = @as(*c.GtkWidget, @ptrCast(surface.gl_area)); @@ -178,6 +188,17 @@ pub fn removeChild(self: *Tab) void { self.child = .none; } +// TODO: move this +/// Replace the surface element that this tab is showing. +pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { + // Remove our previous widget + c.gtk_box_remove(self.box, self.elem.widget()); + + // Add our new one + c.gtk_box_append(self.box, elem.widget()); + self.elem = elem; +} + /// Sets child to given child and sets parent on child. pub fn setChild(self: *Tab, child: Child) void { const widget = child.widget() orelse return; diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 184628f03..d12439f4a 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -260,16 +260,17 @@ pub fn closeTab(self: *Window, tab: *Tab) void { pub fn closeSurface(self: *Window, surface: *Surface) void { assert(surface.container.window().? == self); - switch (surface.parent) { - .none => unreachable, - .tab => |tab| self.closeTab(tab), - .paned => |paned_tuple| { - const paned = paned_tuple[0]; - const position = paned_tuple[1]; - - self.closeSurfaceInPaned(surface, paned, position); - }, - } + // TODO: fixme + // switch (surface.parent) { + // .none => unreachable, + // .tab => |tab| self.closeTab(tab), + // .paned => |paned_tuple| { + // const paned = paned_tuple[0]; + // const position = paned_tuple[1]; + // + // self.closeSurfaceInPaned(surface, paned, position); + // }, + // } } fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position: Position) void { From 17445a7d8746f3a0df86dbaf2a17211da056e269 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Nov 2023 11:00:01 -0700 Subject: [PATCH 49/74] apprt/gtk: nested splits are good --- src/apprt/gtk/Split.zig | 18 ++++++++++++++++-- src/apprt/gtk/Tab.zig | 21 --------------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 458d22765..a15697fba 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -87,6 +87,9 @@ pub fn init( // Update our children so that our GL area is properly // added to the paned. self.updateChildren(); + + // The new surface should always grab focus + surface.grabFocus(); } /// Focus on first Surface that can be found in given position. If there's a @@ -199,8 +202,19 @@ pub fn replace( // Update our paned children. This will reset the divider // position but we want to keep it in place so save and restore it. const pos = c.gtk_paned_get_position(self.paned); - self.updateChildren(); - c.gtk_paned_set_position(self.paned, pos); + defer c.gtk_paned_set_position(self.paned, pos); + + if (ptr == &self.top_left) { + c.gtk_paned_set_start_child( + @ptrCast(self.paned), + self.top_left.widget(), + ); + } else { + c.gtk_paned_set_end_child( + @ptrCast(self.paned), + self.bottom_right.widget(), + ); + } } /// Update the paned children to represent the current state. diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 9417d0052..3110f06b0 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -159,27 +159,6 @@ pub fn deinit(self: *Tab) void { } } -/// Splits the current child surface into a Paned in given direction. Child of -/// Tab must be a Surface. -pub fn splitSurface(self: *Tab, direction: input.SplitDirection) !void { - assert(self.child == .surface); - - const surface = switch (self.child) { - .surface => |s| s, - else => unreachable, - }; - self.removeChild(); - - // Create a Paned with two Surfaces. - const paned = try Paned.create(self.window.app.core_app.alloc, surface, direction); - - // Add Paned to the Tab. - self.setChild(.{ .paned = paned }); - - // Focus on new surface - paned.focusFirstSurfaceInPosition(.end); -} - /// Remove the current child from the Tab. Noop if no child set. pub fn removeChild(self: *Tab) void { const widget = self.child.widget() orelse return; From cdd76a3b0b571bebb7be61cb7c1bfd5988a42641 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Nov 2023 11:07:12 -0700 Subject: [PATCH 50/74] apprt/gtk: alternate approach to setting pane children --- src/apprt/gtk/Split.zig | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index a15697fba..fcf7f5d05 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -204,17 +204,12 @@ pub fn replace( const pos = c.gtk_paned_get_position(self.paned); defer c.gtk_paned_set_position(self.paned, pos); - if (ptr == &self.top_left) { - c.gtk_paned_set_start_child( - @ptrCast(self.paned), - self.top_left.widget(), - ); - } else { - c.gtk_paned_set_end_child( - @ptrCast(self.paned), - self.bottom_right.widget(), - ); - } + // We have to set both to null. If we overwrite the pane with + // the same value, then GTK bugs out (the GL area unrealizes + // and never rerealizes). + c.gtk_paned_set_start_child(@ptrCast(self.paned), null); + c.gtk_paned_set_end_child(@ptrCast(self.paned), null); + self.updateChildren(); } /// Update the paned children to represent the current state. From 06b40a8b89e3d99694a53f91ac975be0da296c0d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Nov 2023 11:15:25 -0700 Subject: [PATCH 51/74] apprt/gtk: closing tabs works again --- src/apprt/gtk/Surface.zig | 28 ++++++++++++++++++++-------- src/apprt/gtk/Tab.zig | 7 +++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 2891c0788..eaf320bb4 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -121,6 +121,17 @@ pub const Container = union(enum) { }, } } + + /// Remove ourselves from the container. This is used by + /// children to effectively notify they're containing that + /// all children at this level are exiting. + pub fn remove(self: Container) void { + switch (self) { + .none => {}, + .tab_ => |t| t.closeElem(), + else => @panic("TOOD"), + } + } }; /// Whether the surface has been realized or not yet. When a surface is @@ -388,14 +399,16 @@ pub fn redraw(self: *Surface) void { /// Close this surface. pub fn close(self: *Surface, processActive: bool) void { - // If we are not currently in a window, then we don't need to do any - // cleanup. If we are in a window, we need to potentially confirm, - // remove ourselves from the view hierarchy, etc. - const window = self.container.window() orelse return; + // If we're not part of a window hierarchy, we never confirm + // so we can just directly remove ourselves and exit. + const window = self.container.window() orelse { + self.container.remove(); + return; + }; + // If we have no process active we can just exit immediately. if (!processActive) { - // TODO: change to container doing this directly - window.closeSurface(self); + self.container.remove(); return; } @@ -1472,8 +1485,7 @@ fn gtkCloseConfirmation( c.gtk_window_destroy(@ptrCast(alert)); if (response == c.GTK_RESPONSE_YES) { const self = userdataSelf(ud.?); - const window = self.container.window() orelse return; - window.closeSurface(self); + self.container.remove(); } } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 3110f06b0..78b4953de 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -178,6 +178,13 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { self.elem = elem; } +// TODO: move this +/// The surface element is closing. If we're the direct parent +/// then that means our tab is also closing. +pub fn closeElem(self: *Tab) void { + self.window.closeTab(self); +} + /// Sets child to given child and sets parent on child. pub fn setChild(self: *Tab, child: Child) void { const widget = child.widget() orelse return; From ec2aa8e3221a7e9b3b2afae972d843215495d200 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Nov 2023 12:17:33 -0700 Subject: [PATCH 52/74] apprt/gtk: maintain container pointers --- src/apprt/gtk/Split.zig | 37 +++++++++++++++++++++++++++---------- src/apprt/gtk/Surface.zig | 12 ++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index fcf7f5d05..ee997af64 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -156,10 +156,10 @@ pub fn replaceChildInPosition(self: *Split, child: Child, position: Position) vo } /// Remove both children, setting *c.GtkSplit start/end children to null. -pub fn removeChildren(self: *Split) void { - self.removeChildInPosition(.start); - self.removeChildInPosition(.end); -} +// pub fn removeChildren(self: *Split) void { +// self.removeChildInPosition(.start); +// self.removeChildInPosition(.end); +//} /// Deinit the Split by deiniting its child Split, if they exist. pub fn deinit(self: *Split, alloc: Allocator) void { @@ -189,6 +189,18 @@ fn removeChildInPosition(self: *Split, position: Position) void { } } +/// Remove the top left child. +pub fn removeTopLeft(self: *Split) void { + // Remove our children since we are going to no longer be + // a split anyways. This prevents widgets with multiple parents. + self.removeChildren(); + + // Our container must become whatever our bottom right is + self.container.replace(self.bottom_right); + + // TODO: memory management of top left +} + // TODO: ehhhhhh pub fn replace( self: *Split, @@ -203,12 +215,6 @@ pub fn replace( // position but we want to keep it in place so save and restore it. const pos = c.gtk_paned_get_position(self.paned); defer c.gtk_paned_set_position(self.paned, pos); - - // We have to set both to null. If we overwrite the pane with - // the same value, then GTK bugs out (the GL area unrealizes - // and never rerealizes). - c.gtk_paned_set_start_child(@ptrCast(self.paned), null); - c.gtk_paned_set_end_child(@ptrCast(self.paned), null); self.updateChildren(); } @@ -216,6 +222,12 @@ pub fn replace( /// This should be called anytime the top/left or bottom/right /// element is changed. fn updateChildren(self: *const Split) void { + // We have to set both to null. If we overwrite the pane with + // the same value, then GTK bugs out (the GL area unrealizes + // and never rerealizes). + self.removeChildren(); + + // Set our current children c.gtk_paned_set_start_child( @ptrCast(self.paned), self.top_left.widget(), @@ -226,6 +238,11 @@ fn updateChildren(self: *const Split) void { ); } +fn removeChildren(self: *const Split) void { + c.gtk_paned_set_start_child(@ptrCast(self.paned), null); + c.gtk_paned_set_end_child(@ptrCast(self.paned), null); +} + fn addChild1(self: *Split, child: Child) void { assert(self.child1 == .none); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index eaf320bb4..e13871050 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -72,6 +72,13 @@ pub const Container = union(enum) { .split => |s| @ptrCast(@alignCast(s.paned)), }; } + + pub fn containerPtr(self: Elem) *Container { + return switch (self) { + .surface => |s| &s.container, + .split => |s| &s.container, + }; + } }; /// Returns the window that this surface is attached to. @@ -112,6 +119,7 @@ pub const Container = union(enum) { /// from a surface to a split or a split back to a surface or /// a split to a nested split and so on. pub fn replace(self: Container, elem: Elem) void { + // Move the element into the container switch (self) { .none => {}, .tab_ => |t| t.replaceElem(elem), @@ -120,6 +128,9 @@ pub const Container = union(enum) { s.replace(ptr, elem); }, } + + // Update the reverse reference to the container + elem.containerPtr().* = self; } /// Remove ourselves from the container. This is used by @@ -129,6 +140,7 @@ pub const Container = union(enum) { switch (self) { .none => {}, .tab_ => |t| t.closeElem(), + .split_tl => self.split().?.removeTopLeft(), else => @panic("TOOD"), } } From 956fcf73490df80e53ea431a2706758c3583daff Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 18 Nov 2023 14:11:57 +0100 Subject: [PATCH 53/74] gtk: fix compilation error after rebase --- src/apprt/gtk/Tab.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 78b4953de..1a0d52a13 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -84,13 +84,13 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // 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); + 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(@ptrCast(label_text), 100, 1); + c.gtk_widget_set_size_request(label_text_widget, 100, 1); } // Create a Box in which we'll later keep either Surface or Paned From ecbe91071408c5fa2627989d199b324b4ca77639 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 20 Nov 2023 07:04:14 +0100 Subject: [PATCH 54/74] gtk: fix closing of surfaces by releasing the ref to gl_area --- src/apprt/gtk/Surface.zig | 6 ++++++ src/apprt/gtk/Tab.zig | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index e13871050..848dd02a5 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -387,6 +387,12 @@ pub fn deinit(self: *Surface) void { if (self.cursor) |cursor| c.g_object_unref(cursor); } +// shutdown removes the long-held reference to the gl_area and kicks off the +// deinit/destroy process for this surface. +pub fn shutdown(self: *Surface) void { + c.g_object_unref(self.gl_area); +} + // TODO: move this /// Change the container for the surface to `container`. pub fn setContainer(self: *Surface, container: Container) void { diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 1a0d52a13..d1d33893a 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -151,7 +151,8 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { /// Deinits tab by deiniting child if child is Paned. pub fn deinit(self: *Tab) void { switch (self.child) { - .none, .surface => return, + .none => return, + .surface => |s| s.shutdown(), .paned => |paned| { paned.deinit(self.window.app.core_app.alloc); self.window.app.core_app.alloc.destroy(paned); From 8cf9d97ac315cf1346f0542552c623be1e13d62b Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 23 Nov 2023 19:45:19 +0100 Subject: [PATCH 55/74] gtk: fix replacing of splits, remove dead code --- src/apprt/gtk/Paned.zig | 205 ------------------------------------- src/apprt/gtk/Split.zig | 159 +++++----------------------- src/apprt/gtk/Surface.zig | 39 +++++-- src/apprt/gtk/Tab.zig | 40 +------- src/apprt/gtk/Window.zig | 71 ------------- src/apprt/gtk/relation.zig | 40 -------- 6 files changed, 61 insertions(+), 493 deletions(-) delete mode 100644 src/apprt/gtk/Paned.zig delete mode 100644 src/apprt/gtk/relation.zig diff --git a/src/apprt/gtk/Paned.zig b/src/apprt/gtk/Paned.zig deleted file mode 100644 index ac699f960..000000000 --- a/src/apprt/gtk/Paned.zig +++ /dev/null @@ -1,205 +0,0 @@ -const Paned = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const font = @import("../../font/main.zig"); -const input = @import("../../input.zig"); -const CoreSurface = @import("../../Surface.zig"); - -const Window = @import("Window.zig"); -const Surface = @import("Surface.zig"); -const Tab = @import("Tab.zig"); -const Position = @import("relation.zig").Position; -const Parent = @import("relation.zig").Parent; -const Child = @import("relation.zig").Child; -const c = @import("c.zig"); - -const log = std.log.scoped(.gtk); - -/// Our actual GtkPaned widget -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, - -// 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. -parent: Parent, - -pub fn create(alloc: Allocator, sibling: *Surface, direction: input.SplitDirection) !*Paned { - var paned = try alloc.create(Paned); - errdefer alloc.destroy(paned); - try paned.init(sibling, direction); - return paned; -} - -pub fn init(self: *Paned, sibling: *Surface, direction: input.SplitDirection) !void { - self.* = .{ - .paned = undefined, - .child1 = .none, - .child2 = .none, - .parent = undefined, - }; - errdefer self.* = undefined; - - const orientation: c_uint = switch (direction) { - .right => c.GTK_ORIENTATION_HORIZONTAL, - .down => c.GTK_ORIENTATION_VERTICAL, - }; - - const paned = c.gtk_paned_new(orientation); - errdefer c.g_object_unref(paned); - - const gtk_paned: *c.GtkPaned = @ptrCast(paned); - self.paned = gtk_paned; - - const alloc = sibling.app.core_app.alloc; - var surface = try Surface.create(alloc, sibling.app, .{ - .parent2 = &sibling.core_surface, - .parent = .{ .paned = .{ self, .end } }, - }); - errdefer surface.destroy(alloc); - surface.container = sibling.container; // TODO - - self.addChild1(.{ .surface = sibling }); - self.addChild2(.{ .surface = surface }); -} - -/// Set the parent of Paned. -pub fn setParent(self: *Paned, parent: Parent) void { - self.parent = parent; -} - -/// Focus on first Surface that can be found in given position. If there's a -/// Paned in the position, it will focus on the first surface in that position. -pub fn focusFirstSurfaceInPosition(self: *Paned, position: Position) void { - const child = self.childInPosition(position); - switch (child) { - .surface => |s| s.grabFocus(), - .paned => |p| p.focusFirstSurfaceInPosition(position), - .none => { - log.warn("attempted to focus on first surface, found none", .{}); - return; - }, - } -} - -/// Split the Surface in the given position into a Paned with two surfaces. -pub fn splitSurfaceInPosition(self: *Paned, position: Position, direction: input.SplitDirection) !void { - const surface: *Surface = self.surfaceInPosition(position) orelse return; - - // Keep explicit reference to surface gl_area before we remove it. - const object: *c.GObject = @ptrCast(surface.gl_area); - _ = c.g_object_ref(object); - defer c.g_object_unref(object); - - // Keep position of divider - const parent_paned_position_before = c.gtk_paned_get_position(self.paned); - // Now remove it - self.removeChildInPosition(position); - - // Create new Paned - // NOTE: We cannot use `replaceChildInPosition` here because we need to - // first remove the surface before we create a new pane. - const paned = try Paned.create(surface.app.core_app.alloc, surface, direction); - switch (position) { - .start => self.addChild1(.{ .paned = paned }), - .end => self.addChild2(.{ .paned = paned }), - } - // Restore position - c.gtk_paned_set_position(self.paned, parent_paned_position_before); - - // Focus on new surface - paned.focusFirstSurfaceInPosition(.end); -} - -/// Replace the existing .start or .end Child with the given new Child. -pub fn replaceChildInPosition(self: *Paned, child: Child, position: Position) void { - // Keep position of divider - const parent_paned_position_before = c.gtk_paned_get_position(self.paned); - - // Focus on the sibling, otherwise we'll get a GTK warning - self.focusFirstSurfaceInPosition(if (position == .start) .end else .start); - - // Now we can remove the other one - self.removeChildInPosition(position); - - switch (position) { - .start => self.addChild1(child), - .end => self.addChild2(child), - } - - // Restore position - c.gtk_paned_set_position(self.paned, parent_paned_position_before); -} - -/// Remove both children, setting *c.GtkPaned start/end children to null. -pub fn removeChildren(self: *Paned) void { - self.removeChildInPosition(.start); - self.removeChildInPosition(.end); -} - -/// Deinit the Paned by deiniting its child Paneds, if they exist. -pub fn deinit(self: *Paned, alloc: Allocator) void { - for ([_]Child{ self.child1, self.child2 }) |child| { - switch (child) { - .none, .surface => continue, - .paned => |paned| { - paned.deinit(alloc); - alloc.destroy(paned); - }, - } - } -} - -fn removeChildInPosition(self: *Paned, position: Position) void { - switch (position) { - .start => { - assert(self.child1 != .none); - self.child1 = .none; - c.gtk_paned_set_start_child(@ptrCast(self.paned), null); - }, - .end => { - assert(self.child2 != .none); - self.child2 = .none; - c.gtk_paned_set_end_child(@ptrCast(self.paned), null); - }, - } -} - -fn addChild1(self: *Paned, child: Child) void { - assert(self.child1 == .none); - - const widget = child.widget() orelse return; - c.gtk_paned_set_start_child(@ptrCast(self.paned), widget); - - self.child1 = child; - child.setParent(.{ .paned = .{ self, .start } }); -} - -fn addChild2(self: *Paned, child: Child) void { - assert(self.child2 == .none); - - const widget = child.widget() orelse return; - c.gtk_paned_set_end_child(@ptrCast(self.paned), widget); - - self.child2 = child; - child.setParent(.{ .paned = .{ self, .end } }); -} - -fn childInPosition(self: *Paned, position: Position) Child { - return switch (position) { - .start => self.child1, - .end => self.child2, - }; -} - -fn surfaceInPosition(self: *Paned, position: Position) ?*Surface { - return switch (self.childInPosition(position)) { - .surface => |surface| surface, - else => null, - }; -} diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index ee997af64..ab7580f17 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -11,8 +11,6 @@ const CoreSurface = @import("../../Surface.zig"); const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); -const Position = @import("relation.zig").Position; -const Child = @import("relation.zig").Child; const c = @import("c.zig"); const log = std.log.scoped(.gtk); @@ -92,113 +90,38 @@ pub fn init( surface.grabFocus(); } -/// Focus on first Surface that can be found in given position. If there's a -/// Split in the position, it will focus on the first surface in that position. -pub fn focusFirstSurfaceInPosition(self: *Split, position: Position) void { - const child = self.childInPosition(position); - switch (child) { - .surface => |s| s.grabFocus(), - .paned => |p| p.focusFirstSurfaceInPosition(position), - .none => { - log.warn("attempted to focus on first surface, found none", .{}); - return; - }, - } -} - -/// Split the Surface in the given position into a Split with two surfaces. -pub fn splitSurfaceInPosition(self: *Split, position: Position, direction: input.SplitDirection) !void { - const surface: *Surface = self.surfaceInPosition(position) orelse return; - - // Keep explicit reference to surface gl_area before we remove it. - const object: *c.GObject = @ptrCast(surface.gl_area); - _ = c.g_object_ref(object); - defer c.g_object_unref(object); - - // Keep position of divider - const parent_paned_position_before = c.gtk_paned_get_position(self.paned); - // Now remove it - self.removeChildInPosition(position); - - // Create new Split - // NOTE: We cannot use `replaceChildInPosition` here because we need to - // first remove the surface before we create a new pane. - const paned = try Split.create(surface.app.core_app.alloc, surface, direction); - switch (position) { - .start => self.addChild1(.{ .paned = paned }), - .end => self.addChild2(.{ .paned = paned }), - } - // Restore position - c.gtk_paned_set_position(self.paned, parent_paned_position_before); - - // Focus on new surface - paned.focusFirstSurfaceInPosition(.end); -} - -/// Replace the existing .start or .end Child with the given new Child. -pub fn replaceChildInPosition(self: *Split, child: Child, position: Position) void { - // Keep position of divider - const parent_paned_position_before = c.gtk_paned_get_position(self.paned); - - // Focus on the sibling, otherwise we'll get a GTK warning - self.focusFirstSurfaceInPosition(if (position == .start) .end else .start); - - // Now we can remove the other one - self.removeChildInPosition(position); - - switch (position) { - .start => self.addChild1(child), - .end => self.addChild2(child), - } - - // Restore position - c.gtk_paned_set_position(self.paned, parent_paned_position_before); -} - -/// Remove both children, setting *c.GtkSplit start/end children to null. -// pub fn removeChildren(self: *Split) void { -// self.removeChildInPosition(.start); -// self.removeChildInPosition(.end); -//} - -/// Deinit the Split by deiniting its child Split, if they exist. -pub fn deinit(self: *Split, alloc: Allocator) void { - for ([_]Child{ self.child1, self.child2 }) |child| { - switch (child) { - .none, .surface => continue, - .paned => |paned| { - paned.deinit(alloc); - alloc.destroy(paned); - }, - } - } -} - -fn removeChildInPosition(self: *Split, position: Position) void { - switch (position) { - .start => { - assert(self.child1 != .none); - self.child1 = .none; - c.gtk_paned_set_start_child(@ptrCast(self.paned), null); - }, - .end => { - assert(self.child2 != .none); - self.child2 = .none; - c.gtk_paned_set_end_child(@ptrCast(self.paned), null); - }, - } +/// Remove the top left child. +pub fn removeTopLeft(self: *Split) void { + self.removeChild(self.top_left, self.bottom_right); } /// Remove the top left child. -pub fn removeTopLeft(self: *Split) void { +pub fn removeBottomRight(self: *Split) void { + self.removeChild(self.bottom_right, self.top_left); +} + +// TODO: Is this Zig-y? +inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surface.Container.Elem) void { + const window = self.container.window() orelse return; + + // TODO: Grab focus + + // Keep a reference to the side that we want to keep, so it doesn't get + // destroyed when it's removed from our underlying GtkPaned. + const keep_object: *c.GObject = @ptrCast(keep.widget()); + _ = c.g_object_ref(keep_object); + defer c.g_object_unref(keep_object); + // Remove our children since we are going to no longer be // a split anyways. This prevents widgets with multiple parents. self.removeChildren(); - // Our container must become whatever our bottom right is - self.container.replace(self.bottom_right); + // Our container must become whatever our top left is + self.container.replace(keep); - // TODO: memory management of top left + // TODO: is this correct? + remove.shutdown(); + window.app.core_app.alloc.destroy(self); } // TODO: ehhhhhh @@ -242,37 +165,3 @@ fn removeChildren(self: *const Split) void { c.gtk_paned_set_start_child(@ptrCast(self.paned), null); c.gtk_paned_set_end_child(@ptrCast(self.paned), null); } - -fn addChild1(self: *Split, child: Child) void { - assert(self.child1 == .none); - - const widget = child.widget() orelse return; - c.gtk_paned_set_start_child(@ptrCast(self.paned), widget); - - self.child1 = child; - child.setParent(.{ .paned = .{ self, .start } }); -} - -fn addChild2(self: *Split, child: Child) void { - assert(self.child2 == .none); - - const widget = child.widget() orelse return; - c.gtk_paned_set_end_child(@ptrCast(self.paned), widget); - - self.child2 = child; - child.setParent(.{ .paned = .{ self, .end } }); -} - -fn childInPosition(self: *Split, position: Position) Child { - return switch (position) { - .start => self.child1, - .end => self.child2, - }; -} - -fn surfaceInPosition(self: *Split, position: Position) ?*Surface { - return switch (self.childInPosition(position)) { - .surface => |surface| surface, - else => null, - }; -} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 848dd02a5..7695224cd 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -17,7 +17,6 @@ const Split = @import("Split.zig"); const Tab = @import("Tab.zig"); const Window = @import("Window.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); -const Parent = @import("relation.zig").Parent; const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); @@ -79,6 +78,22 @@ pub const Container = union(enum) { .split => |s| &s.container, }; } + + pub fn shutdown(self: Elem) void { + switch (self) { + .surface => |s| s.shutdown(), + .split => { + @panic("TODO: shutdownsplit"); + }, + } + } + + pub fn debugName(self: Elem) []const u8 { + return switch (self) { + .surface => "surface", + .split => "split", + }; + } }; /// Returns the window that this surface is attached to. @@ -119,10 +134,14 @@ pub const Container = union(enum) { /// from a surface to a split or a split back to a surface or /// a split to a nested split and so on. pub fn replace(self: Container, elem: Elem) void { + log.debug("Container.replace. self={s}, elem={s}", .{ self.debugName(), elem.debugName() }); + // Move the element into the container switch (self) { .none => {}, - .tab_ => |t| t.replaceElem(elem), + .tab_ => |t| { + t.replaceElem(elem); + }, inline .split_tl, .split_br => |ptr| { const s = self.split().?; s.replace(ptr, elem); @@ -137,13 +156,23 @@ pub const Container = union(enum) { /// children to effectively notify they're containing that /// all children at this level are exiting. pub fn remove(self: Container) void { + log.warn("Container.remove", .{}); switch (self) { .none => {}, .tab_ => |t| t.closeElem(), .split_tl => self.split().?.removeTopLeft(), - else => @panic("TOOD"), + .split_br => self.split().?.removeBottomRight(), } } + + pub fn debugName(self: Container) []const u8 { + return switch (self) { + .none => "none", + .tab_ => "tab", + .split_tl => "split_tl", + .split_br => "split_br", + }; + } }; /// Whether the surface has been realized or not yet. When a surface is @@ -664,10 +693,6 @@ 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, diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index d1d33893a..afa3383b1 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -10,9 +10,6 @@ const font = @import("../../font/main.zig"); const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); -const Paned = @import("Paned.zig"); -const Parent = @import("relation.zig").Parent; -const Child = @import("relation.zig").Child; const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const c = @import("c.zig"); @@ -34,9 +31,6 @@ box: *c.GtkBox, /// The element of this tab so that we can handle splits and so on. elem: Surface.Container.Elem, -// The child can be either a Surface if the tab is not split or a Paned -child: Child, - // We'll update this every time a Surface gains focus, so that we have it // when we switch to another Tab. Then when we switch back to this tab, we // can easily re-focus that terminal. @@ -57,7 +51,6 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { .label_text = undefined, .box = undefined, .elem = undefined, - .child = undefined, .focus_child = undefined, }; @@ -93,7 +86,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { c.gtk_widget_set_size_request(label_text_widget, 100, 1); } - // Create a Box in which we'll later keep either Surface or Paned + // Create a Box in which we'll later keep either Surface or Split const box_widget = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); c.gtk_widget_set_hexpand(box_widget, 1); c.gtk_widget_set_vexpand(box_widget, 1); @@ -105,7 +98,6 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { }); errdefer surface.destroy(window.app.core_app.alloc); surface.setContainer(.{ .tab_ = self }); - self.child = .{ .surface = surface }; self.elem = .{ .surface = surface }; // Add Surface to the Tab @@ -148,29 +140,16 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { surface.grabFocus(); } -/// Deinits tab by deiniting child if child is Paned. +/// Deinits tab by deiniting child elem. pub fn deinit(self: *Tab) void { - switch (self.child) { - .none => return, - .surface => |s| s.shutdown(), - .paned => |paned| { - paned.deinit(self.window.app.core_app.alloc); - self.window.app.core_app.alloc.destroy(paned); - }, - } -} - -/// Remove the current child from the Tab. Noop if no child set. -pub fn removeChild(self: *Tab) void { - const widget = self.child.widget() orelse return; - c.gtk_box_remove(self.box, widget); - - self.child = .none; + self.elem.shutdown(); } // TODO: move this /// Replace the surface element that this tab is showing. pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { + // _ = c.g_object_ref_sink(self.elem.widget()); + // Remove our previous widget c.gtk_box_remove(self.box, self.elem.widget()); @@ -186,15 +165,6 @@ pub fn closeElem(self: *Tab) void { self.window.closeTab(self); } -/// Sets child to given child and sets parent on child. -pub fn setChild(self: *Tab, child: Child) void { - const widget = child.widget() orelse return; - c.gtk_box_append(self.box, widget); - - child.setParent(.{ .tab = self }); - self.child = child; -} - 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 d12439f4a..7129599d6 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -16,10 +16,8 @@ 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 Position = @import("relation.zig").Position; const icon = @import("icon.zig"); const c = @import("c.zig"); @@ -256,75 +254,6 @@ pub fn closeTab(self: *Window, tab: *Tab) void { if (remaining > 0) self.focusCurrentTab(); } -/// Close the surface. This surface must be definitely part of this window. -pub fn closeSurface(self: *Window, surface: *Surface) void { - assert(surface.container.window().? == self); - - // TODO: fixme - // switch (surface.parent) { - // .none => unreachable, - // .tab => |tab| self.closeTab(tab), - // .paned => |paned_tuple| { - // const paned = paned_tuple[0]; - // const position = paned_tuple[1]; - // - // self.closeSurfaceInPaned(surface, paned, position); - // }, - // } -} - -fn closeSurfaceInPaned(self: *Window, surface: *Surface, paned: *Paned, position: Position) void { - const sibling_child, const sibling_widget = switch (position) { - .start => .{ - paned.child2, - c.gtk_paned_get_end_child(paned.paned), - }, - .end => .{ - paned.child1, - c.gtk_paned_get_start_child(paned.paned), - }, - }; - - // Keep explicit reference to sibling's widget (gl_area, or Paned), 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 reference on the surface we're closing - surface.setParent(.none); - - // Remove children. - paned.removeChildren(); - // Don't need to call paned.deinit, because we already removed children. - defer self.app.core_app.alloc.destroy(paned); - - switch (paned.parent) { - .none => unreachable, - .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(sibling_child); - }, - .paned => |parent_paned_tuple| { - const parent_paned = parent_paned_tuple[0]; - const parent_paned_position = parent_paned_tuple[1]; - - parent_paned.replaceChildInPosition(sibling_child, parent_paned_position); - }, - } - - switch (sibling_child) { - .surface => |s| s.grabFocus(), - .paned => |p| { - // Focus on first surface in sibling Paned - p.focusFirstSurfaceInPosition(position); - }, - else => {}, - } -} - /// 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; diff --git a/src/apprt/gtk/relation.zig b/src/apprt/gtk/relation.zig deleted file mode 100644 index b14b0b86e..000000000 --- a/src/apprt/gtk/relation.zig +++ /dev/null @@ -1,40 +0,0 @@ -const Surface = @import("Surface.zig"); -const Paned = @import("Paned.zig"); -const Tab = @import("Tab.zig"); -const c = @import("c.zig"); - -pub const Position = enum { - start, - end, -}; - -pub const Parent = union(enum) { - none, - tab: *Tab, - paned: struct { - *Paned, - Position, - }, -}; - -pub const Child = union(enum) { - none, - surface: *Surface, - paned: *Paned, - - pub fn setParent(self: Child, parent: Parent) void { - switch (self) { - .none => return, - .surface => |surface| surface.setParent(parent), - .paned => |paned| paned.setParent(parent), - } - } - - pub fn widget(self: Child) ?*c.GtkWidget { - return switch (self) { - .none => null, - .paned => |paned| @ptrCast(@alignCast(paned.paned)), - .surface => |surface| @ptrCast(surface.gl_area), - }; - } -}; From 0065bae0d43959ed973092b6c50dfa86c6b0977d Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 25 Nov 2023 13:46:48 +0100 Subject: [PATCH 56/74] gtk: get closing of tabs working again (closing windows still broken) --- src/apprt/gtk/Split.zig | 14 +++++++++++++- src/apprt/gtk/Surface.zig | 12 +++++------- src/apprt/gtk/Tab.zig | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index ab7580f17..9d21eb7a9 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -90,6 +90,18 @@ pub fn init( surface.grabFocus(); } +pub fn destroy(self: *Split) void { + const window = self.container.window() orelse return; + + self.top_left.destroy(); + self.bottom_right.destroy(); + + self.removeChildren(); + + // TODO: this is the same as in removeChild? + window.app.core_app.alloc.destroy(self); +} + /// Remove the top left child. pub fn removeTopLeft(self: *Split) void { self.removeChild(self.top_left, self.bottom_right); @@ -120,7 +132,7 @@ inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surfac self.container.replace(keep); // TODO: is this correct? - remove.shutdown(); + remove.destroy(); window.app.core_app.alloc.destroy(self); } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 7695224cd..1d8721f07 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -79,12 +79,10 @@ pub const Container = union(enum) { }; } - pub fn shutdown(self: Elem) void { + pub fn destroy(self: Elem) void { switch (self) { - .surface => |s| s.shutdown(), - .split => { - @panic("TODO: shutdownsplit"); - }, + .surface => |s| s.unref(), + .split => |s| s.destroy(), } } @@ -416,9 +414,9 @@ pub fn deinit(self: *Surface) void { if (self.cursor) |cursor| c.g_object_unref(cursor); } -// shutdown removes the long-held reference to the gl_area and kicks off the +// unref removes the long-held reference to the gl_area and kicks off the // deinit/destroy process for this surface. -pub fn shutdown(self: *Surface) void { +pub fn unref(self: *Surface) void { c.g_object_unref(self.gl_area); } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index afa3383b1..fc16c8e37 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -142,7 +142,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { /// Deinits tab by deiniting child elem. pub fn deinit(self: *Tab) void { - self.elem.shutdown(); + self.elem.destroy(); } // TODO: move this From a18fb4a6615c3a63c5024e65d09d036044b76949 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 25 Nov 2023 15:24:07 +0100 Subject: [PATCH 57/74] gtk: fix closing of windows that contains splits --- src/apprt/gtk/Split.zig | 18 +++++++----------- src/apprt/gtk/Surface.zig | 4 ++-- src/apprt/gtk/Tab.zig | 6 ++---- src/apprt/gtk/Window.zig | 4 ++-- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 9d21eb7a9..792968e4a 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -90,16 +90,11 @@ pub fn init( surface.grabFocus(); } -pub fn destroy(self: *Split) void { - const window = self.container.window() orelse return; +pub fn destroy(self: *Split, alloc: Allocator) void { + self.top_left.deinit(alloc); + self.bottom_right.deinit(alloc); - self.top_left.destroy(); - self.bottom_right.destroy(); - - self.removeChildren(); - - // TODO: this is the same as in removeChild? - window.app.core_app.alloc.destroy(self); + alloc.destroy(self); } /// Remove the top left child. @@ -115,6 +110,7 @@ pub fn removeBottomRight(self: *Split) void { // TODO: Is this Zig-y? inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surface.Container.Elem) void { const window = self.container.window() orelse return; + const alloc = window.app.core_app.alloc; // TODO: Grab focus @@ -132,8 +128,8 @@ inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surfac self.container.replace(keep); // TODO: is this correct? - remove.destroy(); - window.app.core_app.alloc.destroy(self); + remove.deinit(alloc); + alloc.destroy(self); } // TODO: ehhhhhh diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1d8721f07..21fe86a37 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -79,10 +79,10 @@ pub const Container = union(enum) { }; } - pub fn destroy(self: Elem) void { + pub fn deinit(self: Elem, alloc: Allocator) void { switch (self) { .surface => |s| s.unref(), - .split => |s| s.destroy(), + .split => |s| s.destroy(alloc), } } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index fc16c8e37..867956d0e 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -141,15 +141,13 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { } /// Deinits tab by deiniting child elem. -pub fn deinit(self: *Tab) void { - self.elem.destroy(); +pub fn deinit(self: *Tab, alloc: Allocator) void { + self.elem.deinit(alloc); } // TODO: move this /// Replace the surface element that this tab is showing. pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { - // _ = c.g_object_ref_sink(self.elem.widget()); - // Remove our previous widget c.gtk_box_remove(self.box, self.elem.widget()); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 7129599d6..923f23c53 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -193,7 +193,7 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { self.icon.deinit(self.app); for (self.tabs.items) |tab| { - tab.deinit(); + tab.deinit(self.app.core_app.alloc); self.app.core_app.alloc.destroy(tab); } self.tabs.deinit(self.app.core_app.alloc); @@ -219,7 +219,7 @@ pub fn removeTab(self: *Window, tab: *Tab) !void { if (tab_idx) |idx| _ = self.tabs.orderedRemove(idx) else return error.TabNotFound; // Deallocate the tab - tab.deinit(); + tab.deinit(self.app.core_app.alloc); self.app.core_app.alloc.destroy(tab); } From 236e57a1f4cc23c2468eaccea34c76e6a2599a8a Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 25 Nov 2023 16:06:28 +0100 Subject: [PATCH 58/74] gtk: restore focus-grabbing after closing one side in split --- src/apprt/gtk/Split.zig | 10 ++++++-- src/apprt/gtk/Surface.zig | 48 ++++++--------------------------------- 2 files changed, 15 insertions(+), 43 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 792968e4a..5ab4c3e8c 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -112,8 +112,6 @@ inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surfac const window = self.container.window() orelse return; const alloc = window.app.core_app.alloc; - // TODO: Grab focus - // Keep a reference to the side that we want to keep, so it doesn't get // destroyed when it's removed from our underlying GtkPaned. const keep_object: *c.GObject = @ptrCast(keep.widget()); @@ -127,6 +125,9 @@ inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surfac // Our container must become whatever our top left is self.container.replace(keep); + // Grab focus of the left-over side + keep.grabFocus(); + // TODO: is this correct? remove.deinit(alloc); alloc.destroy(self); @@ -149,6 +150,11 @@ pub fn replace( self.updateChildren(); } +// grabFocus grabs the focus of the top-left element. +pub fn grabFocus(self: *Split) void { + self.top_left.grabFocus(); +} + /// Update the paned children to represent the current state. /// This should be called anytime the top/left or bottom/right /// element is changed. diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 21fe86a37..c1eefc3c6 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -86,11 +86,11 @@ pub const Container = union(enum) { } } - pub fn debugName(self: Elem) []const u8 { - return switch (self) { - .surface => "surface", - .split => "split", - }; + pub fn grabFocus(self: Elem) void { + switch (self) { + .surface => |s| s.grabFocus(), + .split => |s| s.grabFocus(), + } } }; @@ -132,14 +132,10 @@ pub const Container = union(enum) { /// from a surface to a split or a split back to a surface or /// a split to a nested split and so on. pub fn replace(self: Container, elem: Elem) void { - log.debug("Container.replace. self={s}, elem={s}", .{ self.debugName(), elem.debugName() }); - // Move the element into the container switch (self) { .none => {}, - .tab_ => |t| { - t.replaceElem(elem); - }, + .tab_ => |t| t.replaceElem(elem), inline .split_tl, .split_br => |ptr| { const s = self.split().?; s.replace(ptr, elem); @@ -154,7 +150,6 @@ pub const Container = union(enum) { /// children to effectively notify they're containing that /// all children at this level are exiting. pub fn remove(self: Container) void { - log.warn("Container.remove", .{}); switch (self) { .none => {}, .tab_ => |t| t.closeElem(), @@ -162,15 +157,6 @@ pub const Container = union(enum) { .split_br => self.split().?.removeBottomRight(), } } - - pub fn debugName(self: Container) []const u8 { - return switch (self) { - .none => "none", - .tab_ => "tab", - .split_tl => "split_tl", - .split_br => "split_br", - }; - } }; /// Whether the surface has been realized or not yet. When a surface is @@ -541,22 +527,8 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { } pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { - log.debug("splitting direction={}", .{direction}); const alloc = self.app.core_app.alloc; _ = try Split.create(alloc, self, direction); - - // switch (self.parent) { - // .none => return, - // .paned => |parent_paned_tuple| { - // const paned = parent_paned_tuple[0]; - // const position = parent_paned_tuple[1]; - // - // try paned.splitSurfaceInPosition(position, direction); - // }, - // .tab => |tab| { - // try tab.splitSurface(direction); - // }, - // } } pub fn newTab(self: *Surface) !void { @@ -1059,13 +1031,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) { - 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 - // another sibling might have updated the title. - // TODO: fixme - //if (self.parent != Parent.tab) self.updateTitleLabels(); + self.grabFocus(); } self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| { From 3fef4fce5682975230f39a678e7858c33ba2a66e Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 25 Nov 2023 16:21:42 +0100 Subject: [PATCH 59/74] gtk: move surface destroy through deref --- src/apprt/gtk/Surface.zig | 9 +++------ src/apprt/gtk/Tab.zig | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index c1eefc3c6..b0b317199 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -223,7 +223,9 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { // GL area be moved around i.e. between a split, a tab, etc. // without having to be really careful about ordering to // prevent a destroy. - // TODO: unref in deinit + // + // This is unref'd in the unref() method that's called by the + // self.container through Elem.deinit. _ = c.g_object_ref_sink(@ptrCast(gl_area)); errdefer c.g_object_unref(@ptrCast(gl_area)); @@ -375,11 +377,6 @@ fn realize(self: *Surface) !void { self.realized = true; } -pub fn destroy(self: *Surface, alloc: Allocator) void { - self.deinit(); - alloc.destroy(self); -} - pub fn deinit(self: *Surface) void { // We don't allocate anything if we aren't realized. if (!self.realized) return; diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 867956d0e..4b7ef5ccb 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -96,7 +96,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { var surface = try Surface.create(window.app.core_app.alloc, window.app, .{ .parent2 = parent_, }); - errdefer surface.destroy(window.app.core_app.alloc); + errdefer surface.unref(); surface.setContainer(.{ .tab_ = self }); self.elem = .{ .surface = surface }; From ba65b61fa9a20b6ab43f61c042b427490a7fb8b2 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 25 Nov 2023 19:07:44 +0100 Subject: [PATCH 60/74] gtk: restore detachable-tabs feature after adding splits --- src/apprt/gtk/Window.zig | 58 +++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 923f23c53..fbe1f64f2 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -210,6 +210,13 @@ pub fn newTab(self: *Window, parentSurface: ?*CoreSurface) !void { // does not (cursor doesn't blink) unless reactivated by refocusing. } +// addTab adds a tab to the windows list of tabs. +// This does *not* manage the underlying GtkNotebook pages. +pub fn addTab(self: *Window, tab: *Tab) !void { + tab.window = self; + try self.tabs.append(self.app.core_app.alloc, tab); +} + pub fn removeTab(self: *Window, tab: *Tab) !void { // Remove the tab from our stored tabs. const tab_idx = for (self.tabs.items, 0..) |t, i| { @@ -217,10 +224,6 @@ pub fn removeTab(self: *Window, tab: *Tab) !void { } else null; if (tab_idx) |idx| _ = self.tabs.orderedRemove(idx) else return error.TabNotFound; - - // Deallocate the tab - tab.deinit(self.app.core_app.alloc); - self.app.core_app.alloc.destroy(tab); } /// Close the tab for the given notebook page. This will automatically @@ -236,6 +239,9 @@ pub fn closeTab(self: *Window, tab: *Tab) void { log.warn("tab {} not removable: {}", .{ page_idx, err }); return; }; + // Deallocate the tab + tab.deinit(self.app.core_app.alloc); + self.app.core_app.alloc.destroy(tab); c.gtk_notebook_remove_page(self.notebook, page_idx); @@ -381,31 +387,35 @@ fn gtkNotebookCreateWindow( page: *c.GtkWidget, ud: ?*anyopaque, ) callconv(.C) ?*c.GtkNotebook { - _ = ud; - _ = page; - log.warn("feature needs to be re-implemented after adding gtk splits", .{}); - 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 tab: *Tab = @ptrCast(@alignCast( + c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null, + )); - // const self = userdataSelf(ud.?); - // const alloc = self.app.core_app.alloc; + const currentWindow = userdataSelf(ud.?); + const alloc = currentWindow.app.core_app.alloc; + const app = currentWindow.app; - // // Create a new window - // const window = Window.create(alloc, self.app) catch |err| { - // log.warn("error creating new window error={}", .{err}); - // return null; - // }; + // Create a new window + const window = Window.create(alloc, app) catch |err| { + log.warn("error creating new window error={}", .{err}); + return null; + }; - // // 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]; + // Now remove the tab from the old window. + currentWindow.removeTab(tab) catch |err| { + log.warn("error removing tab error={}", .{err}); + return null; + }; - // return window.notebook; + // And add it to the new window. + tab.window = window; + window.addTab(tab) catch |err| { + log.warn("error adding tab to new window error={}", .{err}); + return null; + }; + + return window.notebook; } fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { From e2a58b340c5691a5dbdc1857b0a13c77555c4660 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 25 Nov 2023 20:05:26 +0100 Subject: [PATCH 61/74] gtk: (temp) fix splitting top_left side in split Before this change, it would crash when you had the following surfaces split1 / \ / \ surf1 \ split2 / \ surf2 surf3 and you'd want to split `surf1` again. Splitting `surf2` or `surf3` would be fine, but surf1 would break things. --- src/apprt/gtk/Split.zig | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 5ab4c3e8c..db9fc58ab 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -159,6 +159,19 @@ pub fn grabFocus(self: *Split) void { /// This should be called anytime the top/left or bottom/right /// element is changed. fn updateChildren(self: *const Split) void { + // TODO: Not sure we should keep this. + // + // We keep references to both widgets, because only Surface widgets have + // long-held references but GtkPaned will also get destroyed if we don't + // keep a reference here before removing. + const top_left_object: *c.GObject = @ptrCast(self.top_left.widget()); + _ = c.g_object_ref(top_left_object); + defer c.g_object_unref(top_left_object); + + const bottom_right_object: *c.GObject = @ptrCast(self.bottom_right.widget()); + _ = c.g_object_ref(bottom_right_object); + defer c.g_object_unref(bottom_right_object); + // We have to set both to null. If we overwrite the pane with // the same value, then GTK bugs out (the GL area unrealizes // and never rerealizes). From 1b4fc83f43b29e0b9f714f66194771fe5c0bc22e Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sun, 26 Nov 2023 07:32:59 +0100 Subject: [PATCH 62/74] gtk: switch to long-held reference for GtkPaned --- src/apprt/gtk/Split.zig | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index db9fc58ab..c0c91c688 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -62,6 +62,9 @@ pub fn init( const paned = c.gtk_paned_new(orientation); errdefer c.g_object_unref(paned); + // Keep a long-lived reference, which we unref in destroy. + _ = c.g_object_ref(paned); + // Update all of our containers to point to the right place. // The split has to point to where the sibling pointed to because // we're inheriting its parent. The sibling points to its location @@ -94,6 +97,9 @@ pub fn destroy(self: *Split, alloc: Allocator) void { self.top_left.deinit(alloc); self.bottom_right.deinit(alloc); + // Clean up our GTK reference. + c.g_object_unref(self.paned); + alloc.destroy(self); } @@ -112,12 +118,6 @@ inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surfac const window = self.container.window() orelse return; const alloc = window.app.core_app.alloc; - // Keep a reference to the side that we want to keep, so it doesn't get - // destroyed when it's removed from our underlying GtkPaned. - const keep_object: *c.GObject = @ptrCast(keep.widget()); - _ = c.g_object_ref(keep_object); - defer c.g_object_unref(keep_object); - // Remove our children since we are going to no longer be // a split anyways. This prevents widgets with multiple parents. self.removeChildren(); @@ -159,19 +159,6 @@ pub fn grabFocus(self: *Split) void { /// This should be called anytime the top/left or bottom/right /// element is changed. fn updateChildren(self: *const Split) void { - // TODO: Not sure we should keep this. - // - // We keep references to both widgets, because only Surface widgets have - // long-held references but GtkPaned will also get destroyed if we don't - // keep a reference here before removing. - const top_left_object: *c.GObject = @ptrCast(self.top_left.widget()); - _ = c.g_object_ref(top_left_object); - defer c.g_object_unref(top_left_object); - - const bottom_right_object: *c.GObject = @ptrCast(self.bottom_right.widget()); - _ = c.g_object_ref(bottom_right_object); - defer c.g_object_unref(bottom_right_object); - // We have to set both to null. If we overwrite the pane with // the same value, then GTK bugs out (the GL area unrealizes // and never rerealizes). From 7e9dce054f39c3b44a70f9c8d6fbfd3f8ec80b03 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sun, 26 Nov 2023 13:18:43 +0100 Subject: [PATCH 63/74] gtk: remove unneeded method setContainer on Surface We don't need to update the titles, etc. since that's done by `grabFocus` and that then makes the method not that useful. Removing for now. --- src/apprt/gtk/Surface.zig | 9 +++------ src/apprt/gtk/Tab.zig | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index b0b317199..2286ecd2b 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -403,12 +403,9 @@ pub fn unref(self: *Surface) void { c.g_object_unref(self.gl_area); } -// TODO: move this -/// Change the container for the surface to `container`. -pub fn setContainer(self: *Surface, container: Container) void { - self.container = container; - - // TODO: do we need to ever update our title or anything here? +pub fn destroy(self: *Surface, alloc: Allocator) void { + self.deinit(); + alloc.destroy(self); } fn render(self: *Surface) !void { diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 4b7ef5ccb..d8c006ce7 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -97,7 +97,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { .parent2 = parent_, }); errdefer surface.unref(); - surface.setContainer(.{ .tab_ = self }); + surface.container = .{ .tab_ = self }; self.elem = .{ .surface = surface }; // Add Surface to the Tab From 89f4cf11c79bb706949f9b29710e6170192f139a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Nov 2023 21:46:43 -0800 Subject: [PATCH 64/74] apprt/gtk: rename parent2 to parent --- src/apprt/gtk/Split.zig | 2 +- src/apprt/gtk/Surface.zig | 6 +++--- src/apprt/gtk/Tab.zig | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index c0c91c688..fce3f74ed 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -50,7 +50,7 @@ pub fn init( // Create the new child surface const alloc = sibling.app.core_app.alloc; var surface = try Surface.create(alloc, sibling.app, .{ - .parent2 = &sibling.core_surface, + .parent = &sibling.core_surface, }); errdefer surface.destroy(alloc); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 2286ecd2b..dfffdef0a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -30,7 +30,7 @@ pub const opengl_single_threaded_draw = true; pub const Options = struct { /// The parent surface to inherit settings such as font size, working /// directory, etc. from. - parent2: ?*CoreSurface = null, + parent: ?*CoreSurface = null, }; /// The container that this surface is directly attached to. @@ -291,7 +291,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { // Inherit the parent's font size if we have a parent. const font_size: ?font.face.DesiredSize = font_size: { if (!app.config.@"window-inherit-font-size") break :font_size null; - const parent = opts.parent2 orelse break :font_size null; + const parent = opts.parent orelse break :font_size null; break :font_size parent.font_size; }; @@ -304,7 +304,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .title_text_buf_len = 0, .core_surface = undefined, .font_size = font_size, - .parentSurface = opts.parent2 != null, + .parentSurface = opts.parent != null, .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .im_context = im_context, diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index d8c006ce7..96acfea0a 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -94,7 +94,7 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { // Create the initial surface since all tabs start as a single non-split var surface = try Surface.create(window.app.core_app.alloc, window.app, .{ - .parent2 = parent_, + .parent = parent_, }); errdefer surface.unref(); surface.container = .{ .tab_ = self }; From c7eeda1f00e02978cb63cbb3640cf9f53f3c7139 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 08:08:06 -0800 Subject: [PATCH 65/74] apprt/gtk: code aesthetic changes --- src/apprt/gtk/Tab.zig | 13 +++++++++++-- src/apprt/gtk/Window.zig | 16 +++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 96acfea0a..c25f4ad55 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -18,9 +18,10 @@ const log = std.log.scoped(.gtk); pub const GHOSTTY_TAB = "ghostty_tab"; +/// The window that owns this tab. window: *Window, -/// The tab label. +/// The tab label. The tab label is the text that appears on the tab. label_text: *c.GtkLabel, /// We'll put our children into this box instead of packing them @@ -86,7 +87,9 @@ pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void { c.gtk_widget_set_size_request(label_text_widget, 100, 1); } - // Create a Box in which we'll later keep either Surface or Split + // 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). const box_widget = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); c.gtk_widget_set_hexpand(box_widget, 1); c.gtk_widget_set_vexpand(box_widget, 1); @@ -145,6 +148,12 @@ pub fn deinit(self: *Tab, alloc: Allocator) void { self.elem.deinit(alloc); } +/// Deinit and deallocate the tab. +pub fn destroy(self: *Tab, alloc: Allocator) void { + self.deinit(alloc); + alloc.destroy(self); +} + // TODO: move this /// Replace the surface element that this tab is showing. pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index fbe1f64f2..0f9d239da 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -35,6 +35,7 @@ notebook: *c.GtkNotebook, /// pointer to this because GTK can use it at any time. icon: icon.Icon, +/// The tab state for this window. tabs: std.ArrayListUnmanaged(*Tab), pub fn create(alloc: Allocator, app: *App) !*Window { @@ -58,13 +59,9 @@ pub fn init(self: *Window, app: *App) !void { .icon = undefined, .window = undefined, .notebook = undefined, - .tabs = undefined, + .tabs = .{}, }; - 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); @@ -192,17 +189,14 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { self.icon.deinit(self.app); - for (self.tabs.items) |tab| { - tab.deinit(self.app.core_app.alloc); - self.app.core_app.alloc.destroy(tab); - } + for (self.tabs.items) |tab| tab.destroy(self.app.core_app.alloc); self.tabs.deinit(self.app.core_app.alloc); } /// Add a new tab to this window. -pub fn newTab(self: *Window, parentSurface: ?*CoreSurface) !void { +pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { const alloc = self.app.core_app.alloc; - const tab = try Tab.create(alloc, self, parentSurface); + const tab = try Tab.create(alloc, self, parent); try self.tabs.append(alloc, tab); // TODO: When this is triggered through a GTK action, the new surface From dd39b29f30e078418e4ce366c35c33eff52a527f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 08:52:08 -0800 Subject: [PATCH 66/74] apprt/gtk: we don't need to keep track of Tabs --- src/apprt/gtk/Tab.zig | 10 ++++++++++ src/apprt/gtk/Window.zig | 40 +--------------------------------------- 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index c25f4ad55..e6a417792 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -134,6 +134,7 @@ 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); // Switch to the new tab c.gtk_notebook_set_current_page(window.notebook, page_idx); @@ -177,3 +178,12 @@ fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { const window = tab.window; window.closeTab(tab); } + +fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + _ = v; + log.debug("tab box destroy", .{}); + + // When our box is destroyed, we want to destroy our tab, too. + const tab: *Tab = @ptrCast(@alignCast(ud)); + tab.destroy(tab.window.app.core_app.alloc); +} diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 0f9d239da..1d02ba80c 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -35,9 +35,6 @@ notebook: *c.GtkNotebook, /// pointer to this because GTK can use it at any time. icon: icon.Icon, -/// The tab state for this window. -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 @@ -59,7 +56,6 @@ pub fn init(self: *Window, app: *App) !void { .icon = undefined, .window = undefined, .notebook = undefined, - .tabs = .{}, }; // Create the window @@ -189,37 +185,18 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { self.icon.deinit(self.app); - for (self.tabs.items) |tab| tab.destroy(self.app.core_app.alloc); - self.tabs.deinit(self.app.core_app.alloc); } /// Add a new tab to this window. pub fn newTab(self: *Window, parent: ?*CoreSurface) !void { const alloc = self.app.core_app.alloc; - const tab = try Tab.create(alloc, self, parent); - try self.tabs.append(alloc, tab); + _ = try Tab.create(alloc, self, parent); // 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) unless reactivated by refocusing. } -// addTab adds a tab to the windows list of tabs. -// This does *not* manage the underlying GtkNotebook pages. -pub fn addTab(self: *Window, tab: *Tab) !void { - tab.window = self; - try self.tabs.append(self.app.core_app.alloc, tab); -} - -pub fn removeTab(self: *Window, tab: *Tab) !void { - // Remove the tab from our stored tabs. - const tab_idx = for (self.tabs.items, 0..) |t, i| { - if (t == tab) break i; - } else null; - - if (tab_idx) |idx| _ = self.tabs.orderedRemove(idx) else return error.TabNotFound; -} - /// 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 { @@ -228,11 +205,6 @@ pub fn closeTab(self: *Window, tab: *Tab) void { // Find page and tab which we're closing const page_idx = getNotebookPageIndex(page); - // Remove the tab from our stored tabs. - self.removeTab(tab) catch |err| { - log.warn("tab {} not removable: {}", .{ page_idx, err }); - return; - }; // Deallocate the tab tab.deinit(self.app.core_app.alloc); self.app.core_app.alloc.destroy(tab); @@ -396,18 +368,8 @@ fn gtkNotebookCreateWindow( return null; }; - // Now remove the tab from the old window. - currentWindow.removeTab(tab) catch |err| { - log.warn("error removing tab error={}", .{err}); - return null; - }; - // And add it to the new window. tab.window = window; - window.addTab(tab) catch |err| { - log.warn("error adding tab to new window error={}", .{err}); - return null; - }; return window.notebook; } From e40b79906eb3faa6d4c0d5e0e7b73e706165f727 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 09:07:09 -0800 Subject: [PATCH 67/74] apprt/gtk: do not deinit tab in closeTab --- src/apprt/gtk/Window.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 1d02ba80c..3fe96a498 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -205,10 +205,8 @@ pub fn closeTab(self: *Window, tab: *Tab) void { // Find page and tab which we're closing const page_idx = getNotebookPageIndex(page); - // Deallocate the tab - tab.deinit(self.app.core_app.alloc); - self.app.core_app.alloc.destroy(tab); - + // 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); const remaining = c.gtk_notebook_get_n_pages(self.notebook); From 14ef6fb2f9e0db71a4012b4a1830ccc5519f2541 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 09:07:37 -0800 Subject: [PATCH 68/74] apprt/gtk: add comments, rename some funcs --- src/apprt/gtk/Split.zig | 13 +++++++++---- src/apprt/gtk/Surface.zig | 4 ++-- src/apprt/gtk/Tab.zig | 6 ++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index fce3f74ed..01562bcc5 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -47,7 +47,7 @@ pub fn init( sibling: *Surface, direction: input.SplitDirection, ) !void { - // Create the new child surface + // Create the new child surface for the other direction. const alloc = sibling.app.core_app.alloc; var surface = try Surface.create(alloc, sibling.app, .{ .parent = &sibling.core_surface, @@ -97,7 +97,8 @@ pub fn destroy(self: *Split, alloc: Allocator) void { self.top_left.deinit(alloc); self.bottom_right.deinit(alloc); - // Clean up our GTK reference. + // Clean up our GTK reference. This will trigger all the destroy callbacks + // that are necessary for the surfaces to clean up. c.g_object_unref(self.paned); alloc.destroy(self); @@ -114,7 +115,11 @@ pub fn removeBottomRight(self: *Split) void { } // TODO: Is this Zig-y? -inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surface.Container.Elem) void { +fn removeChild( + self: *Split, + remove: Surface.Container.Elem, + keep: Surface.Container.Elem, +) void { const window = self.container.window() orelse return; const alloc = window.app.core_app.alloc; @@ -128,7 +133,7 @@ inline fn removeChild(self: *Split, remove: Surface.Container.Elem, keep: Surfac // Grab focus of the left-over side keep.grabFocus(); - // TODO: is this correct? + // When a child is removed we are no longer a split, so destroy ourself remove.deinit(alloc); alloc.destroy(self); } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index dfffdef0a..dfcf663bf 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -147,12 +147,12 @@ pub const Container = union(enum) { } /// Remove ourselves from the container. This is used by - /// children to effectively notify they're containing that + /// children to effectively notify they're container that /// all children at this level are exiting. pub fn remove(self: Container) void { switch (self) { .none => {}, - .tab_ => |t| t.closeElem(), + .tab_ => |t| t.remove(), .split_tl => self.split().?.removeTopLeft(), .split_br => self.split().?.removeBottomRight(), } diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index e6a417792..fff5b9519 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -166,10 +166,8 @@ pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void { self.elem = elem; } -// TODO: move this -/// The surface element is closing. If we're the direct parent -/// then that means our tab is also closing. -pub fn closeElem(self: *Tab) void { +/// Remove this tab from the window. +pub fn remove(self: *Tab) void { self.window.closeTab(self); } From c2c8f78cf8d47923230f2b66b50871324a3916a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 09:11:14 -0800 Subject: [PATCH 69/74] apprt/gtk: comments --- src/apprt/gtk/Split.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 01562bcc5..93558dc36 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -114,7 +114,6 @@ pub fn removeBottomRight(self: *Split) void { self.removeChild(self.bottom_right, self.top_left); } -// TODO: Is this Zig-y? fn removeChild( self: *Split, remove: Surface.Container.Elem, @@ -138,7 +137,10 @@ fn removeChild( alloc.destroy(self); } -// TODO: ehhhhhh +// This replaces the element at the given pointer with a new element. +// The ptr must be either top_left or bottom_right (asserted in debug). +// The memory of the old element must be freed or otherwise handled by +// the caller. pub fn replace( self: *Split, ptr: *Surface.Container.Elem, From 967e091e2cf82999477149508decace360f191e4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 09:14:08 -0800 Subject: [PATCH 70/74] apprt/gtk: just heap allocate the title text --- src/apprt/gtk/Surface.zig | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index dfcf663bf..77a784bbb 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -184,9 +184,7 @@ cursor: ?*c.GdkCursor = null, /// .title will be updated if we have focus. /// When set the text in this buf will be null-terminated, because we need to /// pass it to GTK. -/// TODO: what's a big enough value? -title_text_buf: [4096]u8, -title_text_buf_len: u13, +title_text: ?[:0]const u8 = null, /// The core surface backing this surface core_surface: CoreSurface, @@ -300,8 +298,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .app = app, .container = .{ .none = {} }, .gl_area = gl_area, - .title_text_buf = undefined, - .title_text_buf_len = 0, + .title_text = null, .core_surface = undefined, .font_size = font_size, .parentSurface = opts.parent != null, @@ -378,6 +375,8 @@ fn realize(self: *Surface) !void { } pub fn deinit(self: *Surface) void { + if (self.title_text) |title| self.app.core_app.alloc.free(title); + // We don't allocate anything if we aren't realized. if (!self.realized) return; @@ -630,31 +629,29 @@ pub fn grabFocus(self: *Surface) void { fn updateTitleLabels(self: *Surface) void { // If we have no title, then we have nothing to update. - if (self.title_text_buf_len == 0) return; - const slice: []u8 = self.title_text_buf[0..self.title_text_buf_len]; + const title = self.title_text orelse return; // If we have a tab, then we have to update the tab if (self.container.tab()) |tab| { - c.gtk_label_set_text(tab.label_text, slice.ptr); + c.gtk_label_set_text(tab.label_text, title.ptr); } // If we have a window, then we have to update the window title. if (self.container.window()) |window| { - c.gtk_window_set_title(window.window, slice.ptr); + c.gtk_window_set_title(window.window, title.ptr); } } pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { - const len = @min(self.title_text_buf.len - 1, slice.len); - @memcpy(self.title_text_buf[0..len], slice[0..]); - // Null-terminate this because we then need to pass it to GTK. - self.title_text_buf[len] = 0; - self.title_text_buf_len = len; + const alloc = self.app.core_app.alloc; + const copy = try alloc.dupeZ(u8, slice); + errdefer alloc.free(copy); + + if (self.title_text) |old| alloc.free(old); + self.title_text = copy; const widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); - if (c.gtk_widget_is_focus(widget) == 1) { - self.updateTitleLabels(); - } + if (c.gtk_widget_is_focus(widget) == 1) self.updateTitleLabels(); } pub fn setMouseShape( From dc2cee1c78c6bd9f1cf44deec9990a424c352a93 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 09:15:30 -0800 Subject: [PATCH 71/74] apprt/gtk: rename parentSurface --- src/apprt/gtk/Surface.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 77a784bbb..720de99cb 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -164,8 +164,8 @@ pub const Container = union(enum) { /// surface has been initialized. realized: bool = false, -/// See Options.parentSurface -parentSurface: bool = false, +/// True if this surface had a parent to start with. +parent_surface: bool = false, /// The GUI container that this surface has been attached to. This /// dictates some behaviors such as new splits, etc. @@ -301,7 +301,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .title_text = null, .core_surface = undefined, .font_size = font_size, - .parentSurface = opts.parent != null, + .parent_surface = opts.parent != null, .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .im_context = im_context, @@ -350,8 +350,8 @@ fn realize(self: *Surface) !void { // Get our new surface config var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config); defer config.deinit(); - if (!self.parentSurface) { - // A hack, see the "parentSurface" field for more information. + if (!self.parent_surface) { + // A hack, see the "parent_surface" field for more information. config.@"working-directory" = self.app.config.@"working-directory"; } From f811ac6b18fb7e2dea9c876e4a0a60e1f3b66fb5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 09:58:57 -0800 Subject: [PATCH 72/74] apprt/gtk: gotoSplit, has some bugs --- src/apprt/gtk/Split.zig | 96 +++++++++++++++++++++++++++++++++++++++ src/apprt/gtk/Surface.zig | 11 +++++ 2 files changed, 107 insertions(+) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 93558dc36..fbeb748be 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -182,6 +182,102 @@ fn updateChildren(self: *const Split) void { ); } +/// A mapping of direction to the element (if any) in that direction. +pub const DirectionMap = std.EnumMap( + input.SplitFocusDirection, + ?*Surface, +); + +pub const Side = enum { top_left, bottom_right }; + +/// Returns the map that can be used to determine elements in various +/// directions (primarily for gotoSplit). +pub fn directionMap(self: *const Split, from: Side) DirectionMap { + return switch (from) { + .top_left => self.directionMapFromTopLeft(), + .bottom_right => self.directionMapFromBottomRight(), + }; +} + +fn directionMapFromTopLeft(self: *const Split) DirectionMap { + var result = DirectionMap.initFull(null); + + if (self.container.split()) |parent_split| { + const deepest_br = parent_split.deepestSurface(.bottom_right); + result.put(.previous, deepest_br); + + // This behavior matches the behavior of macOS at the time of writing + // this. There is an open issue (#524) to make this depend on the + // actual physical location of the current split. + result.put(.top, deepest_br); + result.put(.left, deepest_br); + } + + switch (self.bottom_right) { + .surface => |s| { + result.put(.next, s); + result.put(.bottom, s); + result.put(.right, s); + }, + + .split => |s| { + const deepest_tl = s.deepestSurface(.top_left); + result.put(.next, deepest_tl); + result.put(.bottom, deepest_tl); + result.put(.right, deepest_tl); + }, + } + + return result; +} + +fn directionMapFromBottomRight(self: *const Split) DirectionMap { + var result = DirectionMap.initFull(null); + + if (self.container.split()) |parent_split| { + const deepest_tl = parent_split.deepestSurface(.top_left); + result.put(.next, deepest_tl); + + // This behavior matches the behavior of macOS at the time of writing + // this. There is an open issue (#524) to make this depend on the + // actual physical location of the current split. + result.put(.top, deepest_tl); + result.put(.left, deepest_tl); + } + + switch (self.top_left) { + .surface => |s| { + result.put(.previous, s); + result.put(.bottom, s); + result.put(.right, s); + }, + + .split => |s| { + const deepest_br = s.deepestSurface(.bottom_right); + result.put(.previous, deepest_br); + result.put(.bottom, deepest_br); + result.put(.right, deepest_br); + }, + } + + return result; +} + +/// Get the most deeply nested surface for a given side. +fn deepestSurface(self: *const Split, side: Side) *Surface { + return switch (side) { + .bottom_right => switch (self.bottom_right) { + .surface => |s| s, + .split => |s| s.deepestSurface(.bottom_right), + }, + + .top_left => switch (self.top_left) { + .surface => |s| s, + .split => |s| s.deepestSurface(.top_left), + }, + }; +} + fn removeChildren(self: *const Split) void { c.gtk_paned_set_start_child(@ptrCast(self.paned), null); c.gtk_paned_set_end_child(@ptrCast(self.paned), null); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 720de99cb..fa50489a8 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -524,6 +524,17 @@ pub fn newSplit(self: *Surface, direction: input.SplitDirection) !void { _ = try Split.create(alloc, self, direction); } +pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void { + const s = self.container.split() orelse return; + const map = s.directionMap(switch (self.container) { + .split_tl => .top_left, + .split_br => .bottom_right, + .none, .tab_ => unreachable, + }); + const surface_ = map.get(direction) orelse return; + if (surface_) |surface| surface.grabFocus(); +} + pub fn newTab(self: *Surface) !void { const window = self.container.window() orelse { log.info("surface cannot create new tab when not attached to a window", .{}); From d311fb93ed7047e1697b87b41a9d3650e46eb5c4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 13:21:31 -0800 Subject: [PATCH 73/74] apprt/gtk: gotoSplit gets proper previous/next direction --- src/apprt/gtk/Split.zig | 103 +++++++++++++++----------------------- src/apprt/gtk/Surface.zig | 24 +++++++++ 2 files changed, 65 insertions(+), 62 deletions(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index fbeb748be..2f01aaa1d 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -193,89 +193,68 @@ pub const Side = enum { top_left, bottom_right }; /// Returns the map that can be used to determine elements in various /// directions (primarily for gotoSplit). pub fn directionMap(self: *const Split, from: Side) DirectionMap { - return switch (from) { - .top_left => self.directionMapFromTopLeft(), - .bottom_right => self.directionMapFromBottomRight(), - }; -} - -fn directionMapFromTopLeft(self: *const Split) DirectionMap { var result = DirectionMap.initFull(null); - if (self.container.split()) |parent_split| { - const deepest_br = parent_split.deepestSurface(.bottom_right); - result.put(.previous, deepest_br); + if (self.directionPrevious(from)) |prev| { + result.put(.previous, prev); // This behavior matches the behavior of macOS at the time of writing // this. There is an open issue (#524) to make this depend on the // actual physical location of the current split. - result.put(.top, deepest_br); - result.put(.left, deepest_br); + result.put(.top, prev); + result.put(.left, prev); } - switch (self.bottom_right) { - .surface => |s| { - result.put(.next, s); - result.put(.bottom, s); - result.put(.right, s); - }, - - .split => |s| { - const deepest_tl = s.deepestSurface(.top_left); - result.put(.next, deepest_tl); - result.put(.bottom, deepest_tl); - result.put(.right, deepest_tl); - }, + if (self.directionNext(from)) |next| { + result.put(.next, next); + result.put(.bottom, next); + result.put(.right, next); } return result; } -fn directionMapFromBottomRight(self: *const Split) DirectionMap { - var result = DirectionMap.initFull(null); +fn directionPrevious(self: *const Split, from: Side) ?*Surface { + switch (from) { + // From the bottom right, our previous is the deepest surface + // in the top-left of our own split. + .bottom_right => return self.top_left.deepestSurface(.bottom_right), - if (self.container.split()) |parent_split| { - const deepest_tl = parent_split.deepestSurface(.top_left); - result.put(.next, deepest_tl); + // From the top left its more complicated. It is the de + .top_left => { + // If we have no parent split then there can be no previous. + const parent = self.container.split() orelse return null; + const side = self.container.splitSide() orelse return null; - // This behavior matches the behavior of macOS at the time of writing - // this. There is an open issue (#524) to make this depend on the - // actual physical location of the current split. - result.put(.top, deepest_tl); - result.put(.left, deepest_tl); - } - - switch (self.top_left) { - .surface => |s| { - result.put(.previous, s); - result.put(.bottom, s); - result.put(.right, s); - }, - - .split => |s| { - const deepest_br = s.deepestSurface(.bottom_right); - result.put(.previous, deepest_br); - result.put(.bottom, deepest_br); - result.put(.right, deepest_br); + // The previous value is the previous of the side that we are. + return switch (side) { + .top_left => parent.directionPrevious(.top_left), + .bottom_right => parent.directionPrevious(.bottom_right), + }; }, } - - return result; } -/// Get the most deeply nested surface for a given side. -fn deepestSurface(self: *const Split, side: Side) *Surface { - return switch (side) { - .bottom_right => switch (self.bottom_right) { - .surface => |s| s, - .split => |s| s.deepestSurface(.bottom_right), - }, +fn directionNext(self: *const Split, from: Side) ?*Surface { + switch (from) { + // From the top left, our next is the earliest surface in the + // top-left direction of the bottom-right side of our split. Fun! + .top_left => return self.bottom_right.deepestSurface(.top_left), - .top_left => switch (self.top_left) { - .surface => |s| s, - .split => |s| s.deepestSurface(.top_left), + // From the bottom right is more compliated. It is the deepest + // (last) surface in the + .bottom_right => { + // If we have no parent split then there can be no next. + const parent = self.container.split() orelse return null; + const side = self.container.splitSide() orelse return null; + + // The previous value is the previous of the side that we are. + return switch (side) { + .top_left => parent.directionNext(.bottom_right), + .bottom_right => parent.directionNext(.bottom_right), + }; }, - }; + } } fn removeChildren(self: *const Split) void { diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index fa50489a8..266c67215 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -48,6 +48,9 @@ pub const Container = union(enum) { split_tl: *Elem, split_br: *Elem, + /// The side of the split. + pub const SplitSide = enum { top_left, bottom_right }; + /// Elem is the possible element of any container. A container can /// hold both a surface and a split. Any valid container should /// have an Elem value so that it can be properly used with @@ -92,6 +95,18 @@ pub const Container = union(enum) { .split => |s| s.grabFocus(), } } + + /// The last surface in this container in the direction specified. + /// Direction must be "top_left" or "bottom_right". + pub fn deepestSurface(self: Elem, side: SplitSide) ?*Surface { + return switch (self) { + .surface => |s| s, + .split => |s| (switch (side) { + .top_left => s.top_left, + .bottom_right => s.bottom_right, + }).deepestSurface(side), + }; + } }; /// Returns the window that this surface is attached to. @@ -127,6 +142,15 @@ pub const Container = union(enum) { }; } + /// The side that we are in the split. + pub fn splitSide(self: Container) ?SplitSide { + return switch (self) { + .none, .tab_ => null, + .split_tl => .top_left, + .split_br => .bottom_right, + }; + } + /// Replace the container's element with this element. This is /// used by children to modify their parents to for example change /// from a surface to a split or a split back to a surface or From bd49947f985f680e0ace5b19613081bcb7138497 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Dec 2023 13:45:56 -0800 Subject: [PATCH 74/74] apprt/gtk: fix next split issue --- src/apprt/gtk/Split.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 2f01aaa1d..7d6030c9b 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -250,7 +250,7 @@ fn directionNext(self: *const Split, from: Side) ?*Surface { // The previous value is the previous of the side that we are. return switch (side) { - .top_left => parent.directionNext(.bottom_right), + .top_left => parent.directionNext(.top_left), .bottom_right => parent.directionNext(.bottom_right), }; },