Skip to content

gui: the open list lays out its own rows, so opening it no longer parses the fonts again - #130

Merged
donislawdev merged 1 commit into
mainfrom
gui/list-rows
Sep 23, 2026
Merged

donislawdev merged 1 commit into
mainfrom
gui/list-rows

Conversation

@donislawdev

@donislawdev donislawdev commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

The last theme override in the window is gone: the open list of formats lays out its own rows instead of drawing widget.List under one.

Fyne 2.8.1 gives an override a new scope at construction, at CreateRenderer and at every Refresh, and parses the fonts again for every scope a new string is drawn in. Every opening of a list was a new override, and every letter the filter makes bold was a new string.

What changed

  • parts/rowview.go: rowView puts one ListRow at every position of the arrangement, one row apart (rowStack), in a container.Scroll. No override.
  • Keyboard scrolling follows widget.List's scrollTo with no gap between rows. RowShowing and DrawnRows answer the rows in sight.
  • listRowHeight measures the text instead of building a label on every call. It is the same number.
  • An emptied filter in a list with nothing chosen kept the rows it had narrowed to. This was already the case on widget.List, and StartOn now redraws either way.

Measured

Real window, guilag -phases walk,menufilter,menu, interleaved: three runs after the change, four before.

before after
10 openings with a new letter in the filter, live heap +158 MB +1.5 MB
font sets at the end 24 2
process at the end 326-327 MB 99-115 MB
window busy per opening with a letter (median) 22-28 ms 6 ms

Pictures

  • Five stored menu screens are pixel for pixel the same. Only their trees changed, because the toolkit's list and listItem are gone.
  • One picture changed: the catalogue's "typed into, with the letters that matched in bold". It used to come up 124 px down, with the keyboard's row (pdf) out of sight. It now comes up at its top, which is what the real window shows after typing.

Guards

  • TestTheRowTheKeyboardIsOnIsAlwaysInSight (new). It was written before the change and is green on widget.List.
  • TestEmptyingTheFilterOfAListWithNothingChosenDrawsEveryRowAgain (new). It is red on main too.
  • TestNoScreenStandsInAThemeOverride now walks what the open list and the catalogue draw.
  • Mutation: 7 of 7 entries caught.

Also checked in the real window at 1.5 scale: open, wheel, hover, filter, arrows and Esc.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Reduced memory growth when opening or filtering format lists. In testing, ten list openings used about 160 MB, and memory after visiting four tabs fell from about 290 MB to 120 MB.
    • Improved format-list behavior: keyboard navigation keeps the active row visible, and clearing a filter after no matches restores the available rows.
    • The screen’s appearance is unchanged.

…ses the fonts again

The rows of an open list were widget.List's under a theme override that
took out the room between them. Fyne 2.8.1 gives an override a new scope
at construction, at CreateRenderer and at every Refresh, and parses the
fonts again for every scope a new string is drawn in - every opening was
a new override, and every letter a filter made bold a new string.

rowView puts one ListRow at every position of the arrangement, one row
apart (rowStack), in a container.Scroll, with no override. Keyboard
scrolling follows widget.List's scrollTo with no gap between rows.
RowShowing and DrawnRows answer the rows in sight. listRowHeight measures
the text instead of building a label on every call (the same number).

Measured in the real window, interleaved, three runs after and four before:
- ten openings with a new letter in the filter: live heap +158 MB -> +1.5 MB,
  font sets 24 -> 2, process at the end 326 MB -> 100-115 MB;
- the window busy per opening with a letter: 22-28 ms -> 6 ms.

Five stored menu screens are pixel for pixel the same, their trees without
the toolkit's list. One picture changed: the catalogue's filtered state came
up 124 px down with the keyboard's row out of sight, and now comes up at its
top, as typing puts a list on the screen.

An emptied filter in a list with nothing chosen kept the rows it had
narrowed to - on widget.List as well. StartOn now draws either way.

New guards: TestTheRowTheKeyboardIsOnIsAlwaysInSight (written before the
change, green on widget.List) and
TestEmptyingTheFilterOfAListWithNothingChosenDrawsEveryRowAgain.
TestNoScreenStandsInAThemeOverride now walks what the open list and the
catalogue draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 1f0dac8b-00ab-4eac-ad91-16d3159e520d

