Add a property grid to the widget library - #381
Merged
Conversation
Closes #379. ImGuiWidgets.PropertyGrid lays out one labelled editor per property in a two-column resizable table, opened in a using statement and holding no model of its own: each row edits a variable by reference and reports whether it changed, and the grid accumulates those answers in Changed. One overloaded Value row covers bool, int, long, float, double, string, Vector2/Vector3 and their new DoubleVector2/DoubleVector3 counterparts, and the semantic Color. Enum, FilePath, DirectoryPath and ImagePath cover the rest, Section draws collapsible groups, and List draws one editor per element with append and remove buttons -- with an overload per supported element type, since any row method is an element editor. Paths and thumbnails are delegated rather than owned: the library knows nothing about file dialogs or texture upload, and taking on Hexa's dialogs would make the deferred-drawing pump a hard requirement. A browse button raises PropertyGridOptions.OnBrowse with a request the host completes whenever its dialog closes -- from any thread and any later frame -- which the row adopts on its next draw; an image row asks ThumbnailResolver for a texture and draws an empty preview frame when there is none. Also adds a Property Grid section to ImGuiWidgetsDemo, wired to Hexa's file dialogs and to ImGuiApp's texture cache, an isolation suite covering every row type, and a demo-suite test driving the section end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTgzo1j271VvvXUNyLi3sL
…licitly Two findings from the code quality review, both in the pending-browse table. The tick a pending result is stamped with was incremented directly from the grid's constructor, which writes a static field from an instance member and, more to the point, is not atomic. It is read from whatever thread answers a browse, so it is now advanced through Interlocked and read back the same way, with the reader taking one value and passing it into the sweep rather than each comparison reading the field again. The sweep filtered its copied key array with an if inside the loop; the condition now sits on the sequence where the rule wants it. The copy is still what keeps the removal safe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTgzo1j271VvvXUNyLi3sL
Seven non-blocking issues from the Sonar analysis, all in the new files. The Enum row sat in the middle of the Value overloads, splitting the group in two; it now follows them, so the overloads are adjacent. The rest are the MSTest assertions the analyzer prefers for what each one is actually checking: element counts through HasCount, a sequence through AreSequenceEqual, and a lower bound through IsGreaterThan, each of which reports the real value rather than "expected true". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTgzo1j271VvvXUNyLi3sL
Section returned a bool and left EndSection to the caller, which is the one thing in the grid that had to be remembered rather than expressed -- and forgetting it does not fail visibly, it unbalances ImGui's tree stack. It now returns a SectionScope disposable, so a section is a using block like every other scope in this library. The scope does more than pair the call: while the section is collapsed it raises the grid's suppression depth and every row draws nothing, so the rows inside are written exactly as they are anywhere else, with no test around them. That is the same answer the grid already gave for a table that never opened, so there is one rule rather than two. A depth rather than a flag is what lets a section nested inside a collapsed one stay balanced on its own. SectionScope.IsOpen remains for the one case the suppression cannot cover: skipping work that costs something to prepare before a row can be called. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTgzo1j271VvvXUNyLi3sL
The two assertions on SectionScope.IsOpen compared a nullable bool against a literal, where IsTrue and IsFalse say the same thing more directly and report what the value actually was. Null still fails either of them, which is the answer wanted: it means the section body never ran at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTgzo1j271VvvXUNyLi3sL
|
This was referenced Sep 9, 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.



Closes #379.
ImGuiWidgets.PropertyGridlays out one labelled editor per property in a two-column resizable table. It is immediate mode like the rest of the library: it holds no model, each row edits a variable by reference and returns whether it changed, andChangedaccumulates those answers so the whole grid can be tested once.What is covered
Everything the issue asked for. One overloaded
Valuerow coversbool,int,long,float,double,string,Vector2/Vector3and their newDoubleVector2/DoubleVector3counterparts, and the semanticColor;Enum, the three path rows andListcover the rest. The numeric rows have clamped overloads taking a range.A
Listrow draws one editor per element, a+to append and anxto remove. Any row method is an element editor, so there is an overload per supported element type, plusFilePathList/DirectoryPathList/ImagePathListfor the three whose element type is also a string, and a generic overload taking an editor and a factory for anything else.A collapsed section holds its own rows back
Sectionreturns aSectionScopedisposable, so a section is ausingblock like every other scope in this library — there is nothing to remember to call, and forgetting anEndSectionno longer unbalances ImGui's tree stack.The scope does more than pair the call: while the section is collapsed it raises the grid's suppression depth and every row draws nothing, so the rows inside are written exactly as they are anywhere else, with no test around them. That is the same answer the grid already gave for a table that never opened, so there is one rule rather than two, and a depth rather than a flag is what lets a section nested inside a collapsed one stay balanced on its own.
SectionScope.IsOpenremains for the one case suppression cannot cover: skipping work that costs something to prepare before a row can be called.Paths and thumbnails are delegated, not owned
ktsu.ImGui.Widgetshas no business knowing how the host picks files, and reaching for Hexa's own dialogs would make the deferred-drawing pump a hard requirement of every property grid. So a browse button raisesPropertyGridOptions.OnBrowsewith aPropertyPathRequestinstead. A dialog outlives the frame that opened it, so the request carries no reference to the value:Completerecords the answer under the row's ImGui id — safe from any thread and any later frame — and the row adopts it on its next draw. Completing withnullleaves the value alone, which is what a cancelled dialog should do. Without a handler the browse button is disabled and paths can still be typed.Texture upload lives in
ktsu.ImGui.App, which this library deliberately does not reference, so an image row asksPropertyGridOptions.ThumbnailResolverfor a texture id and draws an empty preview frame when there is none, keeping its height stable once a picture appears.Other options:
ReadOnly,LabelColumnWeight/LabelColumnWidth,ListsStartExpanded,ThumbnailSize,FloatFormat/DoubleFormat.Demo and tests
examples/ImGuiWidgetsDemogains a Property Grid section showing every row type, with read-only and label-width toggles, browsing wired to Hexa's file dialogs and thumbnails wired toImGuiApp.GetOrLoadTexture.tests/ImGui.Widgets.UITests/PropertyGridTests.cs— 23 headless tests covering every row type, change reporting, read-only, the browse round trip (including cancellation and the kind each row asks for), thumbnails with and without a texture, list add/remove/edit, and sections: every row type surviving a collapsed one, nesting,IsOpen, and rows after a section not being held back by it.tests/ImGuiWidgetsDemo.UITestsdrives the new demo section end to end.The suites caught a real bug: every browse button shared one ImGui id, as did the add buttons of two lists in one grid, because a row draws into a single id stack. Each button's id now carries its row's label.
Verification
dotnet build ImGui.sln -c Release— clean.tests/ImGui.Widgets.UITests— 340/340 pass (23 of them this widget's).tests/ImGui.Widgets.Tests— 256/256 pass.tests/ImGuiWidgetsDemo.UITests— the new test passes; the 12 failures in that suite are pre-existing in this container (ktsu.pngis an unfetched Git LFS pointer, soGetOrLoadTexturethrows) and are identical with and without this change.Docs updated:
ImGui.Widgets/README.md(feature entry plus a Property Grid section), the rootREADME.mdwidget lists, andCLAUDE.md(widget list, key files, and a Property grid section covering the section suppression, the browse handshake, the resolver contract, the per-row button ids and the list probe names).🤖 Generated with Claude Code
https://claude.ai/code/session_01GTgzo1j271VvvXUNyLi3sL