Compare commits

...

8 Commits

Author SHA1 Message Date
89f41c9d78 TEMP 2025-04-28 21:01:54 +09:00
8d53a185a4 add Merged() util 2025-04-09 00:18:06 +09:00
ca3d26a18d add Mindmap and Project data type 2025-04-09 00:17:39 +09:00
a339d2a392 rename structs 2025-04-08 23:03:19 +09:00
fc303ba553 fix build errors 2025-04-02 23:10:32 +09:00
ee8d0cde34 improve doc comments 2025-04-02 22:52:13 +09:00
845582a14d add new container type, Store 2025-04-02 22:30:57 +09:00
907d71a19a add new data type, Node 2025-04-02 22:30:39 +09:00
11 changed files with 534 additions and 41 deletions

View File

@@ -59,4 +59,13 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step); test_step.dependOn(&run_lib_unit_tests.step);
// ---- document generation
const install_docs = b.addInstallDirectory(.{
.source_dir = lib.getEmittedDocs(),
.install_dir = .prefix,
.install_subdir = "docs",
});
const docs_step = b.step("docs", "Install docs into zig-out/docs");
docs_step.dependOn(&install_docs.step);
} }

View File

@@ -1,16 +1,25 @@
const std = @import("std"); const std = @import("std");
/// A data type to store connections of nodes in directional-graph. /// A data type to store connections of nodes in directional-graph.
pub fn Digraph(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type { pub fn Container(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type {
return struct { return struct {
const Node = T; ///
const Conn = struct { from: T, to: T, }; pub const Node = T;
const ConnList = std.ArrayList(Conn);
const Error = error { /// A connection between 2 nodes.
/// e.g.) `Conn { .from = X, .to = Y }` => "X is connected to Y"
pub const Conn = struct { from: T, to: T, };
///
pub const ConnList = std.ArrayList(Conn);
///
pub const Error = error {
AlreadyConnected, AlreadyConnected,
NotConnected, NotConnected,
}; };
///
map: ConnList, map: ConnList,
/// ///
@@ -31,7 +40,8 @@ pub fn Digraph(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type {
self.map.deinit(); self.map.deinit();
} }
/// /// Makes new connection between `from` to `to`.
/// Returns false if they are already connected otherwise true.
pub fn connectIf(self: *@This(), from: T, to: T) !bool { pub fn connectIf(self: *@This(), from: T, to: T) !bool {
const begin, const end = self.findSegment(from); const begin, const end = self.findSegment(from);
if (self.findConnectionInSegment(from, to, begin, end)) |_| { if (self.findConnectionInSegment(from, to, begin, end)) |_| {
@@ -48,7 +58,8 @@ pub fn Digraph(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type {
} }
} }
/// /// Removes an existing connection between `from` to `to`.
/// Returns false if they are not connected otherwise true.
pub fn disconnectIf(self: *@This(), from: T, to: T) bool { pub fn disconnectIf(self: *@This(), from: T, to: T) bool {
const begin, const end = self.findSegment(from); const begin, const end = self.findSegment(from);
if (self.findConnectionInSegment(from, to, begin, end)) |idx| { if (self.findConnectionInSegment(from, to, begin, end)) |idx| {
@@ -97,16 +108,16 @@ pub fn Digraph(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type {
var begin: usize = undefined; var begin: usize = undefined;
var end : usize = undefined; var end : usize = undefined;
if (baseFrom < from) { if (lessThanFn(baseFrom, from)) {
begin = baseIdx; begin = baseIdx;
while ((begin < n) and (self.map.items[begin].from < from)) { begin += 1; } while ((begin < n) and lessThanFn(self.map.items[begin].from, from)) { begin += 1; }
end = begin; end = begin;
while ((end < n) and (self.map.items[end].from == from)) { end += 1; } while ((end < n) and (self.map.items[end].from == from)) { end += 1; }
} else if (baseFrom > from) { } else if (lessThanFn(from, baseFrom)) {
end = baseIdx; end = baseIdx;
while ((end > 0) and (self.map.items[end-1].from > from)) { end -= 1; } while ((end > 0) and lessThanFn(from, self.map.items[end-1].from)) { end -= 1; }
begin = end; begin = end;
while ((begin > 0) and (self.map.items[begin-1].from == from)) { begin -= 1; } while ((begin > 0) and (self.map.items[begin-1].from == from)) { begin -= 1; }
@@ -133,9 +144,9 @@ pub fn Digraph(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type {
idx = (left + right) / 2; idx = (left + right) / 2;
const target = self.map.items[idx].from; const target = self.map.items[idx].from;
if (target < from) { if (lessThanFn(target, from)) {
left = idx + 1; left = idx + 1;
} else if (target > from) { } else if (lessThanFn(from, target)) {
right = idx -| 1; right = idx -| 1;
} else { } else {
break; break;
@@ -149,7 +160,7 @@ pub fn Digraph(comptime T: type, comptime lessThanFn: LessThanFunc(T)) type {
}; };
} }
/// A type of comparator function for the type T, which is to be passed as an argument of `Digraph()`. /// A type of comparator function for the type T, which is to be passed as an argument of `Container()`.
pub fn LessThanFunc(comptime T: type) type { pub fn LessThanFunc(comptime T: type) type {
return fn (lhs: T, rhs: T) bool; return fn (lhs: T, rhs: T) bool;
} }
@@ -168,15 +179,15 @@ pub fn lessThanFuncFor(comptime T: type) LessThanFunc(T) {
} }
test "compile check for various types" { test "compile check for various types" {
_ = Digraph(u8, lessThanFuncFor(u8)); _ = Container(u8, lessThanFuncFor(u8));
_ = Digraph(u16, lessThanFuncFor(u16)); _ = Container(u16, lessThanFuncFor(u16));
_ = Digraph(i8, lessThanFuncFor(i8)); _ = Container(i8, lessThanFuncFor(i8));
_ = Digraph(i16, lessThanFuncFor(i16)); _ = Container(i16, lessThanFuncFor(i16));
_ = Digraph(*i8, lessThanFuncFor(*i8)); _ = Container(*i8, lessThanFuncFor(*i8));
_ = Digraph(*anyopaque, lessThanFuncFor(*anyopaque)); _ = Container(*anyopaque, lessThanFuncFor(*anyopaque));
} }
test "check if connected" { test "check if connected" {
const Sut = Digraph(u8, lessThanFuncFor(u8)); const Sut = Container(u8, lessThanFuncFor(u8));
const map = [_]Sut.Conn { const map = [_]Sut.Conn {
.{ .from = 3, .to = 0, }, .{ .from = 3, .to = 0, },
@@ -203,7 +214,7 @@ test "check if connected" {
try std.testing.expect(!sut.isConnected(2, 1)); try std.testing.expect(!sut.isConnected(2, 1));
} }
test "make new connection" { test "make new connection" {
const Sut = Digraph(u8, lessThanFuncFor(u8)); const Sut = Container(u8, lessThanFuncFor(u8));
var sut = try Sut.init(std.testing.allocator, &.{}); var sut = try Sut.init(std.testing.allocator, &.{});
defer sut.deinit(); defer sut.deinit();
@@ -219,7 +230,7 @@ test "make new connection" {
try std.testing.expect(!sut.isConnected(1, 3)); try std.testing.expect(!sut.isConnected(1, 3));
} }
test "making an existing connection fails" { test "making an existing connection fails" {
const Sut = Digraph(u8, lessThanFuncFor(u8)); const Sut = Container(u8, lessThanFuncFor(u8));
const map = [_]Sut.Conn { const map = [_]Sut.Conn {
.{ .from = 0, .to = 1, }, .{ .from = 0, .to = 1, },
@@ -231,7 +242,7 @@ test "making an existing connection fails" {
try std.testing.expectError(Sut.Error.AlreadyConnected, sut.connect(0, 1)); try std.testing.expectError(Sut.Error.AlreadyConnected, sut.connect(0, 1));
} }
test "disconnect an existing connection" { test "disconnect an existing connection" {
const Sut = Digraph(u8, lessThanFuncFor(u8)); const Sut = Container(u8, lessThanFuncFor(u8));
const map = [_]Sut.Conn { const map = [_]Sut.Conn {
.{ .from = 0, .to = 1, }, .{ .from = 0, .to = 1, },
@@ -247,7 +258,7 @@ test "disconnect an existing connection" {
try std.testing.expect(!sut.isConnected(2, 3)); try std.testing.expect(!sut.isConnected(2, 3));
} }
test "disconnecting a missing connection fails" { test "disconnecting a missing connection fails" {
const Sut = Digraph(u8, lessThanFuncFor(u8)); const Sut = Container(u8, lessThanFuncFor(u8));
var sut = try Sut.init(std.testing.allocator, &.{}); var sut = try Sut.init(std.testing.allocator, &.{});
defer sut.deinit(); defer sut.deinit();
@@ -256,7 +267,7 @@ test "disconnecting a missing connection fails" {
try std.testing.expectError(Sut.Error.NotConnected, sut.disconnect(1, 0)); try std.testing.expectError(Sut.Error.NotConnected, sut.disconnect(1, 0));
} }
test "chaotic operation" { test "chaotic operation" {
const Sut = Digraph(u16, lessThanFuncFor(u16)); const Sut = Container(u16, lessThanFuncFor(u16));
var sut = try Sut.init(std.testing.allocator, &.{}); var sut = try Sut.init(std.testing.allocator, &.{});
defer sut.deinit(); defer sut.deinit();

71
src/hncore/Merged.zig Normal file
View File

@@ -0,0 +1,71 @@
const std = @import("std");
/// Returns a type having fields that type A and type B have.
/// Fields from B are more preferred than from A in case that field names are duplicated.
pub fn Merged(comptime A: type, comptime B: type) type {
const at = @typeInfo(A).@"struct";
const bt = @typeInfo(B).@"struct";
return @Type(.{
.@"struct" = .{
.layout = .auto,
.fields = mergeFields(at.fields, bt.fields),
.is_tuple = false,
.decls = &.{},
},
});
}
fn mergeFields(
comptime a: []const std.builtin.Type.StructField,
comptime b: []const std.builtin.Type.StructField) []std.builtin.Type.StructField {
var ret: [a.len+b.len]std.builtin.Type.StructField = undefined;
var len: usize = 0;
inline for (b) |f| {
ret[len] = f;
ret[len].is_comptime = false;
len += 1;
}
inline for (a) |f| {
inline for (b) |fb| {
if (std.mem.eql(u8, f.name, fb.name)) {
break;
}
} else {
ret[len] = f;
ret[len].is_comptime = false;
len += 1;
}
}
return ret[0..len];
}
/// Same to `Merged(@TypeOf(a), @TypeOf(b))` but returns a merged value, not type.
pub fn merge(a: anytype, b: anytype) Merged(@TypeOf(a), @TypeOf(b)) {
var ret: Merged(@TypeOf(a), @TypeOf(b)) = undefined;
const af = @typeInfo(@TypeOf(a)).@"struct".fields;
const bf = @typeInfo(@TypeOf(b)).@"struct".fields;
inline for (bf) |f| {
@field(ret, f.name) = @field(b, f.name);
}
inline for (af) |f| {
inline for (bf) |fb| {
if (std.mem.eql(u8, f.name, fb.name)) {
break;
}
} else {
@field(ret, f.name) = @field(a, f.name);
}
}
return ret;
}
test "merging 2 structs" {
const merged = merge(.{.a = 0, .b = 1, .c = 2,}, .{.b = 44, .d = 53,});
try std.testing.expect(merged.a == 0);
try std.testing.expect(merged.b == 44);
try std.testing.expect(merged.c == 2);
try std.testing.expect(merged.d == 53);
}

76
src/hncore/Mindmap.zig Normal file
View File

@@ -0,0 +1,76 @@
const std = @import("std");
const Digraph = @import("./Digraph.zig");
pub const Node = @import("./Node.zig");
pub const NodeList = std.ArrayList(*Node);
pub const NodeDigraph = Digraph.Container(*const Node, Digraph.lessThanFuncFor(*const Node));
///
alloc: std.mem.Allocator,
///
nodes: NodeList,
///
digraph: NodeDigraph,
///
root: *Node,
///
pub fn init(alloc: std.mem.Allocator) !@This() {
var nodes = NodeList.init(alloc);
errdefer nodes.deinit();
var digraph = try NodeDigraph.init(alloc, &.{});
errdefer digraph.deinit();
var root = try alloc.create(Node);
errdefer alloc.destroy(root);
root.* = try Node.init(alloc, 0, "helloworld");
errdefer root.deinit(alloc);
try nodes.append(root);
var node1 = try alloc.create(Node);
errdefer alloc.destroy(node1);
node1.* = try Node.init(alloc, 1, "node1");
errdefer node1.deinit(alloc);
try nodes.append(node1);
try digraph.connect(root, node1);
var node2 = try alloc.create(Node);
errdefer alloc.destroy(node2);
node2.* = try Node.init(alloc, 2, "node1");
errdefer node2.deinit(alloc);
try nodes.append(node2);
try digraph.connect(root, node2);
return .{
.alloc = alloc,
.nodes = nodes,
.digraph = digraph,
.root = root,
};
}
///
pub fn deinit(self: *@This()) void {
for (self.nodes.items) |node| {
node.deinit(self.alloc);
self.alloc.destroy(node);
}
self.digraph.deinit();
self.nodes.deinit();
}
///
pub fn serialize(self: *const @This(), writer: anytype) !void {
_ = self;
_ = writer;
}
///
pub fn deserialize(reader: anytype) !@This() {
_ = reader;
}

35
src/hncore/Node.zig Normal file
View File

@@ -0,0 +1,35 @@
const std = @import("std");
///
/// unique and immutable integer
id: usize,
/// summary text of this node
summary: []const u8,
///
pub fn init(alloc: std.mem.Allocator, id: usize, summary: []const u8) !@This() {
return .{
.id = id,
.summary = try alloc.dupe(u8, summary),
};
}
/// pass the same allocator as init() call
pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void {
alloc.free(self.summary);
}
test "serialize" {
const alloc = std.testing.allocator;
var node = try init(alloc, 0, "helloworld");
defer node.deinit(alloc);
var json = std.ArrayList(u8).init(alloc);
defer json.deinit();
try std.json.stringify(node, .{}, json.writer());
try std.testing.expectEqualStrings(
json.items,
\\{"id":0,"summary":"helloworld"}
);
}

16
src/hncore/Project.zig Normal file
View File

@@ -0,0 +1,16 @@
const std = @import("std");
const Mindmap = @import("./Mindmap.zig");
///
mindmap: Mindmap,
///
pub fn init(alloc: std.mem.Allocator) !@This() {
return .{
.mindmap = try .init(alloc),
};
}
pub fn deinit(self: *@This()) void {
self.mindmap.deinit();
}

210
src/hncore/Store.zig Normal file
View File

@@ -0,0 +1,210 @@
const std = @import("std");
/// A container for all existing instances of T.
/// This is like a dedicated allocator for the type, T, with a refcount system.
pub fn Store(comptime T: type) type {
return struct {
///
pub const Item = T;
///
pub const VTable = struct {
///
udata: ?*anyopaque = null,
///
deinitItem: ?*fn (?*anyopaque, *T) void = null,
};
/// A struct which holds an item and its reference count.
pub const Slot = struct {
item : Item,
refcnt: usize,
pub fn ref(self: *@This()) void {
self.refcnt += 1;
}
pub fn unref(self: *@This()) void {
self.refcnt -= 1;
}
};
///
pub const Error = error {
DetectedItemLeak,
};
///
alloc : std.mem.Allocator,
///
vtable: VTable,
///
slots : std.ArrayList(*Slot),
///
pub fn init(alloc: std.mem.Allocator, vtable: VTable) @This() {
return .{
.alloc = alloc,
.vtable = vtable,
.slots = std.ArrayList(*Slot).init(alloc),
};
}
///
pub fn deinit(self: *@This()) !void {
defer self.slots.deinit();
try self.collectGarbage();
if (self.slots.items.len > 0) {
return Error.DetectedItemLeak;
}
}
/// Returns a slot which contains newly-allocated item.
/// - A reference count of the returned slot is 1
/// - The returned slot and its contents are alive till the reference count becomes 0
pub fn add(self: *@This(), item: Item) !*Slot {
for (self.slots.items) |slot| {
if (slot.refcnt == 0) {
self.deinitItem(&slot.item);
slot.* = .{ .item = item, .refcnt = 1, };
return slot;
}
}
const newSlot = try self.alloc.create(Slot);
errdefer self.alloc.destroy(newSlot);
newSlot.* = .{ .item = item, .refcnt = 1, };
try self.slots.append(newSlot);
return newSlot;
}
/// Releases memory of slots whose reference count is 0.
/// Usually no need to call this function because such slots will be reused.
pub fn collectGarbage(self: *@This()) !void {
var slots = self.slots.items;
var set: usize = 0;
var get: usize = 0;
while (set < slots.len) {
if (slots[set].refcnt == 0) {
self.deinitItem(&slots[set].item);
self.alloc.destroy(slots[set]);
get += 1;
if (get >= slots.len) {
break;
}
slots[set] = slots[get];
} else {
set += 1;
get += 1;
}
}
try self.slots.resize(set);
}
fn deinitItem(self: *@This(), item: *Item) void {
if (self.vtable.deinitItem != null) {
self.vtable.deinitItem.?(self.vtable.udata, item);
}
}
};
}
test "add new item" {
const Item = u16;
var sut = Store(Item).init(std.testing.allocator, .{});
defer sut.deinit() catch unreachable;
{
var slot = try sut.add(123);
defer slot.unref();
// check initial value
try std.testing.expect(slot.refcnt == 1);
try std.testing.expect(slot.item == 123);
// check refcnt handling
slot.ref();
try std.testing.expect(slot.refcnt == 2);
slot.unref();
try std.testing.expect(slot.refcnt == 1);
}
}
test "reuse an expired slot by adding right after removal" {
const Item = u16;
var sut = Store(Item).init(std.testing.allocator, .{});
defer sut.deinit() catch unreachable;
var address: usize = undefined;
{
var slot = try sut.add(123);
defer slot.unref();
address = @intFromPtr(slot);
try std.testing.expect(slot.item == 123);
}
{
var slot = try sut.add(456);
defer slot.unref();
try std.testing.expect(address == @intFromPtr(slot));
try std.testing.expect(slot.item == 456);
}
}
test "reusing doesn't happen by adding twice" {
const Item = u16;
var sut = Store(Item).init(std.testing.allocator, .{});
defer sut.deinit() catch unreachable;
var slot1 = try sut.add(123);
defer slot1.unref();
var slot2 = try sut.add(456);
defer slot2.unref();
try std.testing.expect(slot1 != slot2);
try std.testing.expect(slot1.item == 123);
try std.testing.expect(slot2.item == 456);
}
test "chaotic addition and removals" {
const Item = u16;
var sut = Store(Item).init(std.testing.allocator, .{});
defer sut.deinit() catch unreachable;
var slots = std.ArrayList(*Store(Item).Slot).init(std.testing.allocator);
defer {
for (slots.items) |slot| { slot.unref(); }
slots.deinit();
}
// add 0~300
for (0..300) |i| {
const x: u16 = @intCast(i);
try slots.append(try sut.add(x));
}
// removes other than multiples of 3
for (0..100) |i| {
slots.orderedRemove(300 - (i+1)*3 + 2).unref();
slots.orderedRemove(300 - (i+1)*3 + 1).unref();
}
// add multiples of 3 from 300 to 600
for (0..100) |i| {
const x: u16 = @intCast(i);
try slots.append(try sut.add(300 + x*3));
}
// check if the list is composed of multiples of 3 from 0~600
for (0..200) |i| {
const x: u16 = @intCast(i);
try std.testing.expectEqual(slots.items[i].item, x*3);
}
}

View File

@@ -1,4 +1,8 @@
pub const Digraph = @import("./Digraph.zig").Digraph; pub const Mindmap = @import("./Mindmap.zig");
pub const Node = @import("./Node.zig");
pub const Project = @import("./Project.zig");
pub const merge = @import("./Merged.zig").merge;
test { test {
@import("std").testing.refAllDecls(@This()); @import("std").testing.refAllDecls(@This());

View File

@@ -2,23 +2,32 @@ const std = @import("std");
const dvui = @import("dvui"); const dvui = @import("dvui");
const hncore = @import("hncore"); const hncore = @import("hncore");
const ui = @import("./ui.zig");
///
pub const App = struct { pub const App = struct {
///
project: hncore.Project,
///
pub fn init(alloc: std.mem.Allocator) !App { pub fn init(alloc: std.mem.Allocator) !App {
_ = alloc;
return App { return App {
.project = try .init(alloc),
}; };
} }
///
pub fn deinit(self: *App) void { pub fn deinit(self: *App) void {
_ = self; self.project.deinit();
} }
///
pub fn gui(self: *App) !void { pub fn gui(self: *App) !void {
try self.guiMenu();
try self.guiMain();
}
fn guiMenu(self: *@This()) !void {
_ = self; _ = self;
try gui_menu();
try gui_main();
}
fn gui_menu() !void {
var root = try dvui.menu(@src(), .horizontal, .{ .background = true, .expand = .horizontal }); var root = try dvui.menu(@src(), .horizontal, .{ .background = true, .expand = .horizontal });
defer root.deinit(); defer root.deinit();
@@ -31,17 +40,15 @@ pub const App = struct {
} }
} }
} }
fn gui_main() !void { fn guiMain(self: *@This()) !void {
var box = try dvui.scrollArea(@src(), .{}, .{ var overlay = try dvui.overlay(@src(), .{
.expand = .both, .expand = .both,
.color_fill = .{ .name = .fill_window },
.padding = dvui.Rect.all(8), .padding = dvui.Rect.all(8),
.color_fill = .{ .name = .fill_window },
.background = true,
}); });
defer box.deinit(); defer overlay.deinit();
if (try dvui.button(@src(), "Zoom In", .{}, .{})) { try ui.manipulator(self.project.mindmap.root, &self.project.mindmap.digraph);
}
if (try dvui.button(@src(), "Zoom In", .{}, .{})) {
}
} }
}; };

View File

@@ -23,6 +23,7 @@ pub fn main() !void {
defer win.deinit(); defer win.deinit();
var app = try App.init(gpa.allocator()); var app = try App.init(gpa.allocator());
defer app.deinit();
while (true) { while (true) {
try win.begin( try win.begin(
win.beginWait(backend.hasEvent()), win.beginWait(backend.hasEvent()),

53
src/hnet/ui.zig Normal file
View File

@@ -0,0 +1,53 @@
const std = @import("std");
const dvui = @import("dvui");
const hncore = @import("hncore");
///
pub fn manipulator(root: *const hncore.Mindmap.Node, digraph: *const hncore.Mindmap.NodeDigraph) !void {
var overlay = try dvui.overlay(@src(), .{
.expand = .both,
});
defer overlay.deinit();
_ = try nodeTreeInManipulator(.{ .x = 100, .y = 100, }, root, digraph);
}
///
pub fn nodeTreeInManipulator(base: dvui.Point, node: *const hncore.Mindmap.Node, digraph: *const hncore.Mindmap.NodeDigraph) !dvui.Rect {
const size = try nodeInManipulator(base, node);
const childrenRect = nodeChildrenInManipulator(
.{ .x = base.x + size.x, .y = base.y, }, node, digraph);
return .{
.x = base.x,
.y = base.y,
.w = childrenRect.w + size.w,
.h = @max(childrenRect.h, size.h),
};
}
///
pub fn nodeInManipulator(base: dvui.Point, node: *const hncore.Mindmap.Node) !dvui.Rect {
var box = try dvui.box(@src(), .vertical, .{
.id_extra = node.id,
.border = .all(1),
.rect = .{ .x = base.x, .y = base.y, .w = 100, .h = 20, },
});
defer box.deinit();
return box.wd.borderRect();
}
///
pub fn nodeChildrenInManipulator(base: dvui.Point, node: *const hncore.Mindmap.Node, digraph: *const hncore.Mindmap.NodeDigraph) dvui.Rect {
const parentMargin = 16;
const siblingMargin = 8;
var childDepth : f32 = 0;
var childOffset: f32 = 0;
for (digraph.getChildrenOf(node)) |conn| {
const childRect = nodeTreeInManipulator(
.{ .x = base.x + parentMargin, .y = base.y + childOffset, }, conn.to, digraph) catch unreachable;
childOffset += childRect.h + siblingMargin;
childDepth = @max(childRect.w, childDepth);
}
return .{ .x = base.x, .y = base.y, .w = childDepth, .h = childOffset, };
}