2401 lines
79 KiB
Zig
2401 lines
79 KiB
Zig
const std = @import("std");
|
|
const assert = @import("../quirks.zig").inlineAssert;
|
|
const build_config = @import("../build_config.zig");
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
/// SplitTree represents a tree of view types that can be divided.
|
|
///
|
|
/// Concretely for Ghostty, it represents a tree of terminal views. In
|
|
/// its basic state, there are no splits and it is a single full-sized
|
|
/// terminal. However, it can be split arbitrarily many times among two
|
|
/// axes (horizontal and vertical) to create a tree of terminal views.
|
|
///
|
|
/// This is an immutable tree structure, meaning all operations on it
|
|
/// will return a new tree with the operation applied. This allows us to
|
|
/// store versions of the tree in a history for easy undo/redo. To facilitate
|
|
/// this, the stored View type must implement reference counting; this is left
|
|
/// as an implementation detail of the View type.
|
|
///
|
|
/// The View type will be stored as a pointer within the tree and must
|
|
/// implement a number of functions to work properly:
|
|
///
|
|
/// - `fn ref(*View, Allocator) Allocator.Error!*View` - Increase a
|
|
/// reference count of the view. The Allocator will be the allocator provided
|
|
/// to the tree operation. This is allowed to copy the value if it wants to;
|
|
/// the returned value is expected to be a new reference (but that may
|
|
/// just be a copy).
|
|
///
|
|
/// - `fn unref(*View, Allocator) void` - Decrease the reference count of a
|
|
/// view. The Allocator will be the allocator provided to the tree
|
|
/// operation.
|
|
///
|
|
/// - `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.
|
|
///
|
|
/// Note: for both the ref and unref functions, the allocator is optional.
|
|
/// If the functions take less arguments, then the allocator will not be
|
|
/// passed.
|
|
pub fn SplitTree(comptime V: type) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
/// The view that this tree contains.
|
|
pub const View = V;
|
|
|
|
/// The arena allocator used for all allocations in the tree.
|
|
/// Since the tree is an immutable structure, this lets us
|
|
/// cleanly free all memory when the tree is deinitialized.
|
|
arena: ArenaAllocator,
|
|
|
|
/// All the nodes in the tree. Node at index 0 is always the root.
|
|
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.
|
|
pub const empty: Self = .{
|
|
// Arena can be undefined because we have zero allocated nodes.
|
|
// If our nodes are empty our deinit function doesn't touch the
|
|
// arena.
|
|
.arena = undefined,
|
|
.nodes = &.{},
|
|
.zoomed = null,
|
|
};
|
|
|
|
pub const Node = union(enum) {
|
|
leaf: *View,
|
|
split: Split,
|
|
|
|
/// A handle into the nodes array. This lets us keep track of
|
|
/// nodes with 16-bit handles rather than full pointer-width
|
|
/// values.
|
|
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 {
|
|
layout: Layout,
|
|
ratio: f16,
|
|
left: Node.Handle,
|
|
right: Node.Handle,
|
|
|
|
pub const Layout = enum { horizontal, vertical };
|
|
pub const Direction = enum { left, right, down, up };
|
|
};
|
|
|
|
/// Initialize a new tree with a single view.
|
|
pub fn init(gpa: Allocator, view: *View) Allocator.Error!Self {
|
|
var arena = ArenaAllocator.init(gpa);
|
|
errdefer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
const nodes = try alloc.alloc(Node, 1);
|
|
nodes[0] = .{ .leaf = try viewRef(view, gpa) };
|
|
errdefer viewUnref(view, gpa);
|
|
|
|
return .{
|
|
.arena = arena,
|
|
.nodes = nodes,
|
|
.zoomed = null,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Self) void {
|
|
// Important: only free memory if we have memory to free,
|
|
// because we use an undefined arena for empty trees.
|
|
if (self.nodes.len > 0) {
|
|
// Unref all our views
|
|
const gpa: Allocator = self.arena.child_allocator;
|
|
for (self.nodes) |node| switch (node) {
|
|
.leaf => |view| viewUnref(view, gpa),
|
|
.split => {},
|
|
};
|
|
self.arena.deinit();
|
|
}
|
|
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Clone this tree, returning a new tree with the same nodes.
|
|
pub fn clone(self: *const Self, gpa: Allocator) Allocator.Error!Self {
|
|
// If we're empty then return an empty tree.
|
|
if (self.isEmpty()) return .empty;
|
|
|
|
// Create a new arena allocator for the clone.
|
|
var arena = ArenaAllocator.init(gpa);
|
|
errdefer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
// Allocate a new nodes array and copy the existing nodes into it.
|
|
const nodes = try alloc.dupe(Node, self.nodes);
|
|
|
|
// Increase the reference count of all the views in the nodes.
|
|
try refNodes(gpa, nodes);
|
|
|
|
return .{
|
|
.arena = arena,
|
|
.nodes = nodes,
|
|
.zoomed = self.zoomed,
|
|
};
|
|
}
|
|
|
|
/// Returns true if this is an empty tree.
|
|
pub fn isEmpty(self: *const Self) bool {
|
|
// An empty tree has no nodes.
|
|
return self.nodes.len == 0;
|
|
}
|
|
|
|
/// An iterator over all the views in the tree.
|
|
pub fn iterator(
|
|
self: *const Self,
|
|
) Iterator {
|
|
return .{ .nodes = self.nodes };
|
|
}
|
|
|
|
pub const ViewEntry = struct {
|
|
handle: Node.Handle,
|
|
view: *View,
|
|
};
|
|
|
|
pub const Iterator = struct {
|
|
i: Node.Handle = .root,
|
|
nodes: []const Node,
|
|
|
|
pub fn next(self: *Iterator) ?ViewEntry {
|
|
// If we have no nodes, return null.
|
|
if (@intFromEnum(self.i) >= self.nodes.len) return null;
|
|
|
|
// Get the current node and increment the index.
|
|
const handle = self.i;
|
|
self.i = @enumFromInt(handle.idx() + 1);
|
|
const node = self.nodes[handle.idx()];
|
|
|
|
return switch (node) {
|
|
.leaf => |v| .{ .handle = handle, .view = v },
|
|
.split => self.next(),
|
|
};
|
|
}
|
|
};
|
|
|
|
/// 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) {
|
|
/// Previous view, null if we're the first view.
|
|
previous,
|
|
|
|
/// Next view, null if we're the last view.
|
|
next,
|
|
|
|
/// Previous view, but wrapped around to the last view. May
|
|
/// return the same view if this is the first view.
|
|
previous_wrapped,
|
|
|
|
/// Next view, but wrapped around to the first view. May return
|
|
/// the same view if this is the last view.
|
|
next_wrapped,
|
|
|
|
/// A spatial direction. "Spatial" means that the direction is
|
|
/// based on the nearest surface in the given direction visually
|
|
/// as the surfaces are laid out on a 2D grid.
|
|
spatial: Spatial.Direction,
|
|
};
|
|
|
|
/// Goto a view from a certain point in the split tree. Returns null
|
|
/// if the direction results in no visitable view.
|
|
///
|
|
/// Allocator is only used for temporary state for spatial navigation.
|
|
pub fn goto(
|
|
self: *const Self,
|
|
alloc: Allocator,
|
|
from: Node.Handle,
|
|
to: Goto,
|
|
) Allocator.Error!?Node.Handle {
|
|
return switch (to) {
|
|
.previous => self.previous(from),
|
|
.next => self.next(from),
|
|
.previous_wrapped => self.previous(from) orelse self.deepest(.right, .root),
|
|
.next_wrapped => self.next(from) orelse self.deepest(.left, .root),
|
|
.spatial => |d| spatial: {
|
|
// Get our spatial representation.
|
|
var sp = try self.spatial(alloc);
|
|
defer sp.deinit(alloc);
|
|
break :spatial self.nearest(sp, from, d);
|
|
},
|
|
};
|
|
}
|
|
|
|
pub const Side = enum { left, right };
|
|
|
|
/// Returns the deepest view in the tree in the given direction.
|
|
/// This can be used to find the leftmost/rightmost surface within
|
|
/// a given split structure.
|
|
pub fn deepest(
|
|
self: *const Self,
|
|
side: Side,
|
|
from: Node.Handle,
|
|
) Node.Handle {
|
|
var current: Node.Handle = from;
|
|
while (true) {
|
|
switch (self.nodes[current.idx()]) {
|
|
.leaf => return current,
|
|
.split => |s| current = switch (side) {
|
|
.left => s.left,
|
|
.right => s.right,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the previous view from the given node handle (which itself
|
|
/// doesn't need to be a view). If there is no previous (this is the
|
|
/// most previous view) then this will return null.
|
|
///
|
|
/// "Previous" is defined as the previous node in an in-order
|
|
/// traversal of the tree. This isn't a perfect definition and we
|
|
/// may want to change this to something that better matches a
|
|
/// spatial view of the tree later.
|
|
fn previous(self: *const Self, from: Node.Handle) ?Node.Handle {
|
|
return switch (self.previousBacktrack(from, .root)) {
|
|
.result => |v| v,
|
|
.backtrack, .deadend => null,
|
|
};
|
|
}
|
|
|
|
/// Same as `previous`, but returns the next view instead.
|
|
fn next(self: *const Self, from: Node.Handle) ?Node.Handle {
|
|
return switch (self.nextBacktrack(from, .root)) {
|
|
.result => |v| v,
|
|
.backtrack, .deadend => null,
|
|
};
|
|
}
|
|
|
|
// Design note: we use a recursive backtracking search because
|
|
// split trees are never that deep, so we can abuse the stack as
|
|
// a safe allocator (stack overflow unlikely unless the kernel is
|
|
// tuned in some really weird way).
|
|
const Backtrack = union(enum) {
|
|
deadend,
|
|
backtrack,
|
|
result: Node.Handle,
|
|
};
|
|
|
|
fn previousBacktrack(
|
|
self: *const Self,
|
|
from: Node.Handle,
|
|
current: Node.Handle,
|
|
) Backtrack {
|
|
// If we reached the point that we're trying to find the previous
|
|
// value of, then we need to backtrack from here.
|
|
if (from == current) return .backtrack;
|
|
|
|
return switch (self.nodes[current.idx()]) {
|
|
// If we hit a leaf that isn't our target, then deadend.
|
|
.leaf => .deadend,
|
|
|
|
.split => |s| switch (self.previousBacktrack(from, s.left)) {
|
|
.result => |v| .{ .result = v },
|
|
|
|
// Backtrack from the left means we have to continue
|
|
// backtracking because we can't see what's before the left.
|
|
.backtrack => .backtrack,
|
|
|
|
// If we hit a deadend on the left then let's move right.
|
|
.deadend => switch (self.previousBacktrack(from, s.right)) {
|
|
.result => |v| .{ .result = v },
|
|
|
|
// Deadend means its not in this split at all since
|
|
// we already tracked the left.
|
|
.deadend => .deadend,
|
|
|
|
// Backtrack means that its in our left view because
|
|
// we can see the immediate previous and there MUST
|
|
// be leaves (we can't have split-only leaves).
|
|
.backtrack => .{ .result = self.deepest(.right, s.left) },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
// See previousBacktrack for detailed comments. This is a mirror
|
|
// of that.
|
|
fn nextBacktrack(
|
|
self: *const Self,
|
|
from: Node.Handle,
|
|
current: Node.Handle,
|
|
) Backtrack {
|
|
if (from == current) return .backtrack;
|
|
return switch (self.nodes[current.idx()]) {
|
|
.leaf => .deadend,
|
|
.split => |s| switch (self.nextBacktrack(from, s.right)) {
|
|
.result => |v| .{ .result = v },
|
|
.backtrack => .backtrack,
|
|
.deadend => switch (self.nextBacktrack(from, s.left)) {
|
|
.result => |v| .{ .result = v },
|
|
.deadend => .deadend,
|
|
.backtrack => .{ .result = self.deepest(.left, s.right) },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Returns the nearest leaf node (view) in the given direction.
|
|
fn nearest(
|
|
self: *const Self,
|
|
sp: Spatial,
|
|
from: Node.Handle,
|
|
direction: Spatial.Direction,
|
|
) ?Node.Handle {
|
|
const target = sp.slots[from.idx()];
|
|
|
|
var result: ?struct {
|
|
handle: Node.Handle,
|
|
distance: f16,
|
|
} = null;
|
|
for (sp.slots, 0..) |slot, handle| {
|
|
// Never match ourself
|
|
if (handle == from.idx()) continue;
|
|
|
|
// Only match leaves
|
|
switch (self.nodes[handle]) {
|
|
.leaf => {},
|
|
.split => continue,
|
|
}
|
|
|
|
// Ensure it is in the proper direction
|
|
if (!switch (direction) {
|
|
.left => slot.maxX() <= target.x,
|
|
.right => slot.x >= target.maxX(),
|
|
.up => slot.maxY() <= target.y,
|
|
.down => slot.y >= target.maxY(),
|
|
}) continue;
|
|
|
|
// Track our distance
|
|
const dx = slot.x - target.x;
|
|
const dy = slot.y - target.y;
|
|
const distance = @sqrt(dx * dx + dy * dy);
|
|
|
|
// If we have a nearest it must be closer.
|
|
if (result) |n| {
|
|
if (distance >= n.distance) continue;
|
|
}
|
|
result = .{
|
|
.handle = @enumFromInt(handle),
|
|
.distance = distance,
|
|
};
|
|
}
|
|
|
|
return if (result) |n| n.handle else null;
|
|
}
|
|
|
|
/// Resize the given node in place. The node MUST be a split (asserted).
|
|
///
|
|
/// In general, this is an immutable data structure so this is
|
|
/// heavily discouraged. However, this is provided for convenience
|
|
/// and performance reasons where its very important for GUIs to
|
|
/// update the ratio during a live resize than to redraw the entire
|
|
/// widget tree.
|
|
pub fn resizeInPlace(
|
|
self: *Self,
|
|
at: Node.Handle,
|
|
ratio: f16,
|
|
) void {
|
|
// 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 s: *Split = @constCast(&self.nodes[at.idx()].split);
|
|
s.ratio = ratio;
|
|
}
|
|
|
|
/// 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
|
|
/// `insert` is inserted right of the existing node.
|
|
///
|
|
/// The allocator will be used for the newly created tree.
|
|
/// The previous trees will not be freed, but reference counts
|
|
/// for the views will be increased accordingly for the new tree.
|
|
pub fn split(
|
|
self: *const Self,
|
|
gpa: Allocator,
|
|
at: Node.Handle,
|
|
direction: Split.Direction,
|
|
ratio: f16,
|
|
insert: *const Self,
|
|
) Allocator.Error!Self {
|
|
// The new arena for our new tree.
|
|
var arena = ArenaAllocator.init(gpa);
|
|
errdefer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
// We know we're going to need the sum total of the nodes
|
|
// between the two trees plus one for the new split node.
|
|
const nodes = try alloc.alloc(Node, self.nodes.len + insert.nodes.len + 1);
|
|
if (nodes.len > std.math.maxInt(Node.Handle.Backing)) return error.OutOfMemory;
|
|
|
|
// We can copy our nodes exactly as they are, since they're
|
|
// mostly not changing (only `at` is changing).
|
|
@memcpy(nodes[0..self.nodes.len], self.nodes);
|
|
|
|
// We can copy the destination nodes as well directly next to
|
|
// the source nodes. We just have to go through and offset
|
|
// all the handles in the destination tree to account for
|
|
// the shift.
|
|
const nodes_inserted = nodes[self.nodes.len..][0..insert.nodes.len];
|
|
@memcpy(nodes_inserted, insert.nodes);
|
|
for (nodes_inserted) |*node| switch (node.*) {
|
|
.leaf => {},
|
|
.split => |*s| {
|
|
// We need to offset the handles in the split
|
|
s.left = s.left.offset(self.nodes.len);
|
|
s.right = s.right.offset(self.nodes.len);
|
|
},
|
|
};
|
|
|
|
// Determine our split layout and if we're on the left
|
|
const layout: Split.Layout, const left: bool = switch (direction) {
|
|
.left => .{ .horizontal, true },
|
|
.right => .{ .horizontal, false },
|
|
.up => .{ .vertical, true },
|
|
.down => .{ .vertical, false },
|
|
};
|
|
|
|
// Copy our previous value to the end of the nodes list and
|
|
// create our new split node.
|
|
nodes[nodes.len - 1] = nodes[at.idx()];
|
|
nodes[at.idx()] = .{ .split = .{
|
|
.layout = layout,
|
|
.ratio = ratio,
|
|
.left = @enumFromInt(if (left) self.nodes.len else nodes.len - 1),
|
|
.right = @enumFromInt(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,
|
|
// Splitting always resets zoom state.
|
|
.zoomed = null,
|
|
};
|
|
}
|
|
|
|
/// Remove a node from the tree.
|
|
pub fn remove(
|
|
self: *Self,
|
|
gpa: Allocator,
|
|
at: Node.Handle,
|
|
) Allocator.Error!Self {
|
|
assert(at.idx() < self.nodes.len);
|
|
|
|
// If we're removing node zero then we're clearing the tree.
|
|
if (at == .root) 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(
|
|
.root,
|
|
at,
|
|
0,
|
|
));
|
|
|
|
var result: Self = .{
|
|
.arena = arena,
|
|
.nodes = nodes,
|
|
.zoomed = null,
|
|
};
|
|
|
|
// Traverse the tree and copy all our nodes into place.
|
|
assert(self.removeNode(
|
|
&result,
|
|
0,
|
|
.root,
|
|
at,
|
|
) != 0);
|
|
|
|
// Increase the reference count of all the nodes.
|
|
try refNodes(gpa, nodes);
|
|
|
|
return result;
|
|
}
|
|
|
|
fn removeNode(
|
|
old: *Self,
|
|
new: *Self,
|
|
new_offset: usize,
|
|
current: Node.Handle,
|
|
target: Node.Handle,
|
|
) usize {
|
|
assert(current != target);
|
|
|
|
// 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
|
|
// yet because it'd make undo (errdefer) harder. We do that
|
|
// all at once later.
|
|
.leaf => |view| {
|
|
new_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 old.removeNode(
|
|
new,
|
|
new_offset,
|
|
s.right,
|
|
target,
|
|
);
|
|
if (s.right == target) return old.removeNode(
|
|
new,
|
|
new_offset,
|
|
s.left,
|
|
target,
|
|
);
|
|
|
|
// Neither side is being directly removed, so we traverse.
|
|
const left = old.removeNode(
|
|
new,
|
|
new_offset + 1,
|
|
s.left,
|
|
target,
|
|
);
|
|
assert(left != 0);
|
|
const right = old.removeNode(
|
|
new,
|
|
new_offset + left + 1,
|
|
s.right,
|
|
target,
|
|
);
|
|
assert(right != 0);
|
|
new_nodes[new_offset] = .{ .split = .{
|
|
.layout = s.layout,
|
|
.ratio = s.ratio,
|
|
.left = @enumFromInt(new_offset + 1),
|
|
.right = @enumFromInt(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.idx()]) {
|
|
// 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.
|
|
var reffed: usize = 0;
|
|
errdefer for (0..reffed) |i| {
|
|
switch (nodes[i]) {
|
|
.split => {},
|
|
.leaf => |view| viewUnref(view, gpa),
|
|
}
|
|
};
|
|
for (0..nodes.len) |i| {
|
|
switch (nodes[i]) {
|
|
.split => {},
|
|
.leaf => |view| nodes[i] = .{ .leaf = try viewRef(view, gpa) },
|
|
}
|
|
reffed = i;
|
|
}
|
|
assert(reffed == nodes.len - 1);
|
|
}
|
|
|
|
/// Equalize this node and all its children, returning a new node with splits
|
|
/// adjusted so that each split's ratio is based on the relative weight
|
|
/// (number of leaves) of its children.
|
|
pub fn equalize(
|
|
self: *const Self,
|
|
gpa: Allocator,
|
|
) Allocator.Error!Self {
|
|
if (self.isEmpty()) return .empty;
|
|
|
|
// Create a new arena allocator for the clone.
|
|
var arena = ArenaAllocator.init(gpa);
|
|
errdefer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
// Allocate a new nodes array and copy the existing nodes into it.
|
|
const nodes = try alloc.dupe(Node, self.nodes);
|
|
|
|
// Go through and equalize our ratios based on weights.
|
|
for (nodes) |*node| switch (node.*) {
|
|
.leaf => {},
|
|
.split => |*s| {
|
|
const weight_left = self.weight(s.left, s.layout, 0);
|
|
const weight_right = self.weight(s.right, s.layout, 0);
|
|
assert(weight_left > 0);
|
|
assert(weight_right > 0);
|
|
const total_f16: f16 = @floatFromInt(weight_left + weight_right);
|
|
const weight_left_f16: f16 = @floatFromInt(weight_left);
|
|
s.ratio = weight_left_f16 / total_f16;
|
|
},
|
|
};
|
|
|
|
// Increase the reference count of all the views in the nodes.
|
|
try refNodes(gpa, nodes);
|
|
|
|
return .{
|
|
.arena = arena,
|
|
.nodes = nodes,
|
|
.zoomed = self.zoomed,
|
|
};
|
|
}
|
|
|
|
fn weight(
|
|
self: *const Self,
|
|
from: Node.Handle,
|
|
layout: Split.Layout,
|
|
acc: usize,
|
|
) usize {
|
|
return switch (self.nodes[from.idx()]) {
|
|
.leaf => acc + 1,
|
|
.split => |s| if (s.layout == layout)
|
|
self.weight(s.left, layout, acc) +
|
|
self.weight(s.right, layout, acc)
|
|
else
|
|
1,
|
|
};
|
|
}
|
|
|
|
/// Resize the nearest split matching the layout by the given ratio.
|
|
/// Positive is right and down.
|
|
///
|
|
/// The ratio is a value between 0 and 1 representing the percentage
|
|
/// to move the divider in the given direction. The percentage is
|
|
/// of the entire grid size, not just the specific split size.
|
|
/// We use the entire grid size because that's what Ghostty's
|
|
/// `resize_split` keybind does, because it maps to a general human
|
|
/// understanding of moving a split relative to the entire window
|
|
/// (generally).
|
|
///
|
|
/// For example, a ratio of 0.1 and a layout of `vertical` will find
|
|
/// the nearest vertical split and move the divider down by 10% of
|
|
/// the total grid height.
|
|
///
|
|
/// If no matching split is found, this does nothing, but will always
|
|
/// still return a cloned tree.
|
|
pub fn resize(
|
|
self: *const Self,
|
|
gpa: Allocator,
|
|
from: Node.Handle,
|
|
layout: Split.Layout,
|
|
ratio: f16,
|
|
) Allocator.Error!Self {
|
|
assert(ratio >= 0 and ratio <= 1);
|
|
assert(!std.math.isNan(ratio));
|
|
assert(!std.math.isInf(ratio));
|
|
|
|
// Fast path empty trees.
|
|
if (self.isEmpty()) return .empty;
|
|
|
|
// From this point forward worst case we return a clone.
|
|
var result = try self.clone(gpa);
|
|
errdefer result.deinit();
|
|
|
|
// Find our nearest parent split node matching the layout.
|
|
const parent_handle = switch (self.findParentSplit(
|
|
layout,
|
|
from,
|
|
.root,
|
|
)) {
|
|
.deadend, .backtrack => return result,
|
|
.result => |v| v,
|
|
};
|
|
|
|
// Get our spatial layout, because we need the dimensions of this
|
|
// split with regards to the entire grid.
|
|
var sp = try result.spatial(gpa);
|
|
defer sp.deinit(gpa);
|
|
|
|
// Get the ratio of the split relative to the full grid.
|
|
const full_ratio = full_ratio: {
|
|
// Our scale is the amount we need to multiply our individual
|
|
// ratio by to get the full ratio. Its actually a ratio on its
|
|
// own but I'm trying to avoid that word: its the ratio of
|
|
// our spatial width/height to the total.
|
|
const scale = switch (layout) {
|
|
.horizontal => sp.slots[parent_handle.idx()].width / sp.slots[0].width,
|
|
.vertical => sp.slots[parent_handle.idx()].height / sp.slots[0].height,
|
|
};
|
|
|
|
const current = result.nodes[parent_handle.idx()].split.ratio;
|
|
break :full_ratio current * scale;
|
|
};
|
|
|
|
// Set the final new ratio, clamping it to [0, 1]
|
|
result.resizeInPlace(
|
|
parent_handle,
|
|
@min(@max(full_ratio + ratio, 0), 1),
|
|
);
|
|
return result;
|
|
}
|
|
|
|
fn findParentSplit(
|
|
self: *const Self,
|
|
layout: Split.Layout,
|
|
from: Node.Handle,
|
|
current: Node.Handle,
|
|
) Backtrack {
|
|
if (from == current) return .backtrack;
|
|
return switch (self.nodes[current.idx()]) {
|
|
.leaf => .deadend,
|
|
.split => |s| switch (self.findParentSplit(
|
|
layout,
|
|
from,
|
|
s.left,
|
|
)) {
|
|
.result => |v| .{ .result = v },
|
|
.backtrack => if (s.layout == layout)
|
|
.{ .result = current }
|
|
else
|
|
.backtrack,
|
|
.deadend => switch (self.findParentSplit(
|
|
layout,
|
|
from,
|
|
s.right,
|
|
)) {
|
|
.deadend => .deadend,
|
|
.result => |v| .{ .result = v },
|
|
.backtrack => if (s.layout == layout)
|
|
.{ .result = current }
|
|
else
|
|
.backtrack,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Spatial representation of the split tree. See spatial.
|
|
pub const Spatial = struct {
|
|
/// The slots of the spatial representation in the same order
|
|
/// as the tree it was created from.
|
|
slots: []const Slot,
|
|
|
|
pub const empty: Spatial = .{ .slots = &.{} };
|
|
|
|
pub const Direction = enum { left, right, down, up };
|
|
|
|
const Slot = struct {
|
|
x: f16,
|
|
y: f16,
|
|
width: f16,
|
|
height: f16,
|
|
|
|
fn maxX(self: *const Slot) f16 {
|
|
return self.x + self.width;
|
|
}
|
|
|
|
fn maxY(self: *const Slot) f16 {
|
|
return self.y + self.height;
|
|
}
|
|
};
|
|
|
|
pub fn deinit(self: *Spatial, alloc: Allocator) void {
|
|
alloc.free(self.slots);
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
|
|
/// Spatial representation of the split tree. This can be used to
|
|
/// better understand the layout of the tree in a 2D space.
|
|
///
|
|
/// The bounds of the representation are always based on the total
|
|
/// 2D space being 1x1. The x/y coordinates and width/height dimensions
|
|
/// of each individual split and leaf are relative to this.
|
|
/// This means that the spatial representation is a normalized
|
|
/// representation of the actual space.
|
|
///
|
|
/// The top-left corner of the tree is always (0, 0).
|
|
///
|
|
/// We use a normalized form because we can calculate it without
|
|
/// accessing to the actual rendered view sizes. These actual sizes
|
|
/// may not be available at various times because GUI toolkits often
|
|
/// only make them available once they're part of a widget tree and
|
|
/// a SplitTree can represent views that aren't currently visible.
|
|
pub fn spatial(
|
|
self: *const Self,
|
|
alloc: Allocator,
|
|
) Allocator.Error!Spatial {
|
|
// No nodes, empty spatial representation.
|
|
if (self.nodes.len == 0) return .empty;
|
|
|
|
// Get our total dimensions.
|
|
const dim = self.dimensions(.root);
|
|
|
|
// Create our slots which will match our nodes exactly.
|
|
const slots = try alloc.alloc(Spatial.Slot, self.nodes.len);
|
|
errdefer alloc.free(slots);
|
|
slots[0] = .{
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = @floatFromInt(dim.width),
|
|
.height = @floatFromInt(dim.height),
|
|
};
|
|
self.fillSpatialSlots(slots, .root);
|
|
|
|
// Normalize the dimensions to 1x1 grid.
|
|
for (slots) |*slot| {
|
|
slot.x /= @floatFromInt(dim.width);
|
|
slot.y /= @floatFromInt(dim.height);
|
|
slot.width /= @floatFromInt(dim.width);
|
|
slot.height /= @floatFromInt(dim.height);
|
|
}
|
|
|
|
return .{ .slots = slots };
|
|
}
|
|
|
|
fn fillSpatialSlots(
|
|
self: *const Self,
|
|
slots: []Spatial.Slot,
|
|
current_: Node.Handle,
|
|
) void {
|
|
const current = current_.idx();
|
|
assert(slots[current].width >= 0 and slots[current].height >= 0);
|
|
switch (self.nodes[current]) {
|
|
// Leaf node, current slot is already filled by caller.
|
|
.leaf => {},
|
|
|
|
.split => |s| {
|
|
switch (s.layout) {
|
|
.horizontal => {
|
|
slots[s.left.idx()] = .{
|
|
.x = slots[current].x,
|
|
.y = slots[current].y,
|
|
.width = slots[current].width * s.ratio,
|
|
.height = slots[current].height,
|
|
};
|
|
slots[s.right.idx()] = .{
|
|
.x = slots[current].x + slots[current].width * s.ratio,
|
|
.y = slots[current].y,
|
|
.width = slots[current].width * (1 - s.ratio),
|
|
.height = slots[current].height,
|
|
};
|
|
},
|
|
|
|
.vertical => {
|
|
slots[s.left.idx()] = .{
|
|
.x = slots[current].x,
|
|
.y = slots[current].y,
|
|
.width = slots[current].width,
|
|
.height = slots[current].height * s.ratio,
|
|
};
|
|
slots[s.right.idx()] = .{
|
|
.x = slots[current].x,
|
|
.y = slots[current].y + slots[current].height * s.ratio,
|
|
.width = slots[current].width,
|
|
.height = slots[current].height * (1 - s.ratio),
|
|
};
|
|
},
|
|
}
|
|
|
|
self.fillSpatialSlots(slots, s.left);
|
|
self.fillSpatialSlots(slots, s.right);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Get the dimensions of the tree starting from the given node.
|
|
///
|
|
/// This creates relative dimensions (see Spatial) by assuming each
|
|
/// leaf is exactly 1x1 unit in size.
|
|
fn dimensions(self: *const Self, current: Node.Handle) struct {
|
|
width: u16,
|
|
height: u16,
|
|
} {
|
|
return switch (self.nodes[current.idx()]) {
|
|
.leaf => .{ .width = 1, .height = 1 },
|
|
.split => |s| split: {
|
|
const left = self.dimensions(s.left);
|
|
const right = self.dimensions(s.right);
|
|
break :split switch (s.layout) {
|
|
.horizontal => .{
|
|
.width = left.width + right.width,
|
|
.height = @max(left.height, right.height),
|
|
},
|
|
|
|
.vertical => .{
|
|
.width = @max(left.width, right.width),
|
|
.height = left.height + right.height,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Format the tree in a human-readable format. By default this will
|
|
/// output a diagram followed by a textual representation.
|
|
pub fn format(
|
|
self: *const Self,
|
|
writer: *std.Io.Writer,
|
|
) !void {
|
|
if (self.nodes.len == 0) {
|
|
try writer.writeAll("empty");
|
|
return;
|
|
}
|
|
self.formatDiagram(writer) catch {};
|
|
try self.formatText(writer);
|
|
}
|
|
|
|
pub fn formatText(self: Self, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
|
if (self.nodes.len == 0) {
|
|
try writer.writeAll("empty");
|
|
return;
|
|
}
|
|
try self.formatTextInner(writer, .root, 0);
|
|
}
|
|
|
|
fn formatTextInner(
|
|
self: Self,
|
|
writer: *std.Io.Writer,
|
|
current: Node.Handle,
|
|
depth: usize,
|
|
) std.Io.Writer.Error!void {
|
|
for (0..depth) |_| try writer.writeAll(" ");
|
|
|
|
if (self.zoomed) |zoomed| if (zoomed == current) {
|
|
try writer.writeAll("(zoomed) ");
|
|
};
|
|
|
|
switch (self.nodes[current.idx()]) {
|
|
.leaf => |v| if (@hasDecl(View, "splitTreeLabel"))
|
|
try writer.print("leaf: {s}\n", .{v.splitTreeLabel()})
|
|
else
|
|
try writer.print("leaf: {d}\n", .{current}),
|
|
|
|
.split => |s| {
|
|
try writer.print("split (layout: {t}, ratio: {d:.2})\n", .{
|
|
s.layout,
|
|
s.ratio,
|
|
});
|
|
try self.formatTextInner(writer, s.left, depth + 1);
|
|
try self.formatTextInner(writer, s.right, depth + 1);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn formatDiagram(
|
|
self: Self,
|
|
writer: *std.Io.Writer,
|
|
) std.Io.Writer.Error!void {
|
|
if (self.nodes.len == 0) {
|
|
try writer.writeAll("empty");
|
|
return;
|
|
}
|
|
|
|
// Use our arena's GPA to allocate some intermediate memory.
|
|
// Requiring allocation for formatting is nasty but this is really
|
|
// only used for debugging and testing and shouldn't hit OOM
|
|
// scenarios.
|
|
var arena: ArenaAllocator = .init(self.arena.child_allocator);
|
|
defer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
// Get our spatial representation.
|
|
const sp = spatial: {
|
|
const sp = self.spatial(alloc) catch return error.WriteFailed;
|
|
|
|
// Scale our spatial representation to have minimum width/height 1.
|
|
var min_w: f16 = 1;
|
|
var min_h: f16 = 1;
|
|
for (sp.slots) |slot| {
|
|
if (slot.width > 0) min_w = @min(min_w, slot.width);
|
|
if (slot.height > 0) min_h = @min(min_h, slot.height);
|
|
}
|
|
|
|
const ratio_w: f16 = 1 / min_w;
|
|
const ratio_h: f16 = 1 / min_h;
|
|
const slots = alloc.dupe(Spatial.Slot, sp.slots) catch return error.WriteFailed;
|
|
for (slots) |*slot| {
|
|
slot.x *= ratio_w;
|
|
slot.y *= ratio_h;
|
|
slot.width *= ratio_w;
|
|
slot.height *= ratio_h;
|
|
}
|
|
|
|
break :spatial .{ .slots = slots };
|
|
};
|
|
|
|
// 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: {
|
|
// Border + whitespace + label + whitespace + border.
|
|
break :cell_width 2 + max_label_width + 2;
|
|
};
|
|
const cell_height = cell_height: {
|
|
// Border + label + border. No whitespace needed on the
|
|
// vertical axis.
|
|
break :cell_height 1 + 1 + 1;
|
|
};
|
|
|
|
// Make a grid that can fit our entire ASCII diagram. We know
|
|
// the width/height based on node 0.
|
|
const grid = grid: {
|
|
// Get our initial width/height. Each leaf is 1x1 in this.
|
|
// We round up for this because partial widths/heights should
|
|
// take up an extra cell.
|
|
var width: usize = @intFromFloat(@ceil(sp.slots[0].width));
|
|
var height: usize = @intFromFloat(@ceil(sp.slots[0].height));
|
|
|
|
// We need space for whitespace and ASCII art so add that.
|
|
// We need to accommodate the leaf handle, whitespace, and
|
|
// then the border.
|
|
width *= cell_width;
|
|
height *= cell_height;
|
|
|
|
const rows = alloc.alloc([]u8, height) catch return error.WriteFailed;
|
|
for (0..rows.len) |y| {
|
|
rows[y] = alloc.alloc(u8, width + 1) catch return error.WriteFailed;
|
|
@memset(rows[y], ' ');
|
|
rows[y][width] = '\n';
|
|
}
|
|
break :grid rows;
|
|
};
|
|
|
|
// 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,
|
|
}
|
|
|
|
// If our width/height is zero then we skip this.
|
|
if (slot.width == 0 or slot.height == 0) continue;
|
|
|
|
var x: usize = @intFromFloat(@floor(slot.x));
|
|
var y: usize = @intFromFloat(@floor(slot.y));
|
|
var width: usize = @intFromFloat(@max(@floor(slot.width), 1));
|
|
var height: usize = @intFromFloat(@max(@floor(slot.height), 1));
|
|
x *= cell_width;
|
|
y *= cell_height;
|
|
width *= cell_width;
|
|
height *= cell_height;
|
|
|
|
// Top border
|
|
{
|
|
const top = grid[y][x..][0..width];
|
|
top[0] = '+';
|
|
for (1..width - 1) |i| top[i] = '-';
|
|
top[width - 1] = '+';
|
|
}
|
|
|
|
// Bottom border
|
|
{
|
|
const bottom = grid[y + height - 1][x..][0..width];
|
|
bottom[0] = '+';
|
|
for (1..width - 1) |i| bottom[i] = '-';
|
|
bottom[width - 1] = '+';
|
|
}
|
|
|
|
// Left border
|
|
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
|
|
std.fmt.bufPrint(&buf, "{d}", .{handle}) catch return error.WriteFailed;
|
|
|
|
// Draw the handle in the center
|
|
const x_mid = width / 2 + x;
|
|
const y_mid = height / 2 + y;
|
|
const label_width = label.len;
|
|
const label_start = x_mid - label_width / 2;
|
|
const row = grid[y_mid][label_start..];
|
|
_ = std.fmt.bufPrint(row, "{s}", .{label}) catch return error.WriteFailed;
|
|
}
|
|
|
|
// Output every row
|
|
for (grid) |row| {
|
|
// We currently have a bug in our height calculation that
|
|
// results in trailing blank lines. Ignore those. We should
|
|
// really fix our height calculation instead. If someone wants
|
|
// to do that just remove this line and see the tests that fail
|
|
// and go from there.
|
|
if (row[0] == ' ') break;
|
|
try writer.writeAll(row);
|
|
}
|
|
}
|
|
|
|
fn viewRef(view: *View, gpa: Allocator) Allocator.Error!*View {
|
|
const func = @typeInfo(@TypeOf(View.ref)).@"fn";
|
|
return switch (func.params.len) {
|
|
1 => view.ref(),
|
|
2 => try view.ref(gpa),
|
|
else => @compileError("invalid view ref function"),
|
|
};
|
|
}
|
|
|
|
fn viewUnref(view: *View, gpa: Allocator) void {
|
|
const func = @typeInfo(@TypeOf(View.unref)).@"fn";
|
|
switch (func.params.len) {
|
|
1 => view.unref(),
|
|
2 => view.unref(gpa),
|
|
else => @compileError("invalid view unref function"),
|
|
}
|
|
}
|
|
|
|
/// Make this a valid gobject if we're in a GTK environment.
|
|
pub const getGObjectType = switch (build_config.app_runtime) {
|
|
.gtk => @import("gobject").ext.defineBoxed(
|
|
Self,
|
|
.{
|
|
// To get the type name we get the non-qualified type name
|
|
// of the view and append that to `GhosttySplitTree`.
|
|
.name = name: {
|
|
const type_name = @typeName(View);
|
|
const last = if (std.mem.lastIndexOfScalar(
|
|
u8,
|
|
type_name,
|
|
'.',
|
|
)) |idx|
|
|
type_name[idx + 1 ..]
|
|
else
|
|
type_name;
|
|
assert(last.len > 0);
|
|
break :name "GhosttySplitTree" ++ last;
|
|
},
|
|
|
|
.funcs = .{
|
|
.copy = &struct {
|
|
fn copy(self: *Self) callconv(.c) *Self {
|
|
const ptr = @import("glib").ext.create(Self);
|
|
ptr.* = if (self.nodes.len == 0)
|
|
.empty
|
|
else
|
|
self.clone(self.arena.child_allocator) catch @panic("oom");
|
|
return ptr;
|
|
}
|
|
}.copy,
|
|
.free = &struct {
|
|
fn free(self: *Self) callconv(.c) void {
|
|
self.deinit();
|
|
@import("glib").ext.destroy(self);
|
|
}
|
|
}.free,
|
|
},
|
|
},
|
|
),
|
|
|
|
.none => void,
|
|
};
|
|
|
|
/// A C-compatible API for using a split tree. The caller has to
|
|
/// manually `@export` these symbols if they need them.
|
|
///
|
|
/// This is currently read-only since modification APIs aren't
|
|
/// presently necessary from C. This will likely change in the future.
|
|
pub const CApi = struct {
|
|
pub const NodeTag = enum(c_int) {
|
|
leaf,
|
|
split,
|
|
};
|
|
|
|
pub const Split = extern struct {
|
|
horizontal: bool,
|
|
ratio: f32,
|
|
left: Node.Handle,
|
|
right: Node.Handle,
|
|
};
|
|
|
|
pub fn is_empty(self: *const Self) callconv(.c) bool {
|
|
return self.isEmpty();
|
|
}
|
|
|
|
pub fn len(self: *const Self) callconv(.c) usize {
|
|
return self.nodes.len;
|
|
}
|
|
|
|
pub fn is_split(self: *const Self, handle: Node.Handle) callconv(.c) bool {
|
|
return switch (self.nodes[handle.idx()]) {
|
|
.leaf => false,
|
|
.split => true,
|
|
};
|
|
}
|
|
|
|
pub fn get_split(
|
|
self: *const Self,
|
|
handle: Node.Handle,
|
|
) callconv(.c) CApi.Split {
|
|
const s = self.nodes[handle.idx()].split;
|
|
return .{
|
|
.horizontal = switch (s.layout) {
|
|
.horizontal => true,
|
|
.vertical => false,
|
|
},
|
|
.ratio = @floatCast(s.ratio),
|
|
.left = s.left,
|
|
.right = s.right,
|
|
};
|
|
}
|
|
|
|
pub fn get_leaf(self: *const Self, handle: Node.Handle) callconv(.c) *View {
|
|
return self.nodes[handle.idx()].leaf;
|
|
}
|
|
};
|
|
};
|
|
}
|
|
|
|
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.*;
|
|
return ptr;
|
|
}
|
|
|
|
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" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
var t: TestTree = .empty;
|
|
defer t.deinit();
|
|
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{t});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\empty
|
|
);
|
|
}
|
|
|
|
test "SplitTree: single node" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
var v: TestTree.View = .{ .label = "A" };
|
|
var t: TestTree = try .init(alloc, &v);
|
|
defer t.deinit();
|
|
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---+
|
|
\\| A |
|
|
\\+---+
|
|
\\
|
|
);
|
|
}
|
|
|
|
test "SplitTree: split horizontal" {
|
|
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,
|
|
.root, // at root
|
|
.right, // split right
|
|
0.5,
|
|
&t2, // insert t2
|
|
);
|
|
defer t3.deinit();
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{t3});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---++---+
|
|
\\| A || B |
|
|
\\+---++---+
|
|
\\split (layout: horizontal, ratio: 0.50)
|
|
\\ leaf: A
|
|
\\ leaf: B
|
|
\\
|
|
);
|
|
}
|
|
|
|
// Split right at B
|
|
var vC: TestTree.View = .{ .label = "C" };
|
|
var tC: TestTree = try .init(alloc, &vC);
|
|
defer tC.deinit();
|
|
var it = t3.iterator();
|
|
var t4 = try t3.split(
|
|
alloc,
|
|
while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, "B")) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound,
|
|
.right,
|
|
0.5,
|
|
&tC,
|
|
);
|
|
defer t4.deinit();
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{t4});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+--------++---++---+
|
|
\\| A || B || C |
|
|
\\+--------++---++---+
|
|
\\split (layout: horizontal, ratio: 0.50)
|
|
\\ leaf: A
|
|
\\ split (layout: horizontal, ratio: 0.50)
|
|
\\ leaf: B
|
|
\\ leaf: C
|
|
\\
|
|
);
|
|
}
|
|
|
|
// Split right at C
|
|
var vD: TestTree.View = .{ .label = "D" };
|
|
var tD: TestTree = try .init(alloc, &vD);
|
|
defer tD.deinit();
|
|
it = t4.iterator();
|
|
var t5 = try t4.split(
|
|
alloc,
|
|
while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, "C")) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound,
|
|
.right,
|
|
0.5,
|
|
&tD,
|
|
);
|
|
defer t5.deinit();
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{t5});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(
|
|
\\+------------------++--------++---++---+
|
|
\\| A || B || C || D |
|
|
\\+------------------++--------++---++---+
|
|
\\split (layout: horizontal, ratio: 0.50)
|
|
\\ leaf: A
|
|
\\ split (layout: horizontal, ratio: 0.50)
|
|
\\ leaf: B
|
|
\\ split (layout: horizontal, ratio: 0.50)
|
|
\\ leaf: C
|
|
\\ leaf: D
|
|
\\
|
|
, str);
|
|
}
|
|
|
|
// Find "previous" from D back.
|
|
{
|
|
var current: u8 = 'D';
|
|
while (current != 'A') : (current -= 1) {
|
|
it = t5.iterator();
|
|
const handle = t5.previous(
|
|
while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, &.{current})) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound,
|
|
).?;
|
|
|
|
const entry = t5.nodes[handle.idx()].leaf;
|
|
try testing.expectEqualStrings(
|
|
entry.label,
|
|
&.{current - 1},
|
|
);
|
|
}
|
|
|
|
it = t5.iterator();
|
|
try testing.expect(t5.previous(
|
|
while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, &.{current})) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound,
|
|
) == null);
|
|
}
|
|
|
|
// Find "next" from A forward.
|
|
{
|
|
var current: u8 = 'A';
|
|
while (current != 'D') : (current += 1) {
|
|
it = t5.iterator();
|
|
const handle = t5.next(
|
|
while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, &.{current})) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound,
|
|
).?;
|
|
|
|
const entry = t5.nodes[handle.idx()].leaf;
|
|
try testing.expectEqualStrings(
|
|
entry.label,
|
|
&.{current + 1},
|
|
);
|
|
}
|
|
|
|
it = t5.iterator();
|
|
try testing.expect(t5.next(
|
|
while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, &.{current})) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound,
|
|
) == null);
|
|
}
|
|
}
|
|
|
|
test "SplitTree: split vertical" {
|
|
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,
|
|
.root, // at root
|
|
.down, // split down
|
|
0.5,
|
|
&t2, // insert t2
|
|
);
|
|
defer t3.deinit();
|
|
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t3, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---+
|
|
\\| A |
|
|
\\+---+
|
|
\\+---+
|
|
\\| B |
|
|
\\+---+
|
|
\\
|
|
);
|
|
}
|
|
|
|
test "SplitTree: split horizontal with zero ratio" {
|
|
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 splitAB = try t1.split(
|
|
alloc,
|
|
.root, // at root
|
|
.right, // split right
|
|
0,
|
|
&t2, // insert t2
|
|
);
|
|
defer splitAB.deinit();
|
|
const split = splitAB;
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---+
|
|
\\| B |
|
|
\\+---+
|
|
\\
|
|
);
|
|
}
|
|
}
|
|
|
|
test "SplitTree: split vertical with zero ratio" {
|
|
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 splitAB = try t1.split(
|
|
alloc,
|
|
.root, // at root
|
|
.down, // split right
|
|
0,
|
|
&t2, // insert t2
|
|
);
|
|
defer splitAB.deinit();
|
|
const split = splitAB;
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---+
|
|
\\| B |
|
|
\\+---+
|
|
\\
|
|
);
|
|
}
|
|
}
|
|
|
|
test "SplitTree: split horizontal with full width" {
|
|
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 splitAB = try t1.split(
|
|
alloc,
|
|
.root, // at root
|
|
.right, // split right
|
|
1,
|
|
&t2, // insert t2
|
|
);
|
|
defer splitAB.deinit();
|
|
const split = splitAB;
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---+
|
|
\\| A |
|
|
\\+---+
|
|
\\
|
|
);
|
|
}
|
|
}
|
|
|
|
test "SplitTree: split vertical with full width" {
|
|
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 splitAB = try t1.split(
|
|
alloc,
|
|
.root, // at root
|
|
.down, // split right
|
|
1,
|
|
&t2, // insert t2
|
|
);
|
|
defer splitAB.deinit();
|
|
const split = splitAB;
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---+
|
|
\\| A |
|
|
\\+---+
|
|
\\
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
.root, // at root
|
|
.right, // split right
|
|
0.5,
|
|
&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, "{f}", .{std.fmt.alt(t4, .formatDiagram)});
|
|
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,
|
|
.root, // at root
|
|
.right, // split right
|
|
0.5,
|
|
&t2, // insert t2
|
|
);
|
|
defer split1.deinit();
|
|
|
|
// Insert C below that.
|
|
var split2 = try split1.split(
|
|
alloc,
|
|
.root, // at root
|
|
.down, // split down
|
|
0.5,
|
|
&t3, // insert t3
|
|
);
|
|
defer split2.deinit();
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split2, .formatDiagram)});
|
|
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, "{f}", .{std.fmt.alt(split3, .formatDiagram)});
|
|
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, @enumFromInt(i));
|
|
t.deinit();
|
|
}
|
|
}
|
|
|
|
test "SplitTree: spatial goto" {
|
|
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();
|
|
var v4: TestTree.View = .{ .label = "D" };
|
|
var t4: TestTree = try .init(alloc, &v4);
|
|
defer t4.deinit();
|
|
|
|
// A | B horizontal
|
|
var splitAB = try t1.split(
|
|
alloc,
|
|
.root, // at root
|
|
.right, // split right
|
|
0.5,
|
|
&t2, // insert t2
|
|
);
|
|
defer splitAB.deinit();
|
|
|
|
// A | C vertical
|
|
var splitAC = try splitAB.split(
|
|
alloc,
|
|
at: {
|
|
var it = splitAB.iterator();
|
|
break :at while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, "A")) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound;
|
|
},
|
|
.down, // split down
|
|
0.8,
|
|
&t3, // insert t3
|
|
);
|
|
defer splitAC.deinit();
|
|
|
|
// B | D vertical
|
|
var splitBD = try splitAC.split(
|
|
alloc,
|
|
at: {
|
|
var it = splitAB.iterator();
|
|
break :at while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, "B")) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound;
|
|
},
|
|
.down, // split down
|
|
0.3,
|
|
&t4, // insert t4
|
|
);
|
|
defer splitBD.deinit();
|
|
const split = splitBD;
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---++---+
|
|
\\| || B |
|
|
\\| |+---+
|
|
\\| |+---+
|
|
\\| A || |
|
|
\\| || |
|
|
\\| || |
|
|
\\| || D |
|
|
\\+---+| |
|
|
\\+---+| |
|
|
\\| C || |
|
|
\\+---++---+
|
|
\\
|
|
);
|
|
}
|
|
|
|
// Spatial C => right
|
|
{
|
|
const target = (try split.goto(
|
|
alloc,
|
|
from: {
|
|
var it = split.iterator();
|
|
break :from while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, "C")) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound;
|
|
},
|
|
.{ .spatial = .right },
|
|
)).?;
|
|
const view = split.nodes[target.idx()].leaf;
|
|
try testing.expectEqualStrings(view.label, "D");
|
|
}
|
|
|
|
// Spatial D => left
|
|
{
|
|
const target = (try split.goto(
|
|
alloc,
|
|
from: {
|
|
var it = split.iterator();
|
|
break :from while (it.next()) |entry| {
|
|
if (std.mem.eql(u8, entry.view.label, "D")) {
|
|
break entry.handle;
|
|
}
|
|
} else return error.NotFound;
|
|
},
|
|
.{ .spatial = .left },
|
|
)).?;
|
|
const view = split.nodes[target.idx()].leaf;
|
|
try testing.expectEqualStrings("A", view.label);
|
|
}
|
|
|
|
// Equalize
|
|
var equal = try split.equalize(alloc);
|
|
defer equal.deinit();
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(equal, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---++---+
|
|
\\| A || B |
|
|
\\+---++---+
|
|
\\+---++---+
|
|
\\| C || D |
|
|
\\+---++---+
|
|
\\
|
|
);
|
|
}
|
|
}
|
|
|
|
test "SplitTree: resize" {
|
|
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();
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+---++---+
|
|
\\| A || B |
|
|
\\+---++---+
|
|
\\
|
|
);
|
|
}
|
|
|
|
// Resize
|
|
{
|
|
var resized = try split.resize(
|
|
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;
|
|
},
|
|
.horizontal, // resize right
|
|
0.25,
|
|
);
|
|
defer resized.deinit();
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\+-------------++---+
|
|
\\| A || B |
|
|
\\+-------------++---+
|
|
\\
|
|
);
|
|
}
|
|
}
|
|
|
|
test "SplitTree: clone empty tree" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
var t: TestTree = .empty;
|
|
defer t.deinit();
|
|
|
|
var t2 = try t.clone(alloc);
|
|
defer t2.deinit();
|
|
|
|
{
|
|
const str = try std.fmt.allocPrint(alloc, "{f}", .{t2});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\empty
|
|
);
|
|
}
|
|
}
|
|
|
|
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, "{f}", .{std.fmt.alt(split, .formatText)});
|
|
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, "{f}", .{std.fmt.alt(clone, .formatText)});
|
|
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, "{f}", .{std.fmt.alt(split, .formatText)});
|
|
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, "{f}", .{std.fmt.alt(removed, .formatText)});
|
|
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, "{f}", .{std.fmt.alt(removed, .formatText)});
|
|
defer alloc.free(str);
|
|
try testing.expectEqualStrings(str,
|
|
\\(zoomed) leaf: A
|
|
\\
|
|
);
|
|
}
|
|
}
|
|
|
|
test "SplitTree: CApi is_empty" {
|
|
const testing = std.testing;
|
|
|
|
var empty: TestTree = .empty;
|
|
defer empty.deinit();
|
|
try testing.expect(TestTree.CApi.is_empty(&empty));
|
|
|
|
var v1: TestTree.View = .{ .label = "A" };
|
|
var t1: TestTree = try .init(testing.allocator, &v1);
|
|
defer t1.deinit();
|
|
try testing.expect(!TestTree.CApi.is_empty(&t1));
|
|
}
|
|
|
|
test "SplitTree: CApi len" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var empty: TestTree = .empty;
|
|
defer empty.deinit();
|
|
try testing.expectEqual(@as(usize, 0), TestTree.CApi.len(&empty));
|
|
|
|
var v1: TestTree.View = .{ .label = "A" };
|
|
var t1: TestTree = try .init(alloc, &v1);
|
|
defer t1.deinit();
|
|
try testing.expectEqual(@as(usize, 1), TestTree.CApi.len(&t1));
|
|
|
|
var v2: TestTree.View = .{ .label = "B" };
|
|
var t2: TestTree = try .init(alloc, &v2);
|
|
defer t2.deinit();
|
|
|
|
var split = try t1.split(
|
|
alloc,
|
|
.root,
|
|
.right,
|
|
0.5,
|
|
&t2,
|
|
);
|
|
defer split.deinit();
|
|
try testing.expectEqual(@as(usize, 3), TestTree.CApi.len(&split));
|
|
}
|
|
|
|
test "SplitTree: CApi is_split" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
var v1: TestTree.View = .{ .label = "A" };
|
|
var t1: TestTree = try .init(alloc, &v1);
|
|
defer t1.deinit();
|
|
try testing.expect(!TestTree.CApi.is_split(&t1, .root));
|
|
|
|
var v2: TestTree.View = .{ .label = "B" };
|
|
var t2: TestTree = try .init(alloc, &v2);
|
|
defer t2.deinit();
|
|
|
|
var split = try t1.split(
|
|
alloc,
|
|
.root,
|
|
.right,
|
|
0.5,
|
|
&t2,
|
|
);
|
|
defer split.deinit();
|
|
try testing.expect(TestTree.CApi.is_split(&split, .root));
|
|
try testing.expect(!TestTree.CApi.is_split(&split, @enumFromInt(1)));
|
|
try testing.expect(!TestTree.CApi.is_split(&split, @enumFromInt(2)));
|
|
}
|
|
|
|
test "SplitTree: CApi get_split" {
|
|
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 split_h = try t1.split(
|
|
alloc,
|
|
.root,
|
|
.right,
|
|
0.5,
|
|
&t2,
|
|
);
|
|
defer split_h.deinit();
|
|
|
|
const s_h = TestTree.CApi.get_split(&split_h, .root);
|
|
try testing.expect(s_h.horizontal);
|
|
try testing.expectApproxEqAbs(@as(f32, 0.5), s_h.ratio, 0.01);
|
|
try testing.expectEqual(@as(TestTree.Node.Handle, @enumFromInt(2)), s_h.left);
|
|
try testing.expectEqual(@as(TestTree.Node.Handle, @enumFromInt(1)), s_h.right);
|
|
|
|
var v3: TestTree.View = .{ .label = "C" };
|
|
var t3: TestTree = try .init(alloc, &v3);
|
|
defer t3.deinit();
|
|
|
|
var split_v = try t1.split(
|
|
alloc,
|
|
.root,
|
|
.down,
|
|
0.7,
|
|
&t3,
|
|
);
|
|
defer split_v.deinit();
|
|
|
|
const s_v = TestTree.CApi.get_split(&split_v, .root);
|
|
try testing.expect(!s_v.horizontal);
|
|
try testing.expectApproxEqAbs(@as(f32, 0.7), s_v.ratio, 0.01);
|
|
}
|
|
|
|
test "SplitTree: CApi get_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();
|
|
|
|
const leaf = TestTree.CApi.get_leaf(&t1, .root);
|
|
try testing.expectEqualStrings("A", leaf.label);
|
|
|
|
var v2: TestTree.View = .{ .label = "B" };
|
|
var t2: TestTree = try .init(alloc, &v2);
|
|
defer t2.deinit();
|
|
|
|
var split = try t1.split(
|
|
alloc,
|
|
.root,
|
|
.right,
|
|
0.5,
|
|
&t2,
|
|
);
|
|
defer split.deinit();
|
|
|
|
const leaf_a = TestTree.CApi.get_leaf(&split, @enumFromInt(2));
|
|
try testing.expectEqualStrings("A", leaf_a.label);
|
|
|
|
const leaf_b = TestTree.CApi.get_leaf(&split, @enumFromInt(1));
|
|
try testing.expectEqualStrings("B", leaf_b.label);
|
|
}
|