From 3e767c166c6f2b87105d6e48b48eeeed9df94cdf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 6 Aug 2025 07:29:20 -0700 Subject: [PATCH] datastruct: split tree node removal --- src/apprt/gtk-ng.zig | 1 - src/datastruct/main.zig | 2 + .../gtk-ng => datastruct}/split_tree.zig | 384 +++++++++++++++++- 3 files changed, 367 insertions(+), 20 deletions(-) rename src/{apprt/gtk-ng => datastruct}/split_tree.zig (63%) diff --git a/src/apprt/gtk-ng.zig b/src/apprt/gtk-ng.zig index 8f0489b80..de9255fe9 100644 --- a/src/apprt/gtk-ng.zig +++ b/src/apprt/gtk-ng.zig @@ -7,7 +7,6 @@ pub const resourcesDir = internal_os.resourcesDir; // The exported API, custom for the apprt. pub const class = @import("gtk-ng/class.zig"); -pub const SplitTree = @import("gtk-ng/split_tree.zig").SplitTree; pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef; test { diff --git a/src/datastruct/main.zig b/src/datastruct/main.zig index 4f45f9483..14ee0e504 100644 --- a/src/datastruct/main.zig +++ b/src/datastruct/main.zig @@ -6,6 +6,7 @@ const cache_table = @import("cache_table.zig"); const circ_buf = @import("circ_buf.zig"); const intrusive_linked_list = @import("intrusive_linked_list.zig"); const segmented_pool = @import("segmented_pool.zig"); +const split_tree = @import("split_tree.zig"); pub const lru = @import("lru.zig"); pub const BlockingQueue = blocking_queue.BlockingQueue; @@ -13,6 +14,7 @@ pub const CacheTable = cache_table.CacheTable; pub const CircBuf = circ_buf.CircBuf; pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList; pub const SegmentedPool = segmented_pool.SegmentedPool; +pub const SplitTree = split_tree.SplitTree; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/apprt/gtk-ng/split_tree.zig b/src/datastruct/split_tree.zig similarity index 63% rename from src/apprt/gtk-ng/split_tree.zig rename to src/datastruct/split_tree.zig index 275916e68..69cb5201a 100644 --- a/src/apprt/gtk-ng/split_tree.zig +++ b/src/datastruct/split_tree.zig @@ -31,6 +31,12 @@ const Allocator = std.mem.Allocator; /// /// - `fn eql(*const View, *const View) bool` - Check if two views are equal. /// +/// Optionally the following functions can also be implemented: +/// +/// - `fn splitTreeLabel(*const View) []const u8` - Return a label that is used +/// for the debug view. If this isn't specified then the node handle +/// will be used. +/// pub fn SplitTree(comptime V: type) type { return struct { const Self = @This(); @@ -107,6 +113,38 @@ pub fn SplitTree(comptime V: type) type { self.* = undefined; } + /// An iterator over all the views in the tree. + pub fn iterator( + self: *const Self, + ) Iterator { + return .{ .nodes = self.nodes }; + } + + pub const Iterator = struct { + i: Node.Handle = 0, + nodes: []const Node, + + pub const Entry = struct { + handle: Node.Handle, + view: *View, + }; + + pub fn next(self: *Iterator) ?Entry { + // If we have no nodes, return null. + if (self.i >= self.nodes.len) return null; + + // Get the current node and increment the index. + const handle = self.i; + self.i += 1; + const node = self.nodes[handle]; + + return switch (node) { + .leaf => |v| .{ .handle = handle, .view = v }, + .split => self.next(), + }; + } + }; + /// Insert another tree into this tree at the given node in the /// specified direction. The other tree will be inserted in the /// new direction. For example, if the direction is "right" then @@ -169,6 +207,159 @@ pub fn SplitTree(comptime V: type) type { .right = @intCast(if (left) nodes.len - 1 else self.nodes.len), } }; + // We need to increase the reference count of all the nodes. + try refNodes(gpa, nodes); + + return .{ .arena = arena, .nodes = nodes }; + } + + /// Remove a node from the tree. + pub fn remove( + self: *Self, + gpa: Allocator, + at: Node.Handle, + ) Allocator.Error!Self { + assert(at < self.nodes.len); + + // If we're removing node zero then we're clearing the tree. + if (at == 0) return .empty; + + // The new arena for our new tree. + var arena = ArenaAllocator.init(gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Allocate our new nodes list with the number of nodes we'll + // need after the removal. + const nodes = try alloc.alloc(Node, self.countAfterRemoval( + 0, + at, + 0, + )); + + // Traverse the tree and copy all our nodes into place. + assert(self.removeNode( + nodes, + 0, + 0, + at, + ) > 0); + + // Increase the reference count of all the nodes. + try refNodes(gpa, nodes); + + return .{ + .arena = arena, + .nodes = nodes, + }; + } + + fn removeNode( + self: *Self, + nodes: []Node, + new_offset: Node.Handle, + current: Node.Handle, + target: Node.Handle, + ) Node.Handle { + assert(current != target); + + switch (self.nodes[current]) { + // Leaf is simple, just copy it over. We don't ref anything + // yet because it'd make undo (errdefer) harder. We do that + // all at once later. + .leaf => |view| { + nodes[new_offset] = .{ .leaf = view }; + return 1; + }, + + .split => |s| { + // If we're removing one of the split node sides then + // we remove the split node itself as well and only add + // the other (non-removed) side. + if (s.left == target) return self.removeNode( + nodes, + new_offset, + s.right, + target, + ); + if (s.right == target) return self.removeNode( + nodes, + new_offset, + s.left, + target, + ); + + // Neither side is being directly removed, so we traverse. + const left = self.removeNode( + nodes, + new_offset + 1, + s.left, + target, + ); + assert(left > 0); + const right = self.removeNode( + nodes, + new_offset + 1 + left, + s.right, + target, + ); + assert(right > 0); + nodes[new_offset] = .{ .split = .{ + .layout = s.layout, + .ratio = s.ratio, + .left = new_offset + 1, + .right = new_offset + 1 + left, + } }; + + return left + right + 1; + }, + } + } + + /// Returns the number of nodes that would be needed to store + /// the tree if the target node is removed. + fn countAfterRemoval( + self: *Self, + current: Node.Handle, + target: Node.Handle, + acc: usize, + ) usize { + assert(current != target); + + return switch (self.nodes[current]) { + // Leaf is simple, always takes one node. + .leaf => acc + 1, + + // Split is slightly more complicated. If either side is the + // target to remove, then we remove the split node as well + // so our count is just the count of the other side. + // + // If neither side is the target, then we count both sides + // and add one to account for the split node itself. + .split => |s| if (s.left == target) self.countAfterRemoval( + s.right, + target, + acc, + ) else if (s.right == target) self.countAfterRemoval( + s.left, + target, + acc, + ) else self.countAfterRemoval( + s.left, + target, + acc, + ) + self.countAfterRemoval( + s.right, + target, + acc, + ) + 1, + }; + } + + /// Reference all the nodes in the given slice, handling unref if + /// any fail. This should be called LAST so you don't have to undo + /// the refs at any further point after this. + fn refNodes(gpa: Allocator, nodes: []Node) Allocator.Error!void { // We need to increase the reference count of all the nodes. // Careful accounting here so that we properly unref on error // only the nodes we referenced. @@ -187,8 +378,6 @@ pub fn SplitTree(comptime V: type) type { reffed = i; } assert(reffed == nodes.len - 1); - - return .{ .arena = arena, .nodes = nodes }; } /// Spatial representation of the split tree. This can be used to @@ -360,13 +549,28 @@ pub fn SplitTree(comptime V: type) type { // Get our spatial representation. const sp = try self.spatial(alloc); + // The width we need for the largest label. + const max_label_width: usize = max_label_width: { + if (!@hasDecl(View, "splitTreeLabel")) { + break :max_label_width std.math.log10(sp.slots.len) + 1; + } + + var max: usize = 0; + for (self.nodes) |node| switch (node) { + .split => {}, + .leaf => |view| { + const label = view.splitTreeLabel(); + max = @max(max, label.len); + }, + }; + + break :max_label_width max; + }; + // We need space for whitespace and ASCII art so add that. // We need to accommodate the leaf handle, whitespace, and // then the border. const cell_width = cell_width: { - // The width we need for the largest label. - const max_label_width = std.math.log10(sp.slots.len) + 1; - // Border + whitespace + label + whitespace + border. break :cell_width 2 + max_label_width + 2; }; @@ -400,6 +604,13 @@ pub fn SplitTree(comptime V: type) type { // Draw each node for (sp.slots, 0..) |slot, handle| { + // We only draw leaf nodes. Splits are only used for layout. + const node = self.nodes[handle]; + switch (node) { + .leaf => {}, + .split => continue, + } + var x: usize = @intFromFloat(@ceil(slot.x)); var y: usize = @intFromFloat(@ceil(slot.y)); var width: usize = @intFromFloat(@ceil(slot.width)); @@ -429,13 +640,20 @@ pub fn SplitTree(comptime V: type) type { for (y + 1..y + height - 1) |y_cur| grid[y_cur][x] = '|'; for (y + 1..y + height - 1) |y_cur| grid[y_cur][x + width - 1] = '|'; + // Get our label text + var buf: [10]u8 = undefined; + const label: []const u8 = if (@hasDecl(View, "splitTreeLabel")) + node.leaf.splitTreeLabel() + else + try std.fmt.bufPrint(&buf, "{d}", .{handle}); + // Draw the handle in the center const x_mid = width / 2 + x; const y_mid = height / 2 + y; - const label_width = std.math.log10(handle + 1) + 1; + const label_width = label.len; const label_start = x_mid - label_width / 2; const row = grid[y_mid][label_start..]; - _ = try std.fmt.bufPrint(row, "{d}", .{handle}); + _ = try std.fmt.bufPrint(row, "{s}", .{label}); } // Output every row @@ -451,6 +669,8 @@ const TestTree = SplitTree(TestView); const TestView = struct { const Self = @This(); + label: []const u8, + pub fn ref(self: *Self, alloc: Allocator) Allocator.Error!*Self { const ptr = try alloc.create(Self); ptr.* = self.*; @@ -460,6 +680,10 @@ const TestView = struct { pub fn unref(self: *Self, alloc: Allocator) void { alloc.destroy(self); } + + pub fn splitTreeLabel(self: *const Self) []const u8 { + return self.label; + } }; test "SplitTree: empty tree" { @@ -478,7 +702,7 @@ test "SplitTree: empty tree" { test "SplitTree: single node" { const testing = std.testing; const alloc = testing.allocator; - var v: TestTree.View = .{}; + var v: TestTree.View = .{ .label = "A" }; var t: TestTree = try .init(alloc, &v); defer t.deinit(); @@ -486,7 +710,7 @@ test "SplitTree: single node" { defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ - \\| 0 | + \\| A | \\+---+ \\ ); @@ -495,11 +719,11 @@ test "SplitTree: single node" { test "SplitTree: split horizontal" { const testing = std.testing; const alloc = testing.allocator; - var v: TestTree.View = .{}; - - var t1: TestTree = try .init(alloc, &v); + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); defer t1.deinit(); - var t2: TestTree = try .init(alloc, &v); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); defer t2.deinit(); var t3 = try t1.split( @@ -514,7 +738,7 @@ test "SplitTree: split horizontal" { defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ - \\| 2 || 1 | + \\| A || B | \\+---++---+ \\ ); @@ -523,11 +747,12 @@ test "SplitTree: split horizontal" { test "SplitTree: split vertical" { const testing = std.testing; const alloc = testing.allocator; - var v: TestTree.View = .{}; - var t1: TestTree = try .init(alloc, &v); + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); defer t1.deinit(); - var t2: TestTree = try .init(alloc, &v); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); defer t2.deinit(); var t3 = try t1.split( @@ -542,11 +767,132 @@ test "SplitTree: split vertical" { defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ - \\| 2 | + \\| A | \\+---+ \\+---+ - \\| 1 | + \\| B | \\+---+ \\ ); } + +test "SplitTree: remove leaf" { + 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(); + var t3 = try t1.split( + alloc, + 0, // at root + .right, // split right + &t2, // insert t2 + ); + defer t3.deinit(); + + // Remove "A" + var it = t3.iterator(); + var t4 = try t3.remove( + alloc, + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "A")) { + break entry.handle; + } + } else return error.NotFound, + ); + defer t4.deinit(); + + const str = try std.fmt.allocPrint(alloc, "{}", .{t4}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| B | + \\+---+ + \\ + ); +} + +test "SplitTree: split twice, remove intermediary" { + 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(); + var v3: TestTree.View = .{ .label = "C" }; + var t3: TestTree = try .init(alloc, &v3); + defer t3.deinit(); + + // A | B horizontal. + var split1 = try t1.split( + alloc, + 0, // at root + .right, // split right + &t2, // insert t2 + ); + defer split1.deinit(); + + // Insert C below that. + var split2 = try split1.split( + alloc, + 0, // at root + .down, // split down + &t3, // insert t3 + ); + defer split2.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{}", .{split2}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---++---+ + \\| A || B | + \\+---++---+ + \\+--------+ + \\| C | + \\+--------+ + \\ + ); + } + + // Remove "B" + var it = split2.iterator(); + var split3 = try split2.remove( + alloc, + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound, + ); + defer split3.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{}", .{split3}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| A | + \\+---+ + \\+---+ + \\| C | + \\+---+ + \\ + ); + } + + // Remove every node from split2 (our most complex one), which should + // never crash. We don't test the result is correct, this just verifies + // we don't hit any assertion failures. + for (0..split2.nodes.len) |i| { + var t = try split2.remove(alloc, @intCast(i)); + t.deinit(); + } +}