diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 08fd8c0b3..165d15ba2 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -1598,10 +1598,10 @@ func (g *Gui) flush() error { return nil } -// Redraws only tainted views and skips the layout pass. +// Redraws only dirty views and skips the layout pass. // tcell's cell-level dirty tracking ensures only // actually-changed cells are emitted to the terminal. -// Will also redraw any views that overlap tainted views +// Will also redraw any views that overlap dirty views. func (g *Gui) flushContentOnly(views []*View) { // The screen must not be touched while suspended (see Suspend). if g.isSuspended() { @@ -1619,7 +1619,7 @@ func viewsToRedrawContentOnly(views []*View) []*View { redrawIndexes := set.New[int]() for i, v := range views { - if !v.IsTainted() && !redrawIndexes.Includes(i) { + if !v.NeedsRedraw() && !redrawIndexes.Includes(i) { continue } @@ -1649,7 +1649,7 @@ func (g *Gui) ForceLayoutAndRedraw() error { return g.flush() } -// Redraws only tainted views outside of the normal main +// Redraws only dirty views outside of the normal main // loop, without a layout pass. Useful during longer operations that block the // main thread, e.g. to update a spinner in a status view. func (g *Gui) ForceFlushViewsContentOnly(views []*View) { diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 463a31a8f..fa5eb934d 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -86,6 +86,11 @@ type View struct { // tained is true if the viewLines must be updated tainted bool + // needsRedraw is true if the view's current state has not been drawn to the + // screen yet. A tainted view always needs a redraw, but draw-only state can + // require one without invalidating viewLines. + needsRedraw bool + // firstDirtyLine is the index of the lowest line in `lines` that has been // written to since viewLines was last refreshed, and whose cached wrapping // (lineType.wrappedCells) may therefore be stale. Lines below it are @@ -274,11 +279,18 @@ type pos struct { // a view whose size has changed, whose content is the same but has to be wrapped // afresh, call RewrapContent instead. func (v *View) clearViewLines() { - v.tainted = true + v.markViewLinesDirty() v.viewLines = nil v.clearHover() } +// markViewLinesDirty records that the cached viewLines no longer represent the +// view's buffer or wrapping, so both rebuilding and redrawing are required. +func (v *View) markViewLinesDirty() { + v.tainted = true + v.needsRedraw = true +} + // RewrapContent wraps the view's content for the size the view has now, and puts // the positions into that content — the scroll offset, the cursor, a range's // anchor — back on the lines they were on. They are all view lines, which count @@ -793,6 +805,7 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View { Frame: true, Editor: DefaultEditor, tainted: true, + needsRedraw: true, outMode: mode, buf: &viewBuffer{ei: newEscapeInterpreter(mode)}, searcher: &searcher{}, @@ -1177,7 +1190,7 @@ func (v *View) write(p []byte) { return } - v.tainted = true + v.markViewLinesDirty() // write only ever touches lines from v.buf.wy onwards, so any cached wrapping // below that stays valid. v.firstDirtyLine = min(v.firstDirtyLine, v.buf.wy) @@ -1612,7 +1625,7 @@ func (v *View) SwapInOffscreenRender() { } v.buf = v.offscreen v.offscreen = nil - v.tainted = true + v.markViewLinesDirty() v.clearHover() } @@ -1776,6 +1789,12 @@ func (v *View) IsTainted() bool { return v.tainted } +func (v *View) NeedsRedraw() bool { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + return v.needsRedraw +} + // draw re-draws the view's contents. func (v *View) draw(isWindowFocused bool) { v.writeMutex.Lock() @@ -1784,6 +1803,7 @@ func (v *View) draw(isWindowFocused bool) { if !v.Visible { return } + defer func() { v.needsRedraw = false }() v.clearRunes()