Add custom layout creation.

This commit is contained in:
Greyson Parrelli
2026-08-11 13:57:54 -04:00
parent a7a9429d09
commit 3026bdcd4c
14 changed files with 1964 additions and 20 deletions
+138 -2
View File
@@ -12,6 +12,7 @@ const std = @import("std");
const gtk = @import("gtk");
const Layout = @import("Layout.zig");
const Layouts = @import("Layouts.zig");
const Pane = @import("Pane.zig");
const Terminal = @import("Terminal.zig");
@@ -152,8 +153,8 @@ pub fn focus(self: *View) void {
/// Add a pane, splitting the focused one so the new pane appears beside
/// whatever you were working in.
pub fn addPane(self: *View, kind: Kind) !void {
const pane = try Pane.create(self.alloc, self, kind);
pub fn addPane(self: *View, spec: Pane.Spec) !void {
const pane = try Pane.create(self.alloc, self, spec);
errdefer pane.destroy();
const node = try self.layout.newLeaf(pane);
@@ -228,6 +229,141 @@ pub fn paneTitleChanged(self: *View, pane: *Pane) void {
if (self.focused == pane or self.panes.items.len == 1) self.on_title(self.ctx);
}
// -------------------------------------------------------------------------
// Layouts
/// Fill an empty view from a saved layout, substituting `bindings` into every
/// directory, script and URL as the panes are created.
///
/// Only valid on a view that has no panes yet — a layout describes a whole
/// tab, not an addition to one.
pub fn applyLayout(
self: *View,
spec: *const Layouts.Node,
bindings: []const Layouts.Binding,
) !void {
std.debug.assert(self.panes.items.len == 0);
const root = try self.buildNode(spec, bindings);
self.layout.root = root;
root.parent = null;
self.layout.materialize(self.box);
// The first pane in tree order is the top-left one, which is where you
// would start reading the tab and so where focus belongs.
if (self.panes.items.len > 0) {
self.setFocused(self.panes.items[0]);
self.panes.items[0].grabFocus();
}
self.on_title(self.ctx);
}
/// Build one subtree. Panes are appended to `self.panes` as they are created,
/// so a failure part-way leaves them owned by the view and torn down with it
/// rather than leaked.
fn buildNode(
self: *View,
spec: *const Layouts.Node,
bindings: []const Layouts.Binding,
) !*Layout.Node {
switch (spec.*) {
.pane => |p| {
const pane_spec = try self.paneSpec(p, bindings);
defer freePaneSpec(self.alloc, pane_spec);
const pane = try Pane.create(self.alloc, self, pane_spec);
errdefer pane.destroy();
const node = try self.layout.newLeaf(pane);
errdefer self.alloc.destroy(node);
try self.panes.append(self.alloc, pane);
return node;
},
.split => |s| {
const first = try self.buildNode(s.first, bindings);
const second = try self.buildNode(s.second, bindings);
return self.layout.newSplit(
switch (s.orientation) {
.horizontal => .horizontal,
.vertical => .vertical,
},
first,
second,
s.ratio,
);
},
}
}
/// Turn a layout's pane description into the spec `Pane.create` wants, with
/// parameters substituted. The strings are owned by the caller.
fn paneSpec(
self: *View,
p: Layouts.Pane,
bindings: []const Layouts.Binding,
) !Pane.Spec {
return switch (p.kind) {
.terminal => blk: {
const cwd = try Layouts.expandPath(self.alloc, p.cwd, bindings);
errdefer self.alloc.free(cwd);
const command = try Layouts.expand(self.alloc, p.command, bindings);
break :blk .{ .terminal = .{ .cwd = cwd, .command = command } };
},
.web => .{ .web = .{
.url = try Layouts.expand(self.alloc, p.url, bindings),
} },
};
}
fn freePaneSpec(alloc: std.mem.Allocator, spec: Pane.Spec) void {
switch (spec) {
.terminal => |o| {
alloc.free(o.cwd);
alloc.free(o.command);
},
.web => |o| alloc.free(o.url),
}
}
/// Capture this view's arrangement as a layout tree, for "save tab as layout".
///
/// The shape, split orientations and divider ratios come across exactly as
/// they are on screen. What each pane should *run* can't be known from a live
/// pane, so terminals come back with their current directory and an empty
/// command for the user to fill in; web panes bring their current URL.
pub fn capture(self: *View, builder: Layouts.Builder) !?*Layouts.Node {
const root = self.layout.root orelse return null;
return try self.captureNode(root, builder);
}
fn captureNode(self: *View, node: *Layout.Node, builder: Layouts.Builder) !*Layouts.Node {
return switch (node.kind) {
.leaf => |pane| switch (pane.content) {
.terminal => |t| blk: {
var buf: [std.fs.max_path_bytes]u8 = undefined;
break :blk try builder.pane(.{
.kind = .terminal,
.cwd = t.session.pty.cwd(&buf) orelse "",
});
},
.web => |b| try builder.pane(.{
.kind = .web,
.url = b.currentUrl(),
}),
},
.split => |s| try builder.split(
switch (s.paned.as(gtk.Orientable).getOrientation()) {
.vertical => .vertical,
else => .horizontal,
},
s.ratio,
try self.captureNode(s.a, builder),
try self.captureNode(s.b, builder),
),
};
}
// -------------------------------------------------------------------------
// Rearranging