📥 Commits

Reviewing files that changed from the base of the PR and between 6609821 and 366053c.

⛔ Files ignored due to path filters (1)
  • internal/guard/testdata/screens/catalogue.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (14)
  • CHANGELOG.md
  • internal/guard/formatlist_test.go
  • internal/guard/testdata/screens/catalogue.xml
  • internal/guard/testdata/screens/generate-menu-hovered.xml
  • internal/guard/testdata/screens/generate-menu-keyed.xml
  • internal/guard/testdata/screens/generate-menu.xml
  • internal/guard/testdata/screens/preset-menu-setting.xml
  • internal/guard/testdata/screens/preset-menu.xml
  • internal/guard/themescope_test.go
  • internal/gui/catalogue/lists.go
  • internal/gui/parts/listcontents.go
  • internal/gui/parts/listrow.go
  • internal/gui/parts/openlist.go
  • internal/gui/parts/rowview.go

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (14)
  • GitHub Check: test on macos-latest
  • GitHub Check: reference tools actually installed
  • GitHub Check: bill of materials
  • GitHub Check: known vulnerabilities
  • GitHub Check: coverage gate
  • GitHub Check: test on ubuntu-latest
  • GitHub Check: test on windows-latest
  • GitHub Check: semgrep
  • GitHub Check: import table of the window binary
  • GitHub Check: linters
  • GitHub Check: staticcheck
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (actions)
🧰 Additional context used
📓 Path-based instructions (10)
Applies to text shown to the user (labels, buttons, tooltips, placeholders, dialogs, errors, status messages, empty states, translations).

⚙️ CodeRabbit configuration file

Files:

  • internal/gui/catalogue/lists.go
  • internal/guard/formatlist_test.go
  • internal/gui/parts/listrow.go
  • internal/guard/themescope_test.go
  • internal/gui/parts/listcontents.go
  • internal/gui/parts/rowview.go
  • internal/gui/parts/openlist.go
Verify tests check real behavior and would fail if the implementation were broken.

⚙️ CodeRabbit configuration file

Files:

  • internal/guard/formatlist_test.go
  • internal/guard/themescope_test.go
Performance is a known weak spot of these projects.

⚙️ CodeRabbit configuration file

Files:

  • internal/gui/catalogue/lists.go
  • internal/guard/formatlist_test.go
  • internal/gui/parts/listrow.go
  • internal/guard/themescope_test.go
  • internal/gui/parts/listcontents.go
  • internal/gui/parts/rowview.go
  • internal/gui/parts/openlist.go
Applies only to code that builds or styles a GUI.

⚙️ CodeRabbit configuration file

Files:

  • internal/gui/catalogue/lists.go
  • internal/guard/formatlist_test.go
  • internal/gui/parts/listrow.go
  • internal/guard/themescope_test.go
  • internal/gui/parts/listcontents.go
  • internal/gui/parts/rowview.go
  • internal/gui/parts/openlist.go
User-facing changelog.

⚙️ CodeRabbit configuration file

Files:

  • CHANGELOG.md
SECURITY, HIGH PRIORITY.

⚙️ CodeRabbit configuration file

Files:

  • internal/gui/catalogue/lists.go
  • internal/guard/formatlist_test.go
  • internal/gui/parts/listrow.go
  • internal/guard/themescope_test.go
  • internal/gui/parts/listcontents.go
  • internal/gui/parts/rowview.go
  • internal/gui/parts/openlist.go
Go code.

⚙️ CodeRabbit configuration file

Files:

  • internal/gui/catalogue/lists.go
  • internal/guard/formatlist_test.go
  • internal/gui/parts/listrow.go
  • internal/guard/themescope_test.go
  • internal/gui/parts/listcontents.go
  • internal/gui/parts/rowview.go
  • internal/gui/parts/openlist.go
Check that documentation matches the actual code in this PR: commands, flags, config keys, file paths, build steps and examples must exist.

⚙️ CodeRabbit configuration file

Files:

  • CHANGELOG.md
All code in this repository is written by an AI coding agent (Claude Code).

⚙️ CodeRabbit configuration file

