Merge claudedo/3511e9d2d7b44a9aa018739c83143b6f
This commit is contained in:
@@ -20,6 +20,7 @@ These sit **between** the CLAUDE.md files and the code:
|
||||
| [review-merge](review-merge.md) | Approve=merge-unit, verify gate, `MergeCommit`/revert, diff stack, conflict resolver |
|
||||
| [conpty-sessions](conpty-sessions.md) | Interactive/planning/list-handler launch specs + the arg-flattening gotcha |
|
||||
| [installer-preflight](installer-preflight.md) | CLI version/login/auto-mode research, the `ExecutableResolver`/shim root cause, and the Installer's `Checks/`+`SystemCheckPage` implementation status |
|
||||
| [list-virtualization-spike](list-virtualization-spike.md) | Phase 2a gate spike: virtualized `ListBox` + the existing ghost-drag `InputHitTest` model, verified headless |
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# List virtualization spike (Phase 2a gate)
|
||||
|
||||
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||
> Last verified against commit `6a2a19c` (2026-08-10).
|
||||
> Drift check: `git log --oneline 6a2a19c..HEAD -- src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs src/ClaudeDo.Ui/Views/Controls/TaskDragController.cs`
|
||||
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
|
||||
|
||||
Gate task for [ui-reaktivitaet-und-listen-performance-design](../superpowers/specs/2026-08-07-ui-reaktivitaet-und-listen-performance-design.md)
|
||||
Phase 2, Risk 1. Answers the five open questions before any rework of the real
|
||||
`TasksIslandView`/`TasksIslandViewModel`. No production code changed for this spike.
|
||||
|
||||
## Method
|
||||
|
||||
A throwaway Avalonia-headless xUnit test (`Avalonia.Headless` + `Avalonia.Themes.Fluent`,
|
||||
temporarily added to `ClaudeDo.Ui.Tests`, removed again after this report was written — not
|
||||
committed) built a plain `ListBox` with an explicit `VirtualizingStackPanel` and 1000 mixed
|
||||
items (≈14% "complex" two-line/badge rows, mirroring the 68px/90–110px split from the design
|
||||
doc), then drove it programmatically: forced layout/render passes, scrolled the `ScrollViewer`,
|
||||
called the same `TopLevel.InputHitTest` the production drag code uses, and counted realized
|
||||
`ListBoxItem` containers via the visual tree. No screenshots — every number below came from an
|
||||
actual headless run of that harness on 2026-08-10.
|
||||
|
||||
One harness-only wrinkle: `InputHitTest` walks the **compositor's rendered scene**, not just the
|
||||
layout tree, so the test had to call `window.CaptureRenderedFrame()` after each layout-affecting
|
||||
change before hit-testing. A real windowed app renders every frame continuously, so this isn't a
|
||||
production concern — noted here only so the method is reproducible.
|
||||
|
||||
## Q1 — Does a virtualized `ListBox` actually bound the number of realized containers at 1000 items?
|
||||
|
||||
**Geht.** Realized `ListBoxItem` count stayed at 22–23 across the whole scroll range (top, 25%,
|
||||
50%, 75%, bottom) with 1000 source items in the list:
|
||||
|
||||
```
|
||||
realized containers @ top: 22
|
||||
realized containers @ 25% scroll: 22
|
||||
realized containers @ 50% scroll: 22
|
||||
realized containers @ 75% scroll: 23
|
||||
realized containers @ 100% scroll: 22
|
||||
```
|
||||
|
||||
Matches the design doc's estimate (~19 visible + overscan ≈ 22–25) almost exactly. The
|
||||
`ListBox`/`VirtualizingStackPanel` combination genuinely recycles containers instead of building
|
||||
one per item.
|
||||
|
||||
## Q2 — Does the existing ghost-drag `InputHitTest` model survive container recycling?
|
||||
|
||||
**Geht.** Scrolled from top to 60% (forcing recycling), then called `TopLevel.InputHitTest` at the
|
||||
*same screen point* both times — exactly what `RowButtonAt`/`ListItemUnder` in
|
||||
`TasksIslandView.axaml.cs` do live during a drag:
|
||||
|
||||
```
|
||||
hit-test @ top, screen point (450,600): Task[8]
|
||||
hit-test @ 60% scroll, same screen point: Task[577]
|
||||
expected DataContext at that point (geometry): Task[577]
|
||||
```
|
||||
|
||||
The hit-test result matches the geometrically-correct row after recycling, and correctly changed
|
||||
(it did *not* keep returning the stale top-of-list row). The production pattern —
|
||||
`topLevel.InputHitTest(pt)` then walk ancestors to the row `Button`/`ListBoxItem` — is
|
||||
recycling-safe: it's a live geometric query against whatever is currently realized, with no
|
||||
per-row state to go stale.
|
||||
|
||||
## Q3 — Variable row heights (68px vs 90–110px): does the scrollbar/`Extent` estimate drift?
|
||||
|
||||
**Geht, mit einer Einschränkung.** Measured actual template heights in this harness: header
|
||||
57px, simple task row 53px, complex (two-line + badges) row 90px. Hand-summing all 1000 rows by
|
||||
their real measured height gives 58275px; `VirtualizingStackPanel`'s reported `Extent.Height` was
|
||||
58228px — **0.1% error**. Avalonia's own extent bookkeeping for variable-height virtualized rows
|
||||
is not the "gross estimate that drifts" risk the design doc worried about.
|
||||
|
||||
The real risk is a **hand-rolled** pixel↔index conversion. Scrolling to `index * 68px` (a naive
|
||||
"assume every row is 68px" offset) while targeting logical row #700 actually landed on row
|
||||
**#790** — a 90-row drift, because ~14% of the preceding rows were taller. Anything that
|
||||
computes a scroll offset from `index * fixedRowHeight` (a natural shortcut for a "scroll to
|
||||
selected row" or a custom auto-scroll speed curve) will misfire. The existing
|
||||
`ScrollSelectedIntoView` in `TasksIslandView.axaml.cs` already avoids this trap — it finds the
|
||||
realized control by `DataContext` and calls `BringIntoView()`, not index-times-height math. Phase
|
||||
2b must keep using `ListBox.ScrollIntoView(item)` / `Control.BringIntoView()`, never index
|
||||
arithmetic, for any "jump to row" behavior.
|
||||
|
||||
## Q4 — Is auto-scroll-while-dragging feasible with the existing central-pointer-handler model?
|
||||
|
||||
**Geht.** Auto-scroll doesn't exist today and has to be built, but the mechanism it needs — the
|
||||
same UI-thread pointer-moved handler (or a `DispatcherTimer` it starts) mutating
|
||||
`ScrollViewer.Offset` directly while a drag is in progress — was exercised directly: nudging
|
||||
`Offset` by 400px in one synchronous step kept the realized-container count constant (22 before,
|
||||
22 after) and an immediate subsequent `InputHitTest` at the same screen point resolved correctly
|
||||
to the new row under the cursor (`Task[388]`), with no async delay or stale state. Since
|
||||
`TasksIslandView`'s drag handlers already run on the UI thread via tunnelled pointer events, a
|
||||
`DispatcherTimer` (or a direct `Offset` nudge inside `OnPointerMovedDrag` when the cursor is near
|
||||
the list's top/bottom edge) has no threading obstacle. This still needs to be built in Phase 2b;
|
||||
this spike only confirms the approach isn't blocked by virtualization/recycling.
|
||||
|
||||
## Q5 — Mixed item types (header rows vs. task rows) via a template selector, under virtualization
|
||||
|
||||
**Geht.** Used `ListBox.DataTemplates` with two `FuncDataTemplate<T>` entries keyed by type
|
||||
(`HeaderRow` / `TaskRow`) instead of an explicit `IDataTemplate.Match` selector — Avalonia's
|
||||
implicit per-type template lookup is the idiomatic equivalent and needs no selector class. Both
|
||||
types showed up among the realized containers throughout scrolling
|
||||
(`Q5 distinct realized DataContext types: HeaderRow, TaskRow`), confirming the design's flat
|
||||
`Rows` (`HeaderRow | TaskRowViewModel`) collection will virtualize and template correctly.
|
||||
|
||||
## Recommendation for Phase 2b
|
||||
|
||||
**Geht — proceed**, with two carry-overs into the implementation:
|
||||
|
||||
1. Any "scroll to row" logic (selection scroll, auto-scroll speed/targeting) must go through
|
||||
`ScrollIntoView`/`BringIntoView` against the realized control, never `index * rowHeight` math
|
||||
(Q3).
|
||||
2. Auto-scroll is new work, not a retrofit risk — build it as an `Offset` nudge from the existing
|
||||
UI-thread drag handlers (Q4).
|
||||
|
||||
No evidence surfaced against the Phase 2 design as written. The `ITaskListFilter` risk (#3 in the
|
||||
design doc) is unrelated to virtualization and out of this spike's scope.
|
||||
Reference in New Issue
Block a user