Skip to content

Use non-virtualized scrolling for entity Views with fewer than 100 objects #1618

Closed

Description

@JoshuaRowePhantom

Summary

Scrolling entity-card trees in view-model-backed Views is reported as jumpy for small populations (for example, fewer than 100 objects). Use ordinary, non-virtualized scrolling for fewer than 100 object nodes while retaining virtualization for 100 or more; scrolling should remain smooth without sacrificing the large-view performance restored by #1177.

Root Cause (verified scope)

features/Phantom.Workspaces.Gui.Shared/Styles/SharedStyles.axaml:303-327 assigns a VirtualizingStackPanel unconditionally as the ItemsPanel for both TreeView.entity-card-tree and TreeView.entity-card-tree-view:

<Setter Property="ItemsPanel">
    <ItemsPanelTemplate>
        <VirtualizingStackPanel Orientation="Vertical"
                                HorizontalAlignment="Stretch"
                                MinWidth="160"
                                MaxWidth="{Binding $parent[ScrollViewer].Viewport.Width}" />
    </ItemsPanelTemplate>
</Setter>

There is no small-population alternative. EntityBrowserWorkspaceTabView.axaml:31-38 binds entity-card-tree to EntityList.Items; MainWindow.axaml:224-228 binds the same style to CurrentViewPopulation.RootEntities. The former collection includes hierarchical items (EntityListViewModel.cs:8,88-130); the latter is only the roots of a population whose Entities collection includes the nodes (ViewPopulationViewModel.cs:24-26). Thus testing only RootEntities.Count would miss a large nested view. The virtualization-to-jump mechanism has not been reproduced or measured here; the confirmed defect is that small trees cannot opt out of the virtualizing panel. Reproduce/measure the reported jump before attributing its precise cause to viewport estimation, variable card heights, or recycling.

Affected Files

File Role
features/Phantom.Workspaces.Gui.Shared/Styles/SharedStyles.axaml:303-327 Shared tree panel and horizontal width/inner-scroll constraints; introduce both panel regimes without losing these constraints.
features/Phantom.Workspaces/Templates/EntityBrowserWorkspaceTabView.axaml:31-38 / features/Phantom.Workspaces/ViewModels/EntityListViewModel.cs:8,88-130 Browser tree and complete hierarchical Items count.
features/Phantom.Workspaces/MainWindow.axaml:224-228 / features/Phantom.Workspaces/ViewModels/ViewPopulationViewModel.cs:24-26 Primary View tree: root binding and complete object-node count (Entities).
features/Phantom.Workspaces.Agent.Gui/Controls/AgentChatEditorControl.axaml:287-295 / features/Phantom.Workspaces.Agent.Gui/Controls/AgentChatToolsDetailControl.axaml:17-22 Other consumers of these shared styles; check that changing the shared selector does not silently change their navigation-tree behavior.
features/Phantom.Workspaces.Tests/EntityListViewModelTests.cs:418-447 Existing 1,000-item bounded-realization test to preserve.

Design / Fix

  1. Expose a cheap, collection-change-aware complete object-node count for each affected entity View (browser: EntityList.Items.Count; primary View: CurrentViewPopulation.Entities.Count, not RootEntities.Count). Define the boundary precisely: count < 100 uses a non-virtualizing vertical StackPanel; count >= 100 uses VirtualizingStackPanel. Count nested nodes even when a parent is collapsed; avoid enumerating/constructing field editors to count. For views that hide/filter items, explicitly decide and test whether the threshold counts all population nodes or currently displayed nodes, rather than accidentally counting roots.
  2. Drive a named small/large class (or equivalent explicit tree state) from that count and apply mutually exclusive ItemsPanel styles. Sketch: TreeView.entity-card-tree.small -> <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" MinWidth="160" MaxWidth="{Binding $parent[ScrollViewer].Viewport.Width}" />; TreeView.entity-card-tree.large -> the current VirtualizingStackPanel with the same constraints. Do the same for entity-card-tree-view only where its consumer should participate; preserve its background, shared item template, sticky behavior, and a single inner vertical/horizontal scroller. Do not simply replace virtualization globally: View becomes unresponsive with thousands of entity cards (no UI virtualization + eager per-card field-editor build) #1177 fixed a freeze with thousands of cards.
  3. Handle count crossing 99 ↔ 100 during live updates, filtering or a View change without losing expansion, selected object/focus, or the visible scroll anchor/offset. Avoid wholesale tree recreation where possible. Confirm the small-tree scroll is smoother in the reported scenario and investigate any remaining jump separately rather than assuming the panel switch solves all causes. Do not eagerly create field editors for offscreen cards.

