apprt/gtk-ng: split zoom (#8217)

This makes `toggle_split_zoom` work via a new widget action
`split-tree.zoom`. The zoom state is tracked on the core `SplitTree`
data structure. Zoom state is propagated via a `is-zoomed` property on
the split tree in GTK.

I deferred the title changes since I can do that all at once with
subtitle and other things.
pull/8218/head
Mitchell Hashimoto 2025-08-12 13:43:44 -07:00 committed by GitHub
commit bede3d8011
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 397 additions and 98 deletions

View File

@ -615,12 +615,11 @@ pub const Application = extern struct {
.toggle_tab_overview => return Action.toggleTabOverview(target), .toggle_tab_overview => return Action.toggleTabOverview(target),
.toggle_window_decorations => return Action.toggleWindowDecorations(target), .toggle_window_decorations => return Action.toggleWindowDecorations(target),
.toggle_command_palette => return Action.toggleCommandPalette(target), .toggle_command_palette => return Action.toggleCommandPalette(target),
.toggle_split_zoom => return Action.toggleSplitZoom(target),
// Unimplemented but todo on gtk-ng branch // Unimplemented but todo on gtk-ng branch
.prompt_title, .prompt_title,
.inspector, .inspector,
// TODO: splits
.toggle_split_zoom,
=> { => {
log.warn("unimplemented action={}", .{action}); log.warn("unimplemented action={}", .{action});
return false; return false;
@ -2121,6 +2120,21 @@ const Action = struct {
return true; return true;
} }
pub fn toggleSplitZoom(target: apprt.Target) bool {
switch (target) {
.app => {
log.warn("toggle_split_zoom to app is unexpected", .{});
return false;
},
.surface => |core| {
// TODO: pass surface ID when we have that
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction("split-tree.zoom", null) != 0;
},
}
}
fn getQuickTerminalWindow() ?*Window { fn getQuickTerminalWindow() ?*Window {
// Find a quick terminal window. // Find a quick terminal window.
const list = gtk.Window.listToplevels(); const list = gtk.Window.listToplevels();

View File

@ -79,6 +79,25 @@ pub const SplitTree = extern struct {
); );
}; };
pub const @"is-zoomed" = struct {
pub const name = "is-zoomed";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{
.getter = getIsZoomed,
},
),
},
);
};
pub const tree = struct { pub const tree = struct {
pub const name = "tree"; pub const name = "tree";
const impl = gobject.ext.defineProperty( const impl = gobject.ext.defineProperty(
@ -165,6 +184,7 @@ pub const SplitTree = extern struct {
.{ "new-down", actionNewDown, null }, .{ "new-down", actionNewDown, null },
.{ "equalize", actionEqualize, null }, .{ "equalize", actionEqualize, null },
.{ "zoom", actionZoom, null },
}; };
// We need to collect our actions into a group since we're just // We need to collect our actions into a group since we're just
@ -241,7 +261,7 @@ pub const SplitTree = extern struct {
// The handle we create the split relative to. Today this is the active // The handle we create the split relative to. Today this is the active
// surface but this might be the handle of the given parent if we want. // surface but this might be the handle of the given parent if we want.
const handle = self.getActiveSurfaceHandle() orelse 0; const handle = self.getActiveSurfaceHandle() orelse .root;
// Create our split! // Create our split!
var new_tree = try old_tree.split( var new_tree = try old_tree.split(
@ -328,7 +348,7 @@ pub const SplitTree = extern struct {
if (active == target) return false; if (active == target) return false;
// Get the surface at the target location and grab focus. // Get the surface at the target location and grab focus.
const surface = tree.nodes[target].leaf; const surface = tree.nodes[target.idx()].leaf;
surface.grabFocus(); surface.grabFocus();
return true; return true;
@ -388,7 +408,7 @@ pub const SplitTree = extern struct {
pub fn getActiveSurface(self: *Self) ?*Surface { pub fn getActiveSurface(self: *Self) ?*Surface {
const tree = self.getTree() orelse return null; const tree = self.getTree() orelse return null;
const handle = self.getActiveSurfaceHandle() orelse return null; const handle = self.getActiveSurfaceHandle() orelse return null;
return tree.nodes[handle].leaf; return tree.nodes[handle.idx()].leaf;
} }
fn getActiveSurfaceHandle(self: *Self) ?Surface.Tree.Node.Handle { fn getActiveSurfaceHandle(self: *Self) ?Surface.Tree.Node.Handle {
@ -429,6 +449,11 @@ pub const SplitTree = extern struct {
return !tree.isEmpty(); return !tree.isEmpty();
} }
pub fn getIsZoomed(self: *Self) bool {
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
return tree.zoomed != null;
}
/// Get the tree data model that we're showing in this widget. This /// Get the tree data model that we're showing in this widget. This
/// does not clone the tree. /// does not clone the tree.
pub fn getTree(self: *Self) ?*Surface.Tree { pub fn getTree(self: *Self) ?*Surface.Tree {
@ -600,6 +625,23 @@ pub const SplitTree = extern struct {
self.setTree(&new_tree); self.setTree(&new_tree);
} }
pub fn actionZoom(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const tree = self.getTree() orelse return;
if (tree.zoomed != null) {
tree.zoomed = null;
} else {
const active = self.getActiveSurfaceHandle() orelse return;
if (tree.zoomed == active) return;
tree.zoom(active);
}
self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec);
}
fn surfaceCloseRequest( fn surfaceCloseRequest(
surface: *Surface, surface: *Surface,
scope: *const Surface.CloseScope, scope: *const Surface.CloseScope,
@ -679,7 +721,7 @@ pub const SplitTree = extern struct {
// Note: we don't need to ref this or anything because its // Note: we don't need to ref this or anything because its
// guaranteed to remain in the new tree since its not part // guaranteed to remain in the new tree since its not part
// of the handle we're removing. // of the handle we're removing.
break :next_focus old_tree.nodes[next_handle].leaf; break :next_focus old_tree.nodes[next_handle.idx()].leaf;
}; };
// Remove it from the tree. // Remove it from the tree.
@ -781,6 +823,7 @@ pub const SplitTree = extern struct {
// Dependent properties // Dependent properties
self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec); self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
} }
fn onRebuild(ud: ?*anyopaque) callconv(.c) c_int { fn onRebuild(ud: ?*anyopaque) callconv(.c) c_int {
@ -797,7 +840,10 @@ pub const SplitTree = extern struct {
// Rebuild our tree // Rebuild our tree
const tree: *const Surface.Tree = self.private().tree orelse &.empty; const tree: *const Surface.Tree = self.private().tree orelse &.empty;
if (!tree.isEmpty()) { if (!tree.isEmpty()) {
priv.tree_bin.setChild(self.buildTree(tree, 0)); priv.tree_bin.setChild(self.buildTree(
tree,
tree.zoomed orelse .root,
));
} }
// If we have a last focused surface, we need to refocus it, because // If we have a last focused surface, we need to refocus it, because
@ -823,7 +869,7 @@ pub const SplitTree = extern struct {
tree: *const Surface.Tree, tree: *const Surface.Tree,
current: Surface.Tree.Node.Handle, current: Surface.Tree.Node.Handle,
) *gtk.Widget { ) *gtk.Widget {
return switch (tree.nodes[current]) { return switch (tree.nodes[current.idx()]) {
.leaf => |v| v.as(gtk.Widget), .leaf => |v| v.as(gtk.Widget),
.split => |s| SplitTreeSplit.new( .split => |s| SplitTreeSplit.new(
current, current,
@ -982,7 +1028,7 @@ const SplitTreeSplit = extern struct {
self.as(gtk.Widget), self.as(gtk.Widget),
) orelse return 0; ) orelse return 0;
const tree = split_tree.getTree() orelse return 0; const tree = split_tree.getTree() orelse return 0;
const split: *const Surface.Tree.Split = &tree.nodes[priv.handle].split; const split: *const Surface.Tree.Split = &tree.nodes[priv.handle.idx()].split;
// Current, min, and max positions as pixels. // Current, min, and max positions as pixels.
const pos = paned.getPosition(); const pos = paned.getPosition();

View File

@ -56,6 +56,11 @@ pub fn SplitTree(comptime V: type) type {
/// All the nodes in the tree. Node at index 0 is always the root. /// All the nodes in the tree. Node at index 0 is always the root.
nodes: []const Node, nodes: []const Node,
/// The handle of the zoomed node. A "zoomed" node is one that is
/// expected to be made the full size of the split tree. Various
/// operations may unzoom (e.g. resize).
zoomed: ?Node.Handle,
/// An empty tree. /// An empty tree.
pub const empty: Self = .{ pub const empty: Self = .{
// Arena can be undefined because we have zero allocated nodes. // Arena can be undefined because we have zero allocated nodes.
@ -63,6 +68,7 @@ pub fn SplitTree(comptime V: type) type {
// arena. // arena.
.arena = undefined, .arena = undefined,
.nodes = &.{}, .nodes = &.{},
.zoomed = null,
}; };
pub const Node = union(enum) { pub const Node = union(enum) {
@ -72,7 +78,24 @@ pub fn SplitTree(comptime V: type) type {
/// A handle into the nodes array. This lets us keep track of /// A handle into the nodes array. This lets us keep track of
/// nodes with 16-bit handles rather than full pointer-width /// nodes with 16-bit handles rather than full pointer-width
/// values. /// values.
pub const Handle = u16; pub const Handle = enum(Backing) {
root = 0,
_,
pub const Backing = u16;
pub inline fn idx(self: Handle) usize {
return @intFromEnum(self);
}
/// Offset the handle by a given amount.
pub fn offset(self: Handle, v: usize) Handle {
const self_usize: usize = @intCast(@intFromEnum(self));
const final = self_usize + v;
assert(final < std.math.maxInt(Backing));
return @enumFromInt(final);
}
};
}; };
pub const Split = struct { pub const Split = struct {
@ -98,6 +121,7 @@ pub fn SplitTree(comptime V: type) type {
return .{ return .{
.arena = arena, .arena = arena,
.nodes = nodes, .nodes = nodes,
.zoomed = null,
}; };
} }
@ -136,6 +160,7 @@ pub fn SplitTree(comptime V: type) type {
return .{ return .{
.arena = arena, .arena = arena,
.nodes = nodes, .nodes = nodes,
.zoomed = self.zoomed,
}; };
} }
@ -158,17 +183,17 @@ pub fn SplitTree(comptime V: type) type {
}; };
pub const Iterator = struct { pub const Iterator = struct {
i: Node.Handle = 0, i: Node.Handle = .root,
nodes: []const Node, nodes: []const Node,
pub fn next(self: *Iterator) ?ViewEntry { pub fn next(self: *Iterator) ?ViewEntry {
// If we have no nodes, return null. // If we have no nodes, return null.
if (self.i >= self.nodes.len) return null; if (@intFromEnum(self.i) >= self.nodes.len) return null;
// Get the current node and increment the index. // Get the current node and increment the index.
const handle = self.i; const handle = self.i;
self.i += 1; self.i = @enumFromInt(handle.idx() + 1);
const node = self.nodes[handle]; const node = self.nodes[handle.idx()];
return switch (node) { return switch (node) {
.leaf => |v| .{ .handle = handle, .view = v }, .leaf => |v| .{ .handle = handle, .view = v },
@ -177,6 +202,16 @@ pub fn SplitTree(comptime V: type) type {
} }
}; };
/// Change the zoomed state to the given node. Assumes the handle
/// is valid.
pub fn zoom(self: *Self, handle: ?Node.Handle) void {
if (handle) |v| {
assert(@intFromEnum(v) >= 0);
assert(@intFromEnum(v) < self.nodes.len);
}
self.zoomed = handle;
}
pub const Goto = union(enum) { pub const Goto = union(enum) {
/// Previous view, null if we're the first view. /// Previous view, null if we're the first view.
previous, previous,
@ -211,8 +246,8 @@ pub fn SplitTree(comptime V: type) type {
return switch (to) { return switch (to) {
.previous => self.previous(from), .previous => self.previous(from),
.next => self.next(from), .next => self.next(from),
.previous_wrapped => self.previous(from) orelse self.deepest(.right, 0), .previous_wrapped => self.previous(from) orelse self.deepest(.right, .root),
.next_wrapped => self.next(from) orelse self.deepest(.left, 0), .next_wrapped => self.next(from) orelse self.deepest(.left, .root),
.spatial => |d| spatial: { .spatial => |d| spatial: {
// Get our spatial representation. // Get our spatial representation.
var sp = try self.spatial(alloc); var sp = try self.spatial(alloc);
@ -234,7 +269,7 @@ pub fn SplitTree(comptime V: type) type {
) Node.Handle { ) Node.Handle {
var current: Node.Handle = from; var current: Node.Handle = from;
while (true) { while (true) {
switch (self.nodes[current]) { switch (self.nodes[current.idx()]) {
.leaf => return current, .leaf => return current,
.split => |s| current = switch (side) { .split => |s| current = switch (side) {
.left => s.left, .left => s.left,
@ -253,7 +288,7 @@ pub fn SplitTree(comptime V: type) type {
/// may want to change this to something that better matches a /// may want to change this to something that better matches a
/// spatial view of the tree later. /// spatial view of the tree later.
fn previous(self: *const Self, from: Node.Handle) ?Node.Handle { fn previous(self: *const Self, from: Node.Handle) ?Node.Handle {
return switch (self.previousBacktrack(from, 0)) { return switch (self.previousBacktrack(from, .root)) {
.result => |v| v, .result => |v| v,
.backtrack, .deadend => null, .backtrack, .deadend => null,
}; };
@ -261,7 +296,7 @@ pub fn SplitTree(comptime V: type) type {
/// Same as `previous`, but returns the next view instead. /// Same as `previous`, but returns the next view instead.
fn next(self: *const Self, from: Node.Handle) ?Node.Handle { fn next(self: *const Self, from: Node.Handle) ?Node.Handle {
return switch (self.nextBacktrack(from, 0)) { return switch (self.nextBacktrack(from, .root)) {
.result => |v| v, .result => |v| v,
.backtrack, .deadend => null, .backtrack, .deadend => null,
}; };
@ -286,7 +321,7 @@ pub fn SplitTree(comptime V: type) type {
// value of, then we need to backtrack from here. // value of, then we need to backtrack from here.
if (from == current) return .backtrack; if (from == current) return .backtrack;
return switch (self.nodes[current]) { return switch (self.nodes[current.idx()]) {
// If we hit a leaf that isn't our target, then deadend. // If we hit a leaf that isn't our target, then deadend.
.leaf => .deadend, .leaf => .deadend,
@ -322,7 +357,7 @@ pub fn SplitTree(comptime V: type) type {
current: Node.Handle, current: Node.Handle,
) Backtrack { ) Backtrack {
if (from == current) return .backtrack; if (from == current) return .backtrack;
return switch (self.nodes[current]) { return switch (self.nodes[current.idx()]) {
.leaf => .deadend, .leaf => .deadend,
.split => |s| switch (self.nextBacktrack(from, s.right)) { .split => |s| switch (self.nextBacktrack(from, s.right)) {
.result => |v| .{ .result = v }, .result => |v| .{ .result = v },
@ -343,7 +378,7 @@ pub fn SplitTree(comptime V: type) type {
from: Node.Handle, from: Node.Handle,
direction: Spatial.Direction, direction: Spatial.Direction,
) ?Node.Handle { ) ?Node.Handle {
const target = sp.slots[from]; const target = sp.slots[from.idx()];
var result: ?struct { var result: ?struct {
handle: Node.Handle, handle: Node.Handle,
@ -351,7 +386,7 @@ pub fn SplitTree(comptime V: type) type {
} = null; } = null;
for (sp.slots, 0..) |slot, handle| { for (sp.slots, 0..) |slot, handle| {
// Never match ourself // Never match ourself
if (handle == from) continue; if (handle == from.idx()) continue;
// Only match leaves // Only match leaves
switch (self.nodes[handle]) { switch (self.nodes[handle]) {
@ -377,7 +412,7 @@ pub fn SplitTree(comptime V: type) type {
if (distance >= n.distance) continue; if (distance >= n.distance) continue;
} }
result = .{ result = .{
.handle = @intCast(handle), .handle = @enumFromInt(handle),
.distance = distance, .distance = distance,
}; };
} }
@ -402,7 +437,7 @@ pub fn SplitTree(comptime V: type) type {
// who directly access the nodes to be able to modify them // who directly access the nodes to be able to modify them
// (without nasty stuff like this), but given this is internal // (without nasty stuff like this), but given this is internal
// usage its perfectly fine to modify the node in-place. // usage its perfectly fine to modify the node in-place.
const s: *Split = @constCast(&self.nodes[at].split); const s: *Split = @constCast(&self.nodes[at.idx()].split);
s.ratio = ratio; s.ratio = ratio;
} }
@ -430,7 +465,7 @@ pub fn SplitTree(comptime V: type) type {
// We know we're going to need the sum total of the nodes // We know we're going to need the sum total of the nodes
// between the two trees plus one for the new split node. // between the two trees plus one for the new split node.
const nodes = try alloc.alloc(Node, self.nodes.len + insert.nodes.len + 1); const nodes = try alloc.alloc(Node, self.nodes.len + insert.nodes.len + 1);
if (nodes.len > std.math.maxInt(Node.Handle)) return error.OutOfMemory; if (nodes.len > std.math.maxInt(Node.Handle.Backing)) return error.OutOfMemory;
// We can copy our nodes exactly as they are, since they're // We can copy our nodes exactly as they are, since they're
// mostly not changing (only `at` is changing). // mostly not changing (only `at` is changing).
@ -446,8 +481,8 @@ pub fn SplitTree(comptime V: type) type {
.leaf => {}, .leaf => {},
.split => |*s| { .split => |*s| {
// We need to offset the handles in the split // We need to offset the handles in the split
s.left += @intCast(self.nodes.len); s.left = s.left.offset(self.nodes.len);
s.right += @intCast(self.nodes.len); s.right = s.right.offset(self.nodes.len);
}, },
}; };
@ -461,18 +496,23 @@ pub fn SplitTree(comptime V: type) type {
// Copy our previous value to the end of the nodes list and // Copy our previous value to the end of the nodes list and
// create our new split node. // create our new split node.
nodes[nodes.len - 1] = nodes[at]; nodes[nodes.len - 1] = nodes[at.idx()];
nodes[at] = .{ .split = .{ nodes[at.idx()] = .{ .split = .{
.layout = layout, .layout = layout,
.ratio = ratio, .ratio = ratio,
.left = @intCast(if (left) self.nodes.len else nodes.len - 1), .left = @enumFromInt(if (left) self.nodes.len else nodes.len - 1),
.right = @intCast(if (left) nodes.len - 1 else self.nodes.len), .right = @enumFromInt(if (left) nodes.len - 1 else self.nodes.len),
} }; } };
// We need to increase the reference count of all the nodes. // We need to increase the reference count of all the nodes.
try refNodes(gpa, nodes); try refNodes(gpa, nodes);
return .{ .arena = arena, .nodes = nodes }; return .{
.arena = arena,
.nodes = nodes,
// Splitting always resets zoom state.
.zoomed = null,
};
} }
/// Remove a node from the tree. /// Remove a node from the tree.
@ -481,10 +521,10 @@ pub fn SplitTree(comptime V: type) type {
gpa: Allocator, gpa: Allocator,
at: Node.Handle, at: Node.Handle,
) Allocator.Error!Self { ) Allocator.Error!Self {
assert(at < self.nodes.len); assert(at.idx() < self.nodes.len);
// If we're removing node zero then we're clearing the tree. // If we're removing node zero then we're clearing the tree.
if (at == 0) return .empty; if (at == .root) return .empty;
// The new arena for our new tree. // The new arena for our new tree.
var arena = ArenaAllocator.init(gpa); var arena = ArenaAllocator.init(gpa);
@ -494,43 +534,61 @@ pub fn SplitTree(comptime V: type) type {
// Allocate our new nodes list with the number of nodes we'll // Allocate our new nodes list with the number of nodes we'll
// need after the removal. // need after the removal.
const nodes = try alloc.alloc(Node, self.countAfterRemoval( const nodes = try alloc.alloc(Node, self.countAfterRemoval(
0, .root,
at, at,
0, 0,
)); ));
var result: Self = .{
.arena = arena,
.nodes = nodes,
.zoomed = null,
};
// Traverse the tree and copy all our nodes into place. // Traverse the tree and copy all our nodes into place.
assert(self.removeNode( assert(self.removeNode(
nodes, &result,
0,
0, 0,
.root,
at, at,
) > 0); ) != 0);
// Increase the reference count of all the nodes. // Increase the reference count of all the nodes.
try refNodes(gpa, nodes); try refNodes(gpa, nodes);
return .{ return result;
.arena = arena,
.nodes = nodes,
};
} }
fn removeNode( fn removeNode(
self: *Self, old: *Self,
nodes: []Node, new: *Self,
new_offset: Node.Handle, new_offset: usize,
current: Node.Handle, current: Node.Handle,
target: Node.Handle, target: Node.Handle,
) Node.Handle { ) usize {
assert(current != target); assert(current != target);
switch (self.nodes[current]) { // If we have a zoomed node and this is it then we migrate it.
if (old.zoomed) |v| {
if (v == current) {
assert(new.zoomed == null);
new.zoomed = @enumFromInt(new_offset);
}
}
// Let's talk about this constCast. Our member are const but
// we actually always own their memory. We don't want consumers
// who directly access the nodes to be able to modify them
// (without nasty stuff like this), but given this is internal
// usage its perfectly fine to modify the node in-place.
const new_nodes: []Node = @constCast(new.nodes);
switch (old.nodes[current.idx()]) {
// Leaf is simple, just copy it over. We don't ref anything // Leaf is simple, just copy it over. We don't ref anything
// yet because it'd make undo (errdefer) harder. We do that // yet because it'd make undo (errdefer) harder. We do that
// all at once later. // all at once later.
.leaf => |view| { .leaf => |view| {
nodes[new_offset] = .{ .leaf = view }; new_nodes[new_offset] = .{ .leaf = view };
return 1; return 1;
}, },
@ -538,39 +596,39 @@ pub fn SplitTree(comptime V: type) type {
// If we're removing one of the split node sides then // If we're removing one of the split node sides then
// we remove the split node itself as well and only add // we remove the split node itself as well and only add
// the other (non-removed) side. // the other (non-removed) side.
if (s.left == target) return self.removeNode( if (s.left == target) return old.removeNode(
nodes, new,
new_offset, new_offset,
s.right, s.right,
target, target,
); );
if (s.right == target) return self.removeNode( if (s.right == target) return old.removeNode(
nodes, new,
new_offset, new_offset,
s.left, s.left,
target, target,
); );
// Neither side is being directly removed, so we traverse. // Neither side is being directly removed, so we traverse.
const left = self.removeNode( const left = old.removeNode(
nodes, new,
new_offset + 1, new_offset + 1,
s.left, s.left,
target, target,
); );
assert(left > 0); assert(left != 0);
const right = self.removeNode( const right = old.removeNode(
nodes, new,
new_offset + 1 + left, new_offset + left + 1,
s.right, s.right,
target, target,
); );
assert(right > 0); assert(right != 0);
nodes[new_offset] = .{ .split = .{ new_nodes[new_offset] = .{ .split = .{
.layout = s.layout, .layout = s.layout,
.ratio = s.ratio, .ratio = s.ratio,
.left = new_offset + 1, .left = @enumFromInt(new_offset + 1),
.right = new_offset + 1 + left, .right = @enumFromInt(new_offset + 1 + left),
} }; } };
return left + right + 1; return left + right + 1;
@ -588,7 +646,7 @@ pub fn SplitTree(comptime V: type) type {
) usize { ) usize {
assert(current != target); assert(current != target);
return switch (self.nodes[current]) { return switch (self.nodes[current.idx()]) {
// Leaf is simple, always takes one node. // Leaf is simple, always takes one node.
.leaf => acc + 1, .leaf => acc + 1,
@ -679,6 +737,7 @@ pub fn SplitTree(comptime V: type) type {
return .{ return .{
.arena = arena, .arena = arena,
.nodes = nodes, .nodes = nodes,
.zoomed = self.zoomed,
}; };
} }
@ -688,7 +747,7 @@ pub fn SplitTree(comptime V: type) type {
layout: Split.Layout, layout: Split.Layout,
acc: usize, acc: usize,
) usize { ) usize {
return switch (self.nodes[from]) { return switch (self.nodes[from.idx()]) {
.leaf => acc + 1, .leaf => acc + 1,
.split => |s| if (s.layout == layout) .split => |s| if (s.layout == layout)
self.weight(s.left, layout, acc) + self.weight(s.left, layout, acc) +
@ -737,7 +796,7 @@ pub fn SplitTree(comptime V: type) type {
const parent_handle = switch (self.findParentSplit( const parent_handle = switch (self.findParentSplit(
layout, layout,
from, from,
0, .root,
)) { )) {
.deadend, .backtrack => return result, .deadend, .backtrack => return result,
.result => |v| v, .result => |v| v,
@ -755,11 +814,11 @@ pub fn SplitTree(comptime V: type) type {
// own but I'm trying to avoid that word: its the ratio of // own but I'm trying to avoid that word: its the ratio of
// our spatial width/height to the total. // our spatial width/height to the total.
const scale = switch (layout) { const scale = switch (layout) {
.horizontal => sp.slots[parent_handle].width / sp.slots[0].width, .horizontal => sp.slots[parent_handle.idx()].width / sp.slots[0].width,
.vertical => sp.slots[parent_handle].height / sp.slots[0].height, .vertical => sp.slots[parent_handle.idx()].height / sp.slots[0].height,
}; };
const current = result.nodes[parent_handle].split.ratio; const current = result.nodes[parent_handle.idx()].split.ratio;
break :full_ratio current * scale; break :full_ratio current * scale;
}; };
@ -778,7 +837,7 @@ pub fn SplitTree(comptime V: type) type {
current: Node.Handle, current: Node.Handle,
) Backtrack { ) Backtrack {
if (from == current) return .backtrack; if (from == current) return .backtrack;
return switch (self.nodes[current]) { return switch (self.nodes[current.idx()]) {
.leaf => .deadend, .leaf => .deadend,
.split => |s| switch (self.findParentSplit( .split => |s| switch (self.findParentSplit(
layout, layout,
@ -861,7 +920,7 @@ pub fn SplitTree(comptime V: type) type {
if (self.nodes.len == 0) return .empty; if (self.nodes.len == 0) return .empty;
// Get our total dimensions. // Get our total dimensions.
const dim = self.dimensions(0); const dim = self.dimensions(.root);
// Create our slots which will match our nodes exactly. // Create our slots which will match our nodes exactly.
const slots = try alloc.alloc(Spatial.Slot, self.nodes.len); const slots = try alloc.alloc(Spatial.Slot, self.nodes.len);
@ -872,7 +931,7 @@ pub fn SplitTree(comptime V: type) type {
.width = @floatFromInt(dim.width), .width = @floatFromInt(dim.width),
.height = @floatFromInt(dim.height), .height = @floatFromInt(dim.height),
}; };
self.fillSpatialSlots(slots, 0); self.fillSpatialSlots(slots, .root);
// Normalize the dimensions to 1x1 grid. // Normalize the dimensions to 1x1 grid.
for (slots) |*slot| { for (slots) |*slot| {
@ -888,10 +947,10 @@ pub fn SplitTree(comptime V: type) type {
fn fillSpatialSlots( fn fillSpatialSlots(
self: *const Self, self: *const Self,
slots: []Spatial.Slot, slots: []Spatial.Slot,
current: Node.Handle, current_: Node.Handle,
) void { ) void {
const current = current_.idx();
assert(slots[current].width >= 0 and slots[current].height >= 0); assert(slots[current].width >= 0 and slots[current].height >= 0);
switch (self.nodes[current]) { switch (self.nodes[current]) {
// Leaf node, current slot is already filled by caller. // Leaf node, current slot is already filled by caller.
.leaf => {}, .leaf => {},
@ -899,13 +958,13 @@ pub fn SplitTree(comptime V: type) type {
.split => |s| { .split => |s| {
switch (s.layout) { switch (s.layout) {
.horizontal => { .horizontal => {
slots[s.left] = .{ slots[s.left.idx()] = .{
.x = slots[current].x, .x = slots[current].x,
.y = slots[current].y, .y = slots[current].y,
.width = slots[current].width * s.ratio, .width = slots[current].width * s.ratio,
.height = slots[current].height, .height = slots[current].height,
}; };
slots[s.right] = .{ slots[s.right.idx()] = .{
.x = slots[current].x + slots[current].width * s.ratio, .x = slots[current].x + slots[current].width * s.ratio,
.y = slots[current].y, .y = slots[current].y,
.width = slots[current].width * (1 - s.ratio), .width = slots[current].width * (1 - s.ratio),
@ -914,13 +973,13 @@ pub fn SplitTree(comptime V: type) type {
}, },
.vertical => { .vertical => {
slots[s.left] = .{ slots[s.left.idx()] = .{
.x = slots[current].x, .x = slots[current].x,
.y = slots[current].y, .y = slots[current].y,
.width = slots[current].width, .width = slots[current].width,
.height = slots[current].height * s.ratio, .height = slots[current].height * s.ratio,
}; };
slots[s.right] = .{ slots[s.right.idx()] = .{
.x = slots[current].x, .x = slots[current].x,
.y = slots[current].y + slots[current].height * s.ratio, .y = slots[current].y + slots[current].height * s.ratio,
.width = slots[current].width, .width = slots[current].width,
@ -943,7 +1002,7 @@ pub fn SplitTree(comptime V: type) type {
width: u16, width: u16,
height: u16, height: u16,
} { } {
return switch (self.nodes[current]) { return switch (self.nodes[current.idx()]) {
.leaf => .{ .width = 1, .height = 1 }, .leaf => .{ .width = 1, .height = 1 },
.split => |s| split: { .split => |s| split: {
const left = self.dimensions(s.left); const left = self.dimensions(s.left);
@ -988,10 +1047,10 @@ pub fn SplitTree(comptime V: type) type {
self.formatDiagram(writer) catch self.formatDiagram(writer) catch
try writer.writeAll("failed to draw split tree diagram"); try writer.writeAll("failed to draw split tree diagram");
} else if (std.mem.eql(u8, fmt, "text")) { } else if (std.mem.eql(u8, fmt, "text")) {
try self.formatText(writer, 0, 0); try self.formatText(writer, .root, 0);
} else if (fmt.len == 0) { } else if (fmt.len == 0) {
self.formatDiagram(writer) catch {}; self.formatDiagram(writer) catch {};
try self.formatText(writer, 0, 0); try self.formatText(writer, .root, 0);
} else { } else {
return error.InvalidFormat; return error.InvalidFormat;
} }
@ -1005,7 +1064,11 @@ pub fn SplitTree(comptime V: type) type {
) !void { ) !void {
for (0..depth) |_| try writer.writeAll(" "); for (0..depth) |_| try writer.writeAll(" ");
switch (self.nodes[current]) { if (self.zoomed) |zoomed| if (zoomed == current) {
try writer.writeAll("(zoomed) ");
};
switch (self.nodes[current.idx()]) {
.leaf => |v| if (@hasDecl(View, "splitTreeLabel")) .leaf => |v| if (@hasDecl(View, "splitTreeLabel"))
try writer.print("leaf: {s}\n", .{v.splitTreeLabel()}) try writer.print("leaf: {s}\n", .{v.splitTreeLabel()})
else else
@ -1312,7 +1375,7 @@ test "SplitTree: split horizontal" {
defer t2.deinit(); defer t2.deinit();
var t3 = try t1.split( var t3 = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.right, // split right .right, // split right
0.5, 0.5,
&t2, // insert t2 &t2, // insert t2
@ -1416,7 +1479,7 @@ test "SplitTree: split horizontal" {
} else return error.NotFound, } else return error.NotFound,
).?; ).?;
const entry = t5.nodes[handle].leaf; const entry = t5.nodes[handle.idx()].leaf;
try testing.expectEqualStrings( try testing.expectEqualStrings(
entry.label, entry.label,
&.{current - 1}, &.{current - 1},
@ -1446,7 +1509,7 @@ test "SplitTree: split horizontal" {
} else return error.NotFound, } else return error.NotFound,
).?; ).?;
const entry = t5.nodes[handle].leaf; const entry = t5.nodes[handle.idx()].leaf;
try testing.expectEqualStrings( try testing.expectEqualStrings(
entry.label, entry.label,
&.{current + 1}, &.{current + 1},
@ -1477,7 +1540,7 @@ test "SplitTree: split vertical" {
var t3 = try t1.split( var t3 = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.down, // split down .down, // split down
0.5, 0.5,
&t2, // insert t2 &t2, // insert t2
@ -1511,7 +1574,7 @@ test "SplitTree: split horizontal with zero ratio" {
// A | B horizontal // A | B horizontal
var splitAB = try t1.split( var splitAB = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.right, // split right .right, // split right
0, 0,
&t2, // insert t2 &t2, // insert t2
@ -1545,7 +1608,7 @@ test "SplitTree: split vertical with zero ratio" {
// A | B horizontal // A | B horizontal
var splitAB = try t1.split( var splitAB = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.down, // split right .down, // split right
0, 0,
&t2, // insert t2 &t2, // insert t2
@ -1579,7 +1642,7 @@ test "SplitTree: split horizontal with full width" {
// A | B horizontal // A | B horizontal
var splitAB = try t1.split( var splitAB = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.right, // split right .right, // split right
1, 1,
&t2, // insert t2 &t2, // insert t2
@ -1613,7 +1676,7 @@ test "SplitTree: split vertical with full width" {
// A | B horizontal // A | B horizontal
var splitAB = try t1.split( var splitAB = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.down, // split right .down, // split right
1, 1,
&t2, // insert t2 &t2, // insert t2
@ -1645,7 +1708,7 @@ test "SplitTree: remove leaf" {
defer t2.deinit(); defer t2.deinit();
var t3 = try t1.split( var t3 = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.right, // split right .right, // split right
0.5, 0.5,
&t2, // insert t2 &t2, // insert t2
@ -1691,7 +1754,7 @@ test "SplitTree: split twice, remove intermediary" {
// A | B horizontal. // A | B horizontal.
var split1 = try t1.split( var split1 = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.right, // split right .right, // split right
0.5, 0.5,
&t2, // insert t2 &t2, // insert t2
@ -1701,7 +1764,7 @@ test "SplitTree: split twice, remove intermediary" {
// Insert C below that. // Insert C below that.
var split2 = try split1.split( var split2 = try split1.split(
alloc, alloc,
0, // at root .root, // at root
.down, // split down .down, // split down
0.5, 0.5,
&t3, // insert t3 &t3, // insert t3
@ -1752,7 +1815,7 @@ test "SplitTree: split twice, remove intermediary" {
// never crash. We don't test the result is correct, this just verifies // never crash. We don't test the result is correct, this just verifies
// we don't hit any assertion failures. // we don't hit any assertion failures.
for (0..split2.nodes.len) |i| { for (0..split2.nodes.len) |i| {
var t = try split2.remove(alloc, @intCast(i)); var t = try split2.remove(alloc, @enumFromInt(i));
t.deinit(); t.deinit();
} }
} }
@ -1777,7 +1840,7 @@ test "SplitTree: spatial goto" {
// A | B horizontal // A | B horizontal
var splitAB = try t1.split( var splitAB = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.right, // split right .right, // split right
0.5, 0.5,
&t2, // insert t2 &t2, // insert t2
@ -1853,7 +1916,7 @@ test "SplitTree: spatial goto" {
}, },
.{ .spatial = .right }, .{ .spatial = .right },
)).?; )).?;
const view = split.nodes[target].leaf; const view = split.nodes[target.idx()].leaf;
try testing.expectEqualStrings(view.label, "D"); try testing.expectEqualStrings(view.label, "D");
} }
@ -1871,7 +1934,7 @@ test "SplitTree: spatial goto" {
}, },
.{ .spatial = .left }, .{ .spatial = .left },
)).?; )).?;
const view = split.nodes[target].leaf; const view = split.nodes[target.idx()].leaf;
try testing.expectEqualStrings("A", view.label); try testing.expectEqualStrings("A", view.label);
} }
@ -1908,7 +1971,7 @@ test "SplitTree: resize" {
// A | B horizontal // A | B horizontal
var split = try t1.split( var split = try t1.split(
alloc, alloc,
0, // at root .root, // at root
.right, // split right .right, // split right
0.5, 0.5,
&t2, // insert t2 &t2, // insert t2
@ -1970,3 +2033,179 @@ test "SplitTree: clone empty tree" {
); );
} }
} }
test "SplitTree: zoom" {
const testing = std.testing;
const alloc = testing.allocator;
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
// A | B horizontal
var split = try t1.split(
alloc,
.root, // at root
.right, // split right
0.5,
&t2, // insert t2
);
defer split.deinit();
split.zoom(at: {
var it = split.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "B")) {
break entry.handle;
}
} else return error.NotFound;
});
{
const str = try std.fmt.allocPrint(alloc, "{text}", .{split});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\split (layout: horizontal, ratio: 0.50)
\\ leaf: A
\\ (zoomed) leaf: B
\\
);
}
// Clone preserves zoom
var clone = try split.clone(alloc);
defer clone.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{text}", .{clone});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\split (layout: horizontal, ratio: 0.50)
\\ leaf: A
\\ (zoomed) leaf: B
\\
);
}
}
test "SplitTree: split resets zoom" {
const testing = std.testing;
const alloc = testing.allocator;
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
// Zoom A
t1.zoom(at: {
var it = t1.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "A")) {
break entry.handle;
}
} else return error.NotFound;
});
// A | B horizontal
var split = try t1.split(
alloc,
.root, // at root
.right, // split right
0.5,
&t2, // insert t2
);
defer split.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{text}", .{split});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\split (layout: horizontal, ratio: 0.50)
\\ leaf: A
\\ leaf: B
\\
);
}
}
test "SplitTree: remove and zoom" {
const testing = std.testing;
const alloc = testing.allocator;
var v1: TestTree.View = .{ .label = "A" };
var t1: TestTree = try .init(alloc, &v1);
defer t1.deinit();
var v2: TestTree.View = .{ .label = "B" };
var t2: TestTree = try .init(alloc, &v2);
defer t2.deinit();
// A | B horizontal
var split = try t1.split(
alloc,
.root, // at root
.right, // split right
0.5,
&t2, // insert t2
);
defer split.deinit();
split.zoom(at: {
var it = split.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "A")) {
break entry.handle;
}
} else return error.NotFound;
});
// Remove A, should unzoom
{
var removed = try split.remove(
alloc,
at: {
var it = split.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "A")) {
break entry.handle;
}
} else return error.NotFound;
},
);
defer removed.deinit();
try testing.expect(removed.zoomed == null);
const str = try std.fmt.allocPrint(alloc, "{text}", .{removed});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\leaf: B
\\
);
}
// Remove B, should keep zoom
{
var removed = try split.remove(
alloc,
at: {
var it = split.iterator();
break :at while (it.next()) |entry| {
if (std.mem.eql(u8, entry.view.label, "B")) {
break entry.handle;
}
} else return error.NotFound;
},
);
defer removed.deinit();
const str = try std.fmt.allocPrint(alloc, "{text}", .{removed});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\(zoomed) leaf: A
\\
);
}
}