Skip to content

Fix table scrolling and centering, and build only visible rows - #6

Merged
greenhat616 merged 10 commits into
mainfrom
perf/table-virtualization
Oct 22, 2026
Merged

greenhat616 merged 10 commits into
mainfrom
perf/table-virtualization

Conversation

@greenhat616

Copy link
Copy Markdown
Contributor

Summary

  • Center the window on the monitor under the cursor, and start the Binaries column at 200 DIP so it is resizable.
  • Scroll the table inside its ListView, so the vertical scroll bar stays at the card edge in a narrow window and a horizontal wheel scrolls horizontally. The header follows the horizontal offset.
  • Let rows follow a column resize while dragging (at most once per 16 ms), and keep the scroll position when refresh, search or sort replaces the rows.
  • Build only the rows near the list's viewport (ListView::virtual_source in the windows-rs fork) and create cell tooltips only for the hovered row. Startup private working set with ~280 rows: 107 MB → 71 MB.
  • Generate CHANGELOG.md and the release notes with git-cliff.

Fork changes: libnyanpasu/windows-rs nyanpasu (patches tracked in #2). The startup black flash is tracked in #5.

Test plan

  • cargo test -p enable-loopback (53), clippy -D warnings, fmt
  • Reactor tests, native ListView tests, public API
  • Memory measured with and without rows; tooltips, scrolling, search, refresh, checkboxes and keyboard checked by hand

The fork adds two-axis ListView scrolling with offset events and scroll
requests, which the table uses next. It also wraps text tooltips the way
WinUI wraps string content, so a cell's tooltip no longer clips values wider
than the tooltip.
The window opened at the system's cascading default position. It now opens
centered in the work area of the monitor under the cursor. The client size
is converted to that monitor's DPI and framed with AdjustWindowRectExForDpi.
Binaries started at its 48 DIP minimum and only grew to fill spare width. In
a window narrower than the columns it stayed at the minimum and showed a
single character of its header.
The header and the list sat in an outer horizontal ScrollViewer, and the list
scrolled vertically inside it. In a narrow window the list's vertical scroll
bar was at the right edge of the full content width, out of view. A
horizontal wheel reached the inner list first, which scrolled vertically.

The list now scrolls both ways itself, so its vertical scroll bar stays at
the card's edge and a horizontal wheel scrolls horizontally. The header moves
by the list's horizontal offset inside a non-scrolling ScrollViewer that
clips it, with real layout, so header hit testing stays correct.
Rows kept their widths until the drag ended, so the list did not move with
the header. Re-laying out every row costs about two header updates, so rows
now follow at most once per 16 ms during a drag and settle on release.
Refreshing, searching and sorting replace the ListView, so the list jumped
back to the start. The new list now restores the horizontal offset and
keeps an anchor row where it was on screen. The anchor is the focused row
if it survives; otherwise it is the first surviving row from the top of the
old view. A refresh empties the rows before reloading them, so the rows and
offsets from before that are held until the new rows arrive.

Widening Binaries while scrolled to the right end kept the growth out of
view, so the column looked unresizable. That drag now scrolls along to keep
the end and the grip in view.
The fork adds ListView::virtual_source, which builds an item's view only
while the list has a container prepared for it, and a test-support count of
its items.
Every row's element tree was built up front, although the list realizes only
the containers near its viewport. With about 280 rows they held some 58 MB of
the 107 MB private working set at startup. The table now declares its rows
through a virtual source keyed by the list generation, so only the rows the
list prepares are built: about 48, for 73 MB.

Each row names itself after its display name, which the list copies to its
item, so assistive technology still reads app names instead of indices.
Recording tests stand in for the native list and prepare every row.
Every built row attached a tooltip to each of its seven cells, about 2.5 MB
across the rows the list keeps prepared. Only the row under the pointer
needs them, so a row now attaches them while it is hovered.
Releases had no notes, and the project kept no changelog. cliff.toml lists
each version's commit subjects with links. Subjects are imperative sentences
rather than Conventional Commits, so they are not grouped; release, merge
and fork bump commits are left out. The release workflow writes the tagged
version's section as the release notes, and the README describes the
release steps that keep CHANGELOG.md current.
@semanticdiff-com

semanticdiff-com Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  crates/enable-loopback/src/ui/mod.rs  50% smaller
  crates/enable-loopback/src/ui/table/row.rs  49% smaller
  crates/enable-loopback/src/ui/table/mod.rs  21% smaller
  crates/enable-loopback/src/ui/tests.rs  11% smaller
  crates/enable-loopback/src/ui/native.rs  2% smaller
  .github/workflows/release.yml  0% smaller
  CHANGELOG.md Unsupported file format
  README.md Unsupported file format
  cliff.toml Unsupported file format
  crates/enable-loopback/Cargo.toml Unsupported file format
  crates/enable-loopback/src/model/columns.rs  0% smaller
  docs/manual-smoke.md Unsupported file format
  vendor/windows-rs  0% smaller

@greenhat616
greenhat616 merged commit 19d9063 into main Oct 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant