Add collapsible Branch and Leaf nodes to the Tree widget - #419
Merged
Merged
Conversation
Tree drew connector lines around whatever was nested inside it and nothing else, so anyone wanting a tree that actually collapses had to pair it with ImGui.TreeNodeEx themselves. That pairing has a trap in it: Tree indents its own contents, ImGui's tree push indents them again, and every level marches further right than the lines are drawn. The fix is NoTreePushOnOpen on each node, which is not discoverable from either API. Branch and Leaf do that pairing correctly. A branch takes its body as a callback and invokes it only while expanded, so there is no open-state check to write and none to forget, and a collapsed branch never constructs the nested Tree at all, which is why it neither indents nor draws a spine over no children. Branches find their parent from a thread-static stack of open trees rather than being handed one. The handle would carry no information: every member of Tree is private geometry, so a caller could read nothing from it, and the nesting is already lexical. ImGui is an ambient-context API throughout, and ImGui.Text does not ask which window either. A branch with no enclosing branch is a root row, and a root row has nothing above it to join to, so it draws no connector. State overloads let callers pass a static lambda and allocate no closure per node per frame, which matters once a tree is large. Purely additive. The Tree constructor, Child and TreeChild are untouched, so the scope form still works and still draws a spine at the outermost level. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| _ = openTrees!.Pop(); | ||
| }; | ||
|
|
||
| (openTrees ??= new Stack<Tree>()).Push(this); |
Every one of the 26 ImGuiAppDemo UI tests was failing in SetUp with MissingManifestResourceException. The resx embeds under a manifest name derived from RootNamespace, which ktsu.Sdk computes from AuthorsNamespace and ProjectNamespace, while Resources.Designer.cs carries the name as a string baked in when it was last generated. The two had drifted: the assembly holds ktsu.ImGuiAppDemo.Properties.Resources.resources and the designer was asking for ktsu.examples.ImGuiAppDemo.Properties.Resources. Nothing catches that at build time, because both halves are individually valid and only meet at run time on the first lookup. The designer now asks for the name the resource is actually embedded under, and LogicalName pins that name in the project file so a future RootNamespace change cannot desynchronise them again. Aligning on the MSBuild-derived name rather than the stale one also means regenerating the designer reproduces a matching string instead of breaking it a second time. Pre-existing on main rather than anything this branch introduced, and reproduced on Windows as well as the Ubuntu runner, so it was never platform specific. The macOS job was not running this project. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New-code coverage came back at 46.5 against a threshold of 80, because the branch and leaf paths arrived with no tests and the demo drew them collapsed, so their bodies never ran under test either. Nine tests in TreeTests drive them through the existing widget harness rather than leaving them to visual inspection: a collapsed branch does not run its body and an expanded one does, DefaultOpen reaches the underlying node, a branch with nothing enclosing it still draws, each level indents past the last, the connectors are actually drawn, and both state overloads hand their state through. The last of them is the one worth having. A branch finds its parent on a stack, so a body that throws must still unwind it, or everything drawn afterwards is parented to a tree that has gone. That is invisible until it happens and cheap to assert now. The demo drew its branches collapsed, which showed the feature off poorly and left its callbacks unexecuted. They are DefaultOpen now, and the demo test asserts the nodes inside them. Opening the demo's state example needed flags and state together, which the API could not express. That gap would have met the first person who wanted both, so Branch<TState> now has a flags overload to match the non-generic one, and the three-argument form delegates to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This was referenced Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Written by Claude (Claude Code) on behalf of Matt Edmondson.
Treedrew connector lines around whatever was nested inside it and nothing else, so a tree that actually collapses meant pairing it withImGui.TreeNodeExyourself. That pairing has a trap in it:Treeindents its own contents, ImGui's tree push indents them again, and every level marches further right than the lines are drawn.NoTreePushOnOpenon each node is the fix, and it is discoverable from neither API.BranchandLeafdo that pairing correctly.Design notes
Why the body is a callback. A collapsed branch has to skip its content, and a
usingscope cannot decline to run its own body. Deferring it means there is no open-state check to write and none to forget. It also means a collapsed branch never constructs the nestedTreeat all, which is why it neither indents nor draws a spine over no children — no suppression flag needed.Why branches are not handed their parent. Every member of
Treeis private geometry, so a caller could read nothing useful from a handle, and the nesting is already lexical. The parent comes from a[ThreadStatic]stack of open trees instead. ImGui is an ambient-context API throughout andImGui.Textdoes not ask which window either. The scopes keep the stack balanced even when a body throws.Root rows. A branch with no enclosing branch draws no connector, because it has nothing above it to join to — which is what file explorers do. That also means no wrapper object and no
InvalidOperationExceptionfor "no tree open".Allocation. State overloads let the callback be
staticand capture nothing, so a large tree does not pay one closure per node per frame.Compatibility
Purely additive. The
Treeconstructor,ChildandTreeChildare untouched, so the scope form still compiles and still draws a spine at the outermost level, which a bareBranchdoes not. Tagged[minor].Testing
Solution builds with 0 warnings under warnings-as-errors.
ImGui.Widgets.Testspasses 283/283.Not verified by running the demo: the drawing itself. Per this repo's own convention the draw paths are verified visually in
ImGuiWidgetsDemo, and the Tree View section there now exercises branches, leaves, nesting and the state overloads — but I have not looked at the window, so the indent alignment and connector geometry want an eye before merge.🤖 Generated with Claude Code