datastruct: fix split tree ascii diagram

pull/8207/head
Mitchell Hashimoto 2025-08-07 11:41:15 -07:00
parent 75dd8e46b5
commit fbe28477ff
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 143 additions and 32 deletions

View File

@ -379,7 +379,7 @@ pub const SplitTree = extern struct {
) *gtk.Widget {
switch (tree.nodes[current]) {
.leaf => |v| {
// We have to setup our signal handlers.
v.as(gtk.Widget).unparent();
return v.as(gtk.Widget);
},

View File

@ -409,22 +409,7 @@ pub fn SplitTree(comptime V: type) type {
assert(reffed == nodes.len - 1);
}
/// 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 each split
/// being exactly 1 unit wide and high. The x and y coordinates
/// are offsets into that space. 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.
/// 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.
@ -445,8 +430,22 @@ pub fn SplitTree(comptime V: type) type {
}
};
/// Returns the spatial representation of this tree. See Spatial
/// for more details.
/// 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 each split
/// being exactly 1 unit wide and high. The x and y coordinates
/// are offsets into that space. 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,
@ -549,14 +548,20 @@ pub fn SplitTree(comptime V: type) type {
};
}
/// Format the tree in a human-readable format.
/// Format the tree in a human-readable format. By default this will
/// output a diagram followed by a textual representation. This can
/// be controlled via the formatting string:
///
/// - `diagram` - Output a diagram of the split tree only.
/// - `text` - Output a textual representation of the split tree only.
/// - Empty - Output both a diagram and a textual representation.
///
pub fn format(
self: *const Self,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
if (self.nodes.len == 0) {
@ -564,6 +569,48 @@ pub fn SplitTree(comptime V: type) type {
return;
}
if (std.mem.eql(u8, fmt, "diagram")) {
self.formatDiagram(writer) catch
try writer.writeAll("failed to draw split tree diagram");
} else if (std.mem.eql(u8, fmt, "text")) {
try self.formatText(writer, 0, 0);
} else if (fmt.len == 0) {
self.formatDiagram(writer) catch {};
try self.formatText(writer, 0, 0);
} else {
return error.InvalidFormat;
}
}
fn formatText(
self: *const Self,
writer: anytype,
current: Node.Handle,
depth: usize,
) !void {
for (0..depth) |_| try writer.writeAll(" ");
switch (self.nodes[current]) {
.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: {s}, ratio: {d:.2})\n", .{
@tagName(s.layout),
s.ratio,
});
try self.formatText(writer, s.left, depth + 1);
try self.formatText(writer, s.right, depth + 1);
},
}
}
fn formatDiagram(
self: *const Self,
writer: anytype,
) !void {
// 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
@ -573,7 +620,29 @@ pub fn SplitTree(comptime V: type) type {
const alloc = arena.allocator();
// Get our spatial representation.
const sp = try self.spatial(alloc);
const sp = spatial: {
const sp = try self.spatial(alloc);
// Scale our spatial representation to have minimum width/height 1.
var min_w: f16 = 1;
var min_h: f16 = 1;
for (sp.slots) |slot| {
min_w = @min(min_w, slot.width);
min_h = @min(min_h, slot.height);
}
const ratio_w: f16 = 1 / min_w;
const ratio_h: f16 = 1 / min_h;
const slots = try alloc.dupe(Spatial.Slot, sp.slots);
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: {
@ -795,7 +864,7 @@ test "SplitTree: single node" {
var t: TestTree = try .init(alloc, &v);
defer t.deinit();
const str = try std.fmt.allocPrint(alloc, "{}", .{t});
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---+
@ -830,15 +899,17 @@ test "SplitTree: split horizontal" {
\\+---++---+
\\| 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();
// Split right at B
var it = t3.iterator();
var t4 = try t3.split(
alloc,
@ -856,12 +927,52 @@ test "SplitTree: split horizontal" {
const str = try std.fmt.allocPrint(alloc, "{}", .{t4});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---++---++---+
\\| A || B || C |
\\+---++---++---+
\\+--------++---++---+
\\| 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,
&tD,
);
defer t5.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{}", .{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);
}
}
test "SplitTree: split vertical" {
@ -883,7 +994,7 @@ test "SplitTree: split vertical" {
);
defer t3.deinit();
const str = try std.fmt.allocPrint(alloc, "{}", .{t3});
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t3});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---+
@ -926,7 +1037,7 @@ test "SplitTree: remove leaf" {
);
defer t4.deinit();
const str = try std.fmt.allocPrint(alloc, "{}", .{t4});
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t4});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---+
@ -969,7 +1080,7 @@ test "SplitTree: split twice, remove intermediary" {
defer split2.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{}", .{split2});
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split2});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---++---+
@ -995,7 +1106,7 @@ test "SplitTree: split twice, remove intermediary" {
defer split3.deinit();
{
const str = try std.fmt.allocPrint(alloc, "{}", .{split3});
const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split3});
defer alloc.free(str);
try testing.expectEqualStrings(str,
\\+---+