invalidatable: always mark ui as dirty OnInvalidate
The Invalidatable struct is designed so that a widget can have a callback function ran when it is Invalidated. This is used to cascade up the widget tree, marking things as Invalid along the way so that only Invalid widgets are drawn. However, this is only implemented at the grid cell level for checks if the cell is invalidated -- and the grid cells are never set back to a "valid" state. The effect of this is that no matter what is invalidated, the entire UI gets drawn again. The calling through the Invalidate callbacks creates *several* race conditions, as Invalidate is called from several different goroutines, and many widgets call invalidate on their parent or children. Tcell has optimizations to only rerender screen cells that have changed their rune and style. The only performance penalty by redrawing the entire screen for aerc is the operations *within the aerc draw methods*. Most of these are not expensive and have relatively no impact on performance. Skip all of the OnInvalidates, and directly invalidate the UI when DoInvalidate is called by a widget. This reduces data races, and simplifies the widget redraw logic signficantly. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
parent
a49caf96b5
commit
049c72393a
3 changed files with 21 additions and 15 deletions
|
@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
`accounts.conf`.
|
`accounts.conf`.
|
||||||
- Mouse support for embedded editors when `mouse-enabled=true`.
|
- Mouse support for embedded editors when `mouse-enabled=true`.
|
||||||
- Numerous race conditions related to event handling order
|
- Numerous race conditions related to event handling order
|
||||||
|
- Numerous race conditions related to OnInvalidate calls
|
||||||
|
|
||||||
## [0.12.0](https://git.sr.ht/~rjarry/aerc/refs/0.12.0) - 2022-09-01
|
## [0.12.0](https://git.sr.ht/~rjarry/aerc/refs/0.12.0) - 2022-09-01
|
||||||
|
|
||||||
|
|
|
@ -13,12 +13,5 @@ func (i *Invalidatable) OnInvalidate(f func(d Drawable)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Invalidatable) DoInvalidate(d Drawable) {
|
func (i *Invalidatable) DoInvalidate(d Drawable) {
|
||||||
v := i.onInvalidate.Load()
|
atomic.StoreInt32(&dirty, DIRTY)
|
||||||
if v == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f := v.(func(d Drawable))
|
|
||||||
if f != nil {
|
|
||||||
f(d)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
26
lib/ui/ui.go
26
lib/ui/ui.go
|
@ -6,6 +6,11 @@ import (
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DIRTY int32 = iota
|
||||||
|
NOT_DIRTY
|
||||||
|
)
|
||||||
|
|
||||||
var MsgChannel = make(chan AercMsg, 50)
|
var MsgChannel = make(chan AercMsg, 50)
|
||||||
|
|
||||||
// QueueRedraw sends a nil message into the MsgChannel. Nothing will handle this
|
// QueueRedraw sends a nil message into the MsgChannel. Nothing will handle this
|
||||||
|
@ -14,13 +19,23 @@ func QueueRedraw() {
|
||||||
MsgChannel <- nil
|
MsgChannel <- nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dirty is the dirty state of the UI. Any value other than 0 means the UI is in
|
||||||
|
// a dirty state. Dirty should only be accessed via atomic operations to
|
||||||
|
// maintain thread safety
|
||||||
|
var dirty int32
|
||||||
|
|
||||||
|
// Invalidate marks the entire UI as invalid. Invalidate can be called from any
|
||||||
|
// goroutine
|
||||||
|
func Invalidate() {
|
||||||
|
atomic.StoreInt32(&dirty, DIRTY)
|
||||||
|
}
|
||||||
|
|
||||||
type UI struct {
|
type UI struct {
|
||||||
Content DrawableInteractive
|
Content DrawableInteractive
|
||||||
exit atomic.Value // bool
|
exit atomic.Value // bool
|
||||||
ctx *Context
|
ctx *Context
|
||||||
screen tcell.Screen
|
screen tcell.Screen
|
||||||
popover *Popover
|
popover *Popover
|
||||||
invalid int32 // access via atomic
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Initialize(content DrawableInteractive) (*UI, error) {
|
func Initialize(content DrawableInteractive) (*UI, error) {
|
||||||
|
@ -47,10 +62,7 @@ func Initialize(content DrawableInteractive) (*UI, error) {
|
||||||
|
|
||||||
state.exit.Store(false)
|
state.exit.Store(false)
|
||||||
|
|
||||||
state.invalid = 1
|
Invalidate()
|
||||||
content.OnInvalidate(func(_ Drawable) {
|
|
||||||
atomic.StoreInt32(&state.invalid, 1)
|
|
||||||
})
|
|
||||||
if beeper, ok := content.(DrawableInteractiveBeeper); ok {
|
if beeper, ok := content.(DrawableInteractiveBeeper); ok {
|
||||||
beeper.OnBeep(screen.Beep)
|
beeper.OnBeep(screen.Beep)
|
||||||
}
|
}
|
||||||
|
@ -80,8 +92,8 @@ func (state *UI) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *UI) Render() {
|
func (state *UI) Render() {
|
||||||
wasInvalid := atomic.SwapInt32(&state.invalid, 0)
|
dirtyState := atomic.SwapInt32(&dirty, NOT_DIRTY)
|
||||||
if wasInvalid != 0 {
|
if dirtyState == DIRTY {
|
||||||
// reset popover for the next Draw
|
// reset popover for the next Draw
|
||||||
state.popover = nil
|
state.popover = nil
|
||||||
state.Content.Draw(state.ctx)
|
state.Content.Draw(state.ctx)
|
||||||
|
|
Loading…
Reference in a new issue