Add a virtualized table widget [minor] - #435
Merged
Merged
Conversation
ImGui.Widgets had Grid, TabPanel, PropertyGrid and Tree, none of which virtualize, so a consumer with tens of thousands of rows had to drop to ImGuiListClipper directly — and lost the probe marking that keeps a widget UI-testable on the way down. VirtualTable is backed by ImGuiListClipper, so the draw delegate is called only for visible rows and is given the absolute row index. Uniform row height is the clipper's assumption and stays a documented v1 constraint; sorting is reported through OnSortChanged and never applied, since the widget does not own the data. Marking follows the decisions the issue settled: the table is marked under its label whether or not any row is visible, and each drawn row under <label>/[<absoluteIndex>], following the PropertyGrid list convention. The index is absolute so a probe name does not change with scroll position. Column headings are submitted one at a time rather than through TableHeadersRow, which submits them all and leaves nothing to mark — a sortable table whose headings cannot be clicked is not testable. Fixes #411 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYniXikaVdTF8Gwdwj7uns
|
This was referenced Sep 23, 2026
This was referenced Sep 30, 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.



Fixes #411
ImGui.WidgetshadGrid,TabPanel,PropertyGridandTree, none of which virtualize. A consumer with tens of thousands of rows had to drop toImGuiListClipperdirectly — and lost the probe marking that keeps a widget UI-testable on the way down.drawRowis called only for visible rows, in ascending order, and is given the absolute index into the caller's data.The three decisions the issue settled, implemented as specified
Absolute row indices in probe names. Each drawn row is marked
<label>/[<absoluteIndex>], following theTags/[0]conventionPropertyGridListsalready uses. The issue is right that this is the one call that cannot be changed later, so it is asserted from both directions rather than just written down:RowProbeNamesUseTheAbsoluteIndexscrolls to row 15,000 and checks it is addressable ascatalogue/[15000]and that nothing was marked as[0], which is what a view-relative name would produce.The widget never sorts.
OnSortChanged(column, ascending)reports the change and stops.DoesNotReorderRowsItselfpins that, because "it sorts nothing" is a decision rather than an omission and should fail loudly if someone adds sorting later.Uniform row height, documented rather than discovered.
RowHeightcarries the constraint in its own XML docs, along with why variable height is a different widget rather than a flag on this one.Two things worth reviewing rather than skimming
Headings are submitted one at a time, not through
TableHeadersRow. This is the only place I departed from the obvious implementation.TableHeadersRowsubmits every heading and returns, so there is no moment at which any one of them is the current item and nothing can be marked. On a sortable table the headings are the only control there is — a heading a test cannot click is a table a test cannot sort. I found this by writing the sort tests first and watching them fail withNo item matching 'Index' has been marked; submitting the headings individually is ImGui's own supported alternative and costs one loop.ScrollToRowanchors on the row, not on arithmetic. My first version computedSetScrollY(row * rowHeight). That is wrong whenever the real row pitch differs fromRowHeight— which it does as soon as cell padding is in play, and always when the caller leavesRowHeightunset. It now callsclipper.IncludeItemByIndexto force the row to be drawn andSetScrollHereYon the row itself, so no height arithmetic exists to be wrong.ScrollToRowis one-shot (cleared by the widget) and lands on the same frame rather than the one after.Selection is a full-width
SelectablewithSpanAllColumns | AllowOverlapsubmitted under the caller's cells. It is doing three jobs — the highlight, the hit target, and the one real ImGui item that knows the row's true rectangle — which is why the row's mark comes from it rather than from a rectangle the widget computed.options.SelectedRowis a settable property rather than theref intthe issue's prose describes, since the signature it also specifies takes norefparameter; options are already passed by reference, so the read-write behaviour is the same. Flagging it as the one place the issue's two descriptions disagree.Tests
tests/ImGui.Widgets.UITests/VirtualTableTests.cs, 16 cases, one class driving the widget with nothing else on screen per the suite's convention. Full widget suites green: 382 UI tests and 283 unit tests, 0 failed.Confirmed the tests depend on the change by breaking it three ways and re-running:
DrawsABoundedNumberOfRowsWhateverTheRowCount,OnlyDrawnRowsAreProbeAddressable,ScrollToRow_MovesTheViewportRatherThanJustDrawingTheRow,RowProbeNamesUseTheAbsoluteIndexRowProbeNames…assertions andSelectsTheAbsoluteRowThatWasClickedIncludeItemByIndex+SetScrollHereYremovedScrollToRowcaseThe header-marking change earned its place the same way: before it, the two sort tests failed outright.
DrawsABoundedNumberOfRowsWhateverTheRowCountis the one that matters — 30,000 rows, and the assertion is that fewer than 200 were drawn. Without the clipper it draws all 30,000.Demo
examples/ImGuiWidgetsDemo/VirtualTableDemo.cs, a 30,000-row catalogue that reports how many rows were actually drawn last frame, plus a "go to row" control exercisingScrollToRow. Its rows are synthesized from the index — there is no list of 30,000 anything behind it, which is only affordable because of the widget.It is its own class rather than another section in
ImGuiWidgetsDemo, because adding it there trippedCA1506(class coupling 99 against a limit of 96)."Virtual Table"is added toAdvancedDemoSectionsin the demo suite.Not covered, and one thing that isn't mine
SortMultiis not supported; only the primary sort column is reported, and the XML docs say so rather than leaving a caller to find out. There is no test that a row taller thanRowHeightmisbehaves — that is the documented constraint, and asserting the failure mode would pin behaviour the widget does not promise.12 of 29
ImGuiWidgetsDemo.UITestsfail in my container, and they fail identically on an unmodified tree — I stashed everything and re-ran to check. The cause is environmental, not a repo defect:examples/ImGuiWidgetsDemo/ktsu.pngis still a Git LFS pointer here (git lfsis not installed), so the demo's texture load throwsUnrecognised image format; the file starts with 76 65 72 73— that beingversofversion https://git-lfs.... They should pass on a runner with LFS. Worth knowing becauseAdvancedDemos_ListsEverySectionis among them, so my addition to its section list is unverified by a passing run and is the one part of this change CI will be the first to actually exercise.🤖 Generated with Claude Code
https://claude.ai/code/session_01JYniXikaVdTF8Gwdwj7uns
Generated by Claude Code