Files:

  • internal/gui/catalogue/lists.go
  • CHANGELOG.md
  • internal/guard/testdata/screens/preset-menu.xml
  • internal/guard/testdata/screens/generate-menu-hovered.xml
  • internal/guard/testdata/screens/generate-menu.xml
  • internal/guard/testdata/screens/generate-menu-keyed.xml
  • internal/guard/formatlist_test.go
  • internal/guard/testdata/screens/preset-menu-setting.xml
  • internal/gui/parts/listrow.go
  • internal/guard/themescope_test.go
  • internal/gui/parts/listcontents.go
  • internal/gui/parts/rowview.go
  • internal/guard/testdata/screens/catalogue.xml
  • internal/gui/parts/openlist.go
Source excerpt: **Words a user reads are English, with a flat hyphen and no semicolons.**

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Files:

  • CHANGELOG.md
🔇 Additional comments (14)
internal/gui/parts/listcontents.go (1)

5-5: LGTM!

Also applies to: 32-46, 75-94

internal/gui/parts/listrow.go (1)

20-25: LGTM!

internal/gui/catalogue/lists.go (1)

23-28: LGTM!

internal/gui/parts/openlist.go (1)

77-77: LGTM!

Also applies to: 120-120, 131-139, 202-202, 237-262, 364-367, 381-387

internal/guard/formatlist_test.go (1)

238-341: LGTM!

internal/guard/testdata/screens/catalogue.xml (1)

880-893: LGTM!

Also applies to: 915-927, 949-1008, 1028-1089, 1111-1120, 1143-1304, 1342-1444, 1479-1483

internal/guard/testdata/screens/generate-menu-hovered.xml (1)

492-653: LGTM!

internal/guard/testdata/screens/generate-menu-keyed.xml (1)

492-653: LGTM!

internal/guard/testdata/screens/generate-menu.xml (1)

492-653: LGTM!

internal/guard/testdata/screens/preset-menu-setting.xml (1)

455-616: LGTM!

internal/guard/testdata/screens/preset-menu.xml (1)

398-419: LGTM!

internal/guard/themescope_test.go (1)

27-32: LGTM!

Also applies to: 56-101

CHANGELOG.md (1)

478-486: LGTM!

internal/gui/parts/rowview.go (1)

75-78: 🎯 Functional Correctness

The concern is refuted. pop.Resize(...) runs before StartOn(...). In Fyne v2.8.1, BaseWidget.Resize synchronously invokes the renderer layout. The popup renderer then resizes its content, including the rowView.scroll, before StartOn runs. Therefore, the initial bringIntoView call does not encounter a zero-height scroll on this opening path, and the proposed pending-position change is not required.


📝 Walkthrough

Walkthrough

Open format lists now use a custom scrollable row view instead of a toolkit list. Filtering, row visibility, and keyboard navigation use the new view. Tests and screen fixtures cover the updated list behavior.

Changes

Format-list rendering

