diff --git a/core/widgets/TreeWidget.zig b/core/widgets/TreeWidget.zig index 66ddc5531..7f329db0a 100644 --- a/core/widgets/TreeWidget.zig +++ b/core/widgets/TreeWidget.zig @@ -426,6 +426,10 @@ pub const Branch = struct { /// inside it are carried with it. carried_source: bool = false, expanded: bool = false, + /// Left/Right pressed on this row this frame: `expander()` toggles the branch the way a + /// click does (animated, and before the caller reads `expanded` back), so a caller passing + /// last frame's open state cannot undo it. + key_toggle: bool = false, can_expand: bool = false, anim: ?*dvui.AnimateWidget = null, /// Populated while `expander_vbox` is active: clip children to `anim`'s content (animated height). @@ -703,13 +707,13 @@ pub const Branch = struct { if (self.expanded) { dvui.tabIndexNextEx(e.num, self.tree.group.tab_index_prev, self.tree.data().id, false); } else { - self.expanded = true; + self.key_toggle = true; } }, .left => { e.handle(@src(), self.button.data()); if (self.expanded) { - self.expanded = false; + self.key_toggle = true; } else if (self.parent_focus_id) |pid| { dvui.focusWidget(pid, null, e.num); } else { @@ -767,7 +771,7 @@ pub const Branch = struct { default_opts.override(opts), ); - if (clicked or self.init_options.expanded != self.expanded) { + if (clicked or self.key_toggle or self.init_options.expanded != self.expanded) { if (self.expanded) { self.anim.?.init_opts.easing = motion.settle; self.anim.?.init_opts.duration = motion.duration(@divTrunc(self.init_options.animation_duration, 2)); diff --git a/tests/integration.zig b/tests/integration.zig index dc781c089..f1d96f3ef 100644 --- a/tests/integration.zig +++ b/tests/integration.zig @@ -7242,6 +7242,60 @@ test "tree: a drag put down is not dropped where the pointer last was" { try std.testing.expect(CarriedTree.landed == null); } +// A key fold sticks. The caller below keeps its own open state and passes it back each frame, as +// the settings, file and guides trees do, reading `branch.expanded` after `expander()`: Left must +// fold the branch (animated, as a click does) and leave it folded, and Right open it again. +const KeyFoldTree = struct { + const TW = fizzy.core.widgets.TreeWidget; + + var focus = false; + var open = true; + var body_drawn = false; + + fn frame() anyerror!dvui.App.Result { + var tree = TW.tree(@src(), .{}, .{ .expand = .both }); + defer tree.deinit(); + const branch = tree.branch(@src(), .{ .expanded = open }, .{ .expand = .horizontal }); + defer branch.deinit(); + if (focus) { + focus = false; + dvui.focusWidget(branch.button.data().id, null, null); + } + dvui.labelNoFmt(@src(), "row", .{}, .{ .min_size_content = .{ .w = 120, .h = 20 } }); + body_drawn = false; + if (branch.expander(@src(), .{}, .{ .expand = .horizontal })) { + body_drawn = true; + dvui.labelNoFmt(@src(), "child", .{}, .{ .min_size_content = .{ .w = 120, .h = 20 } }); + } + open = branch.expanded; + return .ok; + } +}; + +test "tree: a key fold stays folded when the caller passes its open state back" { + var t = try dvui.testing.init(.{ .allocator = std.testing.allocator }); + defer t.deinit(); + KeyFoldTree.focus = true; + KeyFoldTree.open = true; + try dvui.testing.settle(KeyFoldTree.frame); + try std.testing.expect(KeyFoldTree.open and KeyFoldTree.body_drawn); + + try dvui.testing.pressKey(.left, .none); + // The fold animates: the body is still drawn the frame after the key, closing. + _ = try dvui.testing.step(KeyFoldTree.frame); + try std.testing.expect(KeyFoldTree.body_drawn); + try dvui.testing.settle(KeyFoldTree.frame); + try std.testing.expect(!KeyFoldTree.open and !KeyFoldTree.body_drawn); + _ = try dvui.testing.step(KeyFoldTree.frame); + try std.testing.expect(!KeyFoldTree.open); + + try dvui.testing.pressKey(.right, .none); + try dvui.testing.settle(KeyFoldTree.frame); + try std.testing.expect(KeyFoldTree.open and KeyFoldTree.body_drawn); + _ = try dvui.testing.step(KeyFoldTree.frame); + try std.testing.expect(KeyFoldTree.open); +} + test "a frost never captures more than the picture it reads" { const within = fizzy.core.widgets.BlurBackdrop.within; const Rect = dvui.Rect.Physical;