Posters wait while the selection moves quickly

On a terminal zoomed far out a poster is tens of thousands of cells, and
redrawing it on every step of a held arrow key was more output than the
terminal could draw (about 100 KB per step at 600x180), so the list
lagged. A selection that changes again within 120 ms keeps the poster's
place empty and shows the poster once it rests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
This commit is contained in:
2026-10-21 11:31:51 +02:00
co-authored by Claude Opus 5.5
parent 080ef9ec8e
commit a71ca6e3fb
3 changed files with 72 additions and 3 deletions
+39 -3
View File
@@ -3,6 +3,7 @@ package ui
import (
"image"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
@@ -14,6 +15,12 @@ const (
minCoverRows = 6 // a smaller poster is not worth showing
minCoverCols = 8
minTextRows = 6 // the poster never takes these rows from the text
// posterSettle: when the selection moves again within this time (a held
// arrow key), the poster waits until the selection rests that long. Every
// poster rewrites all of its cells, which on a huge terminal (zoomed far
// out) is more output than the terminal can draw at key-repeat speed.
posterSettle = 120 * time.Millisecond
)
// detailPane shows the details of one title as one flowing text with the
@@ -27,9 +34,11 @@ type detailPane struct {
cover *coverView
detail detailText
lastRef coverRef // the poster of the current detail, to fetch it again after a style change
want string // key of the poster that should be shown
hasCover bool // a poster (or its loading note) is part of the content
lastRef coverRef // the poster of the current detail, to fetch it again after a style change
want string // key of the poster that should be shown
hasCover bool // a poster (or its loading note) is part of the content
lastShow time.Time // when the shown title last changed
settle *time.Timer // shows the poster once the selection rests
// What the text was last wrapped for.
laidOut struct {
@@ -61,7 +70,34 @@ func (d *detailPane) Show(text detailText, ref coverRef) {
d.cover.setImage(nil)
return
}
now := time.Now()
moving := ref.key != d.want && now.Sub(d.lastShow) < posterSettle
if ref.key != d.want {
d.lastShow = now
}
d.want, d.hasCover = ref.key, true
if d.settle != nil {
d.settle.Stop()
}
if moving {
// Keep the poster's place (so the text does not reflow) but leave it
// empty until the selection stops.
d.cover.setNote("")
want := ref.key
d.settle = time.AfterFunc(posterSettle, func() {
d.app.post(func() {
if d.want == want {
d.loadPoster(ref)
}
})
})
return
}
d.loadPoster(ref)
}
// loadPoster shows the poster of ref, fetching it first if needed.
func (d *detailPane) loadPoster(ref coverRef) {
d.cover.setNote("loading cover…")
want := ref.key
d.app.loadCover(ref, func(img *image.RGBA, err error) {