Layer / File(s) Summary
Row view and row tracking
internal/gui/parts/rowview.go, internal/gui/parts/listcontents.go, internal/gui/parts/listrow.go, internal/gui/catalogue/lists.go
The new row view retains rows by list position and lays them out at a fixed height. listContents reports built rows that are in sight.
OpenList rendering and navigation
internal/gui/parts/openlist.go
OpenList uses the row view for filtering, rendering, sizing, and keyboard navigation. Row text height uses fyne.MeasureText.
List behavior tests and screen fixtures
internal/guard/formatlist_test.go, internal/guard/themescope_test.go, internal/guard/testdata/screens/*.xml, CHANGELOG.md
Tests cover row visibility, filter clearing, and drawn-content theme traversal. Screen fixtures use direct ListRow children inside scroll containers. The changelog reports memory measurements.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bug, performance, ui

Merge Risk: ⚪ Minimal · up to 36605

The format list now draws its rows with a lighter custom view. Scrolling, filtering and keyboard navigation keep their behavior, and the stored screens are unchanged. No outstanding issues were found, and the change looks ready to merge.

🚥 Pre-merge checks | ✅ 12 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
No Obvious Performance Problems ⚠️ Warning The PR introduces a non-virtualized list. rowView.show creates one ListRow for every entry, builds an objects slice for all entries, fills every row, and refreshes the scroll. rowStack.Layout … Keep viewport virtualization for OpenList. Use a visible-row pool or retain widget.List with the required row layout and theme behavior. On filtering or keyboard movement, update only rows in or near the viewport instead of allocating, …
No Resource Leaks ⚠️ Warning The new rowView retains one *ListRow for every entry, not only visible rows. rowView.show appends to built and never removes those rows. Each ListRow renderer creates a confirm `canvas.Image… Use a bounded visible-row pool, such as Fyne's list pooling model, instead of allocating one *ListRow per list position. Refill rows as the scroll position changes. When a popup closes, remove unused rows from the scroll container, clear …
✅ Passed checks (12 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main code change and its user-visible effect: the open list lays out its own rows and avoids repeated font parsing. It is specific, relevant, and 88 characters long.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Tests For Changed Behavior ✅ Passed The PR changes runtime behavior in OpenList and rowView, including filtering redraws, keyboard scrolling, row visibility, and row layout. It adds two focused tests in internal/guard/formatlist_test.go…
No Secrets Or Debug Leftovers ✅ Passed The diff adds no CLAUDE.md, AGENTS.md, .claude/, or .env paths. Added-line scans found no credentials, tokens, API keys, private URLs, absolute local paths, personal e-mails, or debug output. Structur…
No Hardcoded Ui Styling ✅ Passed The PR changes Fyne UI code, but the new layout does not introduce hardcoded visual styling. rowView uses listRowHeight(), RowWidthFor, available container width, and shared token values. `OpenL…
Desktop Robustness ✅ Passed PASS: The pull request changes only format-list rendering, layout, tests, screen fixtures, and changelog text. The changed production imports are limited to Fyne UI packages plus math and strings.…
Safe File Parsing ✅ Passed The PR does not add file parsing or file I/O. Changed Go files only update GUI layout, scrolling, and tests; they add no XML/YAML/JSON/CSV/XLSX decoder, path handling, archive handling, or deserializa…
System Changes Are Reversible ✅ Passed PASS: The PR changes GUI list rendering, scrolling, tests, and screen fixtures only. The authoritative diff contains no network filter, proxy, firewall, system-time, process-hooking, service, registry…
Clear User-Facing Text ✅ Passed The PR changes list layout and scrolling, not user-facing wording. XML fixture comparison found no added or removed text-node values, and the changed Go code adds no UI labels, tooltips, errors, or bu…
Scope, Duplication And Docs ✅ Passed The PR stays within the stated open-format-list performance and behavior fix. The implementation changes OpenList and its private listContents state, adds the private rowView/rowStack needed t…
Full details: No Obvious Performance Problems

Explanation

The PR introduces a non-virtualized list. rowView.show creates one ListRow for every entry, builds an objects slice for all entries, fills every row, and refreshes the scroll. rowStack.Layout also iterates over every row. OpenList accepts arbitrary option slices, while the base implementation used widget.List to create only visible rows. The PR itself notes that hundreds of values exceed this design, but it adds no bound or fallback. Filtering and keyboard movement therefore process the full list on the UI thread.

Resolution

Keep viewport virtualization for OpenList. Use a visible-row pool or retain widget.List with the required row layout and theme behavior. On filtering or keyboard movement, update only rows in or near the viewport instead of allocating, filling, and laying out every entry.

Full details: No Resource Leaks

Explanation

The new rowView retains one *ListRow for every entry, not only visible rows. rowView.show appends to built and never removes those rows. Each ListRow renderer creates a confirm canvas.Image and a kind canvas.Image, plus text objects and an onTap closure. The old base implementation used Fyne's visible-row pool. Therefore a large list keeps off-screen row renderers and image objects unnecessarily, and repeated popup use increases resource pressure. No timers or subscriptions were added.

Resolution

Use a bounded visible-row pool, such as Fyne's list pooling model, instead of allocating one *ListRow per list position. Refill rows as the scroll position changes. When a popup closes, remove unused rows from the scroll container, clear each row's onTap callback, and release renderer-owned image and text objects through the renderer cleanup path.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added bug Something isn't working performance ui labels Sep 23, 2026
@donislawdev
donislawdev merged commit 6e6655e into main Sep 23, 2026
20 checks passed
@donislawdev
donislawdev deleted the gui/list-rows branch September 23, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working performance ui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant