aerc/lib/ui/grid.go
Simon Ser 5685a17674 lib/ui: introduce Invalidatable
Many Drawable implementations have their own Invalidate and OnInvalidate
functions, with an unexported onInvalidate field. However OnInvalidate and
Invalidate are usually not called in the same goroutine. This results in a race
on this field, e.g.:

    Read at 0x00c000094748 by goroutine 7:
      git.sr.ht/~sircmpwn/aerc2/widgets.NewDirectoryList.func1()
          /home/simon/src/aerc2/widgets/dirlist.go:85 +0x56
      git.sr.ht/~sircmpwn/aerc2/widgets.(*Spinner).Start.func1()
          /home/simon/src/aerc2/widgets/spinner.go:93 +0x1bb

    Previous write at 0x00c000094748 by main goroutine:
      [failed to restore the stack]

    Goroutine 7 (running) created at:
      git.sr.ht/~sircmpwn/aerc2/widgets.(*Spinner).Start()
          /home/simon/src/aerc2/widgets/spinner.go:46 +0x8f
      git.sr.ht/~sircmpwn/aerc2/widgets.NewDirectoryList()
          /home/simon/src/aerc2/widgets/dirlist.go:37 +0x286
      git.sr.ht/~sircmpwn/aerc2/widgets.NewAccountView()
          /home/simon/src/aerc2/widgets/account.go:50 +0x5ca
      git.sr.ht/~sircmpwn/aerc2/widgets.NewAerc()
          /home/simon/src/aerc2/widgets/aerc.go:60 +0x800
      main.main()
          /home/simon/src/aerc2/aerc.go:65 +0x33e

To fix this, introduce a new type, Invalidatable, which protects the field.
Unfortunately the Drawable must be passed to the callback function in
Invalidate, so we still need to re-implement this in each Invalidatable user.
2019-04-27 14:30:28 -04:00

192 lines
3.9 KiB
Go

package ui
import (
"fmt"
"math"
)
type Grid struct {
Invalidatable
rows []GridSpec
rowLayout []gridLayout
columns []GridSpec
columnLayout []gridLayout
cells []*GridCell
invalid bool
}
const (
SIZE_EXACT = iota
SIZE_WEIGHT = iota
)
// Specifies the layout of a single row or column
type GridSpec struct {
// One of SIZE_EXACT or SIZE_WEIGHT
Strategy int
// If Strategy = SIZE_EXACT, this is the number of cells this row/col shall
// occupy. If SIZE_WEIGHT, the space left after all exact rows/cols are
// measured is distributed amonst the remainder weighted by this value.
Size int
}
// Used to cache layout of each row/column
type gridLayout struct {
Offset int
Size int
}
type GridCell struct {
Row int
Column int
RowSpan int
ColSpan int
Content Drawable
invalid bool
}
func NewGrid() *Grid {
return &Grid{invalid: true}
}
func (cell *GridCell) At(row, col int) *GridCell {
cell.Row = row
cell.Column = col
return cell
}
func (cell *GridCell) Span(rows, cols int) *GridCell {
cell.RowSpan = rows
cell.ColSpan = cols
return cell
}
func (grid *Grid) Rows(spec []GridSpec) *Grid {
grid.rows = spec
return grid
}
func (grid *Grid) Columns(spec []GridSpec) *Grid {
grid.columns = spec
return grid
}
func (grid *Grid) Children() []Drawable {
children := make([]Drawable, len(grid.cells))
for i, cell := range grid.cells {
children[i] = cell.Content
}
return children
}
func (grid *Grid) Draw(ctx *Context) {
invalid := grid.invalid
if invalid {
grid.reflow(ctx)
}
for _, cell := range grid.cells {
if !cell.invalid && !invalid {
continue
}
rows := grid.rowLayout[cell.Row : cell.Row+cell.RowSpan]
cols := grid.columnLayout[cell.Column : cell.Column+cell.ColSpan]
x := cols[0].Offset
y := rows[0].Offset
width := 0
height := 0
for _, col := range cols {
width += col.Size
}
for _, row := range rows {
height += row.Size
}
subctx := ctx.Subcontext(x, y, width, height)
cell.Content.Draw(subctx)
}
}
func (grid *Grid) reflow(ctx *Context) {
grid.rowLayout = nil
grid.columnLayout = nil
flow := func(specs *[]GridSpec, layouts *[]gridLayout, extent int) {
exact := 0
weight := 0
nweights := 0
for _, spec := range *specs {
if spec.Strategy == SIZE_EXACT {
exact += spec.Size
} else if spec.Strategy == SIZE_WEIGHT {
nweights += 1
weight += spec.Size
}
}
offset := 0
for _, spec := range *specs {
layout := gridLayout{Offset: offset}
if spec.Strategy == SIZE_EXACT {
layout.Size = spec.Size
} else if spec.Strategy == SIZE_WEIGHT {
size := float64(spec.Size) / float64(weight)
size *= float64(extent - exact)
layout.Size = int(math.Floor(size))
}
offset += layout.Size
*layouts = append(*layouts, layout)
}
}
flow(&grid.rows, &grid.rowLayout, ctx.Height())
flow(&grid.columns, &grid.columnLayout, ctx.Width())
grid.invalid = false
}
func (grid *Grid) invalidateLayout() {
grid.invalid = true
grid.DoInvalidate(grid)
}
func (grid *Grid) Invalidate() {
grid.invalidateLayout()
for _, cell := range grid.cells {
cell.Content.Invalidate()
}
}
func (grid *Grid) AddChild(content Drawable) *GridCell {
cell := &GridCell{
RowSpan: 1,
ColSpan: 1,
Content: content,
invalid: true,
}
grid.cells = append(grid.cells, cell)
cell.Content.OnInvalidate(grid.cellInvalidated)
cell.invalid = true
grid.invalidateLayout()
return cell
}
func (grid *Grid) RemoveChild(cell *GridCell) {
for i, _cell := range grid.cells {
if _cell == cell {
grid.cells = append(grid.cells[:i], grid.cells[i+1:]...)
break
}
}
grid.invalidateLayout()
}
func (grid *Grid) cellInvalidated(drawable Drawable) {
var cell *GridCell
for _, cell = range grid.cells {
if cell.Content == drawable {
break
}
cell = nil
}
if cell == nil {
panic(fmt.Errorf("Attempted to invalidate unknown cell"))
}
cell.invalid = true
grid.DoInvalidate(grid)
}