Expected Tests

Test Name Class What It Verifies
EntityCardTreeView_WhenBoundToFewerThanHundredItems_UsesNonVirtualizingPanel EntityListViewModelTests For 0, 1 and 99 nodes, the styled tree uses StackPanel and its contents scroll normally.
EntityCardTreeView_WhenBoundToHundredItems_UsesVirtualizingPanel EntityListViewModelTests Boundary at exactly 100 uses VirtualizingStackPanel; existing EntityCardTreeView_WhenBoundToThousandsOfItems_RealizesBoundedNumberOfContainers still passes.
EntityCardTreeView_WhenNestedViewHasHundredNodes_UsesVirtualizingPanel EntityListViewModelTests (or primary View GUI test class) A few root nodes with 100 total descendants are not misclassified as small.
EntityCardTreeView_WhenPopulationCrossesThreshold_PreservesSelectionAndScroll EntityListViewModelTests (or GUI test class) Add/remove across 99/100 switches panels without losing expansion, selection/focus, scroll anchor and horizontal width behavior.

Related: #1177 (closed, large-view virtualization and deferred card-editor construction). No changes to unrelated logs/diff list virtualization are requested here.

Activity

  1. JoshuaRowePhantom commented on Oct 7, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Design

    The unconditional shared ItemsPanel virtualizes even a small tree, and counting primary View roots would miss collapsed descendants. I will retain the existing virtualizing style as the default for agent-chat and other consumers, and add an opt-in small-tree class for the browser and primary View only. Bind an attached total-node-count property to EntityList.Items.Count and CurrentViewPopulation.Entities.Count respectively (including hidden and collapsed nodes). Its transition handler will preserve the inner scroller offset and restore focus after the panel swap; it will not replace the TreeView or ItemsSource. Shared styles will keep the existing horizontal constraints on both panels. Tests will exercise real styled headless trees at 0/1/99/100/1000 and nested-root boundaries, plus live threshold transitions, selection, expansion and scrolling. Measurement will be reported only if reproducible. Files: shared styles, browser and main-window XAML, an attached behavior in the app, and targeted GUI tests.

  2. JoshuaRowePhantom commented on Oct 8, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Implementation notes

    Commit: b20448f

    Deviations from proposed design

    The two affected trees opt in through an attached total-node-count property; unrelated consumers keep the existing virtualizing default. Switching panels retains the same TreeView/ItemsSource and restores realized expansion, focus and the scroll offset after layout. The threshold counts all population nodes, including hidden and collapsed descendants, not just displayed/visible roots. No desktop scrolling-jump measurement was available in headless tests; smoothness is not claimed and needs manual GUI acceptance.

    Scope changes

    None. Shared small-panel style is opt-in; other shared-style consumers remain virtualized.

    Files changed

    • Phantom.Workspaces.Gui.Shared/Styles/SharedStyles.axaml
    • Phantom.Workspaces/Controls/EntityTreePanel.cs
    • Phantom.Workspaces/MainWindow.axaml
    • Phantom.Workspaces/Templates/EntityBrowserWorkspaceTabView.axaml
    • Phantom.Workspaces.Tests/EntityTreePanelTests.cs

    Validation

    Full solution dotnet build --no-incremental --no-restore succeeded. Targeted EntityTreePanelTests,EntityListViewModelTests passed (22/22) on 10 consecutive stability runs using the existing scripts scoped to the GUI test project; script restored afterward. Headless styled trees cover 0, 1, 99, 100, 1000, collapsed nested boundary, narrow viewport, default shared-style consumers and bidirectional live transitions.

  3. JoshuaRowePhantom commented on Oct 8, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification result

    Status: ❌ 4 issue(s) found
    Commits checked: b20448f Fix #1618: switch entity Views to nonvirtualized panel below 100 nodes

    Outstanding gaps

    • Visible scroll anchor is not preserved on live changes. EntityTreePanel.UpdatePanel saves/restores only ScrollViewer.Offset; inserting/removing a card ahead of the viewport changes which object occupies that pixel offset. The issue requires preserving the visible object/anchor across 99 ↔ 100, including dynamic collection changes. Add an item-identity/relative-position anchor or equivalent, and test inserts/removals before a scrolled-to card.
    • Focus preservation is incomplete. The handler captures the focused child control but, if panel replacement detaches it, restores focus to its containing TreeViewItem instead. Focus on an editor/button is lost; an offscreen focused item may have no realized replacement at all. Test and retain focused element/semantic focus through both transitions, including realistic entity cards.
    • Production integration and branch coverage are missing. Headless tests use manually constructed TreeViews and synthetic collections; none instantiate the actual browser template or primary View binding (CurrentViewPopulation.Entities.Count versus roots), change selected Views, filter/hide items, or assert their styled panels at 0/1/99/100/thousands. The nested-100 test supplies a synthetic independent node count, not an actual nested primary View population. Existing tests do cover a shared styled tree, threshold changes, narrow width, and default virtualization, but not the affected production bindings/view changes.
    • Reported scrolling smoothness remains unverified. No desktop reproduction or before/after measurement of the jump is provided. Headless panel-type assertions cannot establish that the reported small-view jump is resolved; manual GUI acceptance is still required. Do not infer smoothness from the passing tests.

    Tests

    ./scripts/run-tests.ps1 -Mode fast -TestNames EntityTreePanelTests,EntityListViewModelTests (worktree 13): scripts/test-results.log reports Executed: 22; Passed: 22; Failed: 0; Skipped: 0. Existing thousands-of-items bounded-realization test passes. No filesystem or Git repository layers are changed; needs-slow-tests does not apply. No new tests were skipped; no new TODO, sleep/polling or synchronous task blocking detected.

  4. JoshuaRowePhantom commented on Oct 8, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification follow-up

    Commit: 50ba65505e2a6cff32e11e3a8d3eaa27688c22f1 (after b20448fcd6b8464fb3d06fa2c2474afeeb77f365)

    • Capture the currently visible tree item by object identity and its position relative to the inner scroller. After a panel change, realize the item if needed and correct the scroll offset by its measured position difference. Regression test inserts and removes a taller item before a scrolled-to card at 99↔100 and checks that the original card stays at its prior viewport coordinate.
    • Restore focus inside the same item via its visual route through the item template, rather than falling back unconditionally to the TreeViewItem; test a focused nested child editor button in both directions.
    • Exercise the instantiated browser template and MainWindow primary View with real view models, collapsed descendants, hide-filter, selected-View change, 0/1/99/100/1000 browser nodes, and bounded realization. This exposed a stale compiled browser binding to Items.Count; the browser now raises an explicit EntityList.NodeCount notification on collection changes.
    • Files changed: Phantom.Workspaces/Controls/EntityTreePanel.cs, Phantom.Workspaces/ViewModels/EntityListViewModel.cs, Phantom.Workspaces/Templates/EntityBrowserWorkspaceTabView.axaml, and tests in EntityTreePanelTests.cs, EntityBrowserWorkspaceTabViewModelTests.cs, MainWindowIntegrationTests.cs.

    Full solution non-incremental build passed. Focused GUI tests passed 26/26 on each of 10 consecutive runs through the existing test/stability scripts, scoped to the GUI test project. Headless tests verify panel selection and object-anchor position, not subjective scrolling smoothness. The existing headless/process-acceptance infrastructure does not provide a measured desktop wheel-scroll trace for this entity-card view; the reported jump has not been reproduced or quantified before/after. Desktop GUI smoothness remains a genuine manual acceptance check; no smoothness improvement is claimed as measured.

  5. JoshuaRowePhantom commented on Oct 8, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification result

    Status: ❌ 4 issue(s) found
    Commits checked: 50ba6550 (follow-up), b20448fc against features.

    Confirmed improvements

    The instantiated browser now binds an explicitly notified EntityList.NodeCount; the instantiated primary View binds CurrentViewPopulation.Entities.Count (not roots). Headless integration exercises collapsed 100-node populations, hide-filter, View changes, and browser 0/1/99/100/1000 with bounded realization. A variable-height insert/remove-before-viewport test verifies the same visible object's screen position through both panel transitions. A nested button-focus test passes. Shared-style trees without opt-in remain virtualized; narrow horizontal width and the single scroller are tested. No scripts/run-tests.ps1 change is in the branch.

    Outstanding gaps

    • Rapid 99→100→99 changes can leave stale restoration handlers. EntityTreePanel.UpdatePanel registers one LayoutUpdated closure per class change. A closure whose expected panel no longer matches returns without unsubscribing; after a subsequent crossing it can run with its old anchor, offset, expansion and focus and overwrite the current state. There is no cancellation/version guard or test for threshold crossings before layout completes. Browser SetItems can perform multiple collection changes in one update.
    • A focused item that is no longer realized may lose focus when an anchor exists. In EntityTreePanel.Restore, the replacement focused container is looked up among visual descendants, but ScrollIntoView(focusedItem) is only attempted when anchorItem is null. When a different visible anchor exists and the previously focused item was recycled/offscreen, the container stays null and no focus is restored. Cover this branch and preserve the focused object/control.
    • Actual editor-focus coverage remains absent. NestedEditorFocus_SurvivesBothPanelTransitions substitutes a synthetic Button as the item template; the browser integration test substitutes TextBlock. No test exercises the real EntityCardControl field-editor template across a panel transition, where an index-based visual-child path can no longer be assumed to identify the same editor after rematerialization. This is an unverified required focus case, not a claim that the tested synthetic button fails.
    • Desktop smoothness is still an acceptance check, not a code finding. The reported jump was not reproduced/measured in a real desktop browser or primary View; passing headless panel/anchor tests do not demonstrate a smoother wheel-scroll experience. No subjective improvement is claimed. Obtain manual GUI reproduction/acceptance or document why it cannot be performed before marking the issue fully verified.

    Tests

    Approved scripts/run-tests.ps1 -Mode fast -TestNames EntityTreePanelTests,EntityListViewModelTests,BrowserTemplate_ActualItemsCountBinding_SwitchesStyledPanelAtThreshold,PrimaryViewTree_ActualPopulationBinding_CountsCollapsedDescendantsAndViewSwitch from worktree 13; inspected scripts/test-results.log: Executed: 26; Passed: 26; Failed: 0; Skipped: 0. The existing thousands-of-roots bounded-realization test passed. No repository/filesystem layers changed; needs-slow-tests is not applicable. No new disabled tests, unresolved TODOs, timing sleeps, dispatcher-pass pumping, or synchronous task blocking found in the changed code/tests.

  6. JoshuaRowePhantom commented on Oct 8, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification follow-up (rapid transitions and editor focus)

    Commit: 5ba0c429cad3a85b7065eb336533d9be5d89503b (after 50ba6550, b20448fc).

    • A per-tree transition generation cancels the previous LayoutUpdated handler before a new threshold crossing; deferred restoration checks the generation again, so queued callbacks cannot apply stale scroll, expansion or focus snapshots. A 99→100→99 before layout, followed by a different anchor/focus and another crossing, is covered.
    • When the previously focused item is no longer realized, it is scrolled into view and focus is restored even if a different visible scroll anchor exists. In this conflicting case, restoring focus takes precedence over restoring the old viewport anchor. A test focuses an offscreen text input before entering virtualization and verifies the focused item is realized and focused afterward.
    • A real EntityCardControl with a StringFieldEditorViewModel in edit mode is tested across 99↔100 while another editor is inserted ahead of the focused editor. Restoration matches the editor view-model identity and control type before falling back to the visual path; the actual field TextBox retains focus.

    Files: Phantom.Workspaces/Controls/EntityTreePanel.cs, Phantom.Workspaces.Tests/EntityTreePanelTests.cs.

    Validation: full solution non-incremental build passed. Targeted GUI tests 29/29 passed on each of 10 consecutive stability runs, via the existing scripts scoped to the GUI test project; the script was restored and the worktree is clean. Desktop wheel-scroll jump/smoothness remains unmeasured; no smoothness claim is made, and real-desktop manual acceptance remains outstanding.

  7. JoshuaRowePhantom commented on Oct 8, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification result

    Status: ❌ 1 acceptance item outstanding; 0 shippable code/test gaps found.
    Commits checked: 5ba0c429, 50ba6550, b20448fc against features (worktree 13, clean).

    Code and test verification

    • Previously reported code gaps resolved. Per-tree transition generation cancels prior LayoutUpdated handlers and rejects stale deferred callbacks; the rapid 99→100→99→100 regression checks the later anchor/focus. An unrealized focused item is scrolled into view even with a different visible anchor; focus takes precedence when the two positions conflict. A real EntityCardControl with a StringFieldEditorViewModel is exercised while another editor is inserted ahead of the focused field; focus remains on the same field identity across both transitions.
    • Instantiated browser and primary View bindings use the complete population counts (EntityList.NodeCount with collection-change notification; CurrentViewPopulation.Entities.Count, not roots). Styled GUI tests cover 0/1/99/100/1000, collapsed descendants at 100, hidden filtering and View changes. Variable-height insertion/removal ahead of the viewport preserves the visible object's position. Thousands of root items retain bounded realization; default shared-style consumers remain virtualized, with one scroller and the horizontal minimum width preserved. No eager offscreen editor construction was introduced.
    • No source/test-runner changes outside the issue scope; scripts/run-tests.ps1 is unchanged. No new skipped/quarantined, timing-dependent, fixed-dispatcher-pass, unbacked TODO or synchronously blocking test/code found. No Git/filesystem layers changed, so needs-slow-tests does not apply.

    Outstanding acceptance item (not a code/test defect)

    • The original real desktop wheel-scroll jump was never reproduced or measured before/after in the browser and primary View. Headless tests establish panel selection, preservation and bounded realization, not perceived scrolling smoothness. Desktop manual acceptance remains necessary; no claim of measured smoothness is made. This is the sole reason verified-locally is not applied.

    Tests

    Approved scripts/run-tests.ps1 -Mode fast -TestNames EntityTreePanelTests,EntityListViewModelTests,BrowserTemplate_ActualItemsCountBinding_SwitchesStyledPanelAtThreshold,PrimaryViewTree_ActualPopulationBinding_CountsCollapsedDescendantsAndViewSwitch run from worktree 13; inspected fresh scripts/test-results.log: Executed: 29; Passed: 29; Failed: 0; Skipped: 0. No verification label applied while desktop acceptance is outstanding.

  8. added 5 commits that reference this issue on Oct 9, 2026
    b20448f
    50ba655
    5ba0c42
    d08c38b
    49216aa
  9. JoshuaRowePhantom commented on Oct 9, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    The <100-node tree panel and the desktop 100→99 anchor/focus transition fix are now on features via #1626 (merge 1b86a98). A red/green test covers the immediate 100↔99 panel transition; targeted GUI checks and hosted fast (8,019/8,019) and full Windows/Linux CI passed. A Windows desktop harness observed 50px offset steps from injected WM_MOUSEWHEEL messages for 99/100 nodes and bounded realization at 1,100 nodes, but physical wheel input did not reach its test window and the original perceived jump was not reproduced. Leaving open until real physical-wheel scrolling in browser and primary View is accepted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingdiagnosedRoot cause identified

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions