2018-02-16 05:05:07 +00:00
|
|
|
package ui
|
|
|
|
|
2018-02-17 21:35:36 +00:00
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math"
|
2019-05-04 14:13:57 +00:00
|
|
|
"sync"
|
2019-04-28 12:20:04 +00:00
|
|
|
"sync/atomic"
|
2019-09-05 22:32:36 +00:00
|
|
|
|
2020-11-30 22:07:03 +00:00
|
|
|
"github.com/gdamore/tcell/v2"
|
2018-02-17 21:35:36 +00:00
|
|
|
)
|
2018-02-16 05:05:07 +00:00
|
|
|
|
|
|
|
type Grid struct {
|
2019-04-27 16:47:59 +00:00
|
|
|
Invalidatable
|
2018-02-18 01:11:58 +00:00
|
|
|
rows []GridSpec
|
|
|
|
rowLayout []gridLayout
|
|
|
|
columns []GridSpec
|
|
|
|
columnLayout []gridLayout
|
2018-02-17 20:21:22 +00:00
|
|
|
invalid bool
|
2019-05-04 14:13:57 +00:00
|
|
|
|
|
|
|
// Protected by mutex
|
|
|
|
cells []*GridCell
|
|
|
|
mutex sync.RWMutex
|
2018-02-16 05:05:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
SIZE_EXACT = iota
|
|
|
|
SIZE_WEIGHT = iota
|
|
|
|
)
|
|
|
|
|
|
|
|
// Specifies the layout of a single row or column
|
2018-02-18 01:11:58 +00:00
|
|
|
type GridSpec struct {
|
2018-02-16 05:05:07 +00:00
|
|
|
// One of SIZE_EXACT or SIZE_WEIGHT
|
2018-02-17 20:21:22 +00:00
|
|
|
Strategy int
|
2020-05-31 11:37:46 +00:00
|
|
|
|
|
|
|
// If Strategy = SIZE_EXACT, this function returns 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 the value returned by this function.
|
|
|
|
Size func() int
|
2018-02-17 20:21:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Used to cache layout of each row/column
|
2018-02-18 01:11:58 +00:00
|
|
|
type gridLayout struct {
|
2018-02-17 20:21:22 +00:00
|
|
|
Offset int
|
|
|
|
Size int
|
2018-02-16 05:05:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type GridCell struct {
|
2018-02-17 20:21:22 +00:00
|
|
|
Row int
|
|
|
|
Column int
|
|
|
|
RowSpan int
|
|
|
|
ColSpan int
|
2018-02-16 05:05:07 +00:00
|
|
|
Content Drawable
|
2019-04-28 12:20:04 +00:00
|
|
|
invalid atomic.Value // bool
|
2018-02-16 05:05:07 +00:00
|
|
|
}
|
|
|
|
|
2018-02-17 21:35:36 +00:00
|
|
|
func NewGrid() *Grid {
|
|
|
|
return &Grid{invalid: true}
|
|
|
|
}
|
|
|
|
|
2019-07-15 18:56:44 +00:00
|
|
|
// MakeGrid creates a grid with the specified number of columns and rows. Each
|
|
|
|
// cell has a size of 1.
|
|
|
|
func MakeGrid(numRows, numCols, rowStrategy, colStrategy int) *Grid {
|
|
|
|
rows := make([]GridSpec, numRows)
|
|
|
|
for i := 0; i < numRows; i++ {
|
2020-05-31 11:37:46 +00:00
|
|
|
rows[i] = GridSpec{rowStrategy, Const(1)}
|
2019-07-15 18:56:44 +00:00
|
|
|
}
|
|
|
|
cols := make([]GridSpec, numCols)
|
|
|
|
for i := 0; i < numCols; i++ {
|
2020-05-31 11:37:46 +00:00
|
|
|
cols[i] = GridSpec{colStrategy, Const(1)}
|
2019-07-15 18:56:44 +00:00
|
|
|
}
|
|
|
|
return NewGrid().Rows(rows).Columns(cols)
|
|
|
|
}
|
|
|
|
|
2018-02-17 21:35:36 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-02-18 01:11:58 +00:00
|
|
|
func (grid *Grid) Rows(spec []GridSpec) *Grid {
|
|
|
|
grid.rows = spec
|
|
|
|
return grid
|
|
|
|
}
|
|
|
|
|
|
|
|
func (grid *Grid) Columns(spec []GridSpec) *Grid {
|
|
|
|
grid.columns = spec
|
|
|
|
return grid
|
|
|
|
}
|
|
|
|
|
2019-01-20 20:06:44 +00:00
|
|
|
func (grid *Grid) Children() []Drawable {
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.RLock()
|
|
|
|
defer grid.mutex.RUnlock()
|
|
|
|
|
2019-01-20 20:06:44 +00:00
|
|
|
children := make([]Drawable, len(grid.cells))
|
|
|
|
for i, cell := range grid.cells {
|
|
|
|
children[i] = cell.Content
|
|
|
|
}
|
|
|
|
return children
|
|
|
|
}
|
|
|
|
|
2018-02-17 20:21:22 +00:00
|
|
|
func (grid *Grid) Draw(ctx *Context) {
|
|
|
|
invalid := grid.invalid
|
|
|
|
if invalid {
|
|
|
|
grid.reflow(ctx)
|
|
|
|
}
|
2019-05-04 14:13:57 +00:00
|
|
|
|
|
|
|
grid.mutex.RLock()
|
|
|
|
defer grid.mutex.RUnlock()
|
|
|
|
|
2018-02-28 00:30:59 +00:00
|
|
|
for _, cell := range grid.cells {
|
2019-04-28 12:20:04 +00:00
|
|
|
cellInvalid := cell.invalid.Load().(bool)
|
|
|
|
if !cellInvalid && !invalid {
|
2018-02-17 20:21:22 +00:00
|
|
|
continue
|
|
|
|
}
|
2018-02-17 21:35:36 +00:00
|
|
|
rows := grid.rowLayout[cell.Row : cell.Row+cell.RowSpan]
|
|
|
|
cols := grid.columnLayout[cell.Column : cell.Column+cell.ColSpan]
|
2018-02-17 20:21:22 +00:00
|
|
|
x := cols[0].Offset
|
|
|
|
y := rows[0].Offset
|
grid: don't draw at a negative offset
aerc panics when using macOS's default terminal emulator, Terminal.app,
when closing all but aerc's tab:
This error was also written to: /tmp/aerc-crash-20220427-194134.log
panic: Attempted to create context with negative offset [recovered]
panic: Attempted to create context with negative offset
goroutine 1 [running]:
git.sr.ht/~rjarry/aerc/logging.PanicHandler()
/Users/ckuehl/src/aerc/logging/panic-logger.go:47 +0x58c
panic({0x100d077a0, 0x14000032700})
/opt/homebrew/Cellar/go/1.18.1/libexec/src/runtime/panic.go:844 +0x258
git.sr.ht/~rjarry/aerc/lib/ui.(*Context).Subcontext(0x1400013e420?, 0x14000202360?, 0x140000ffc48?, 0x1009a10e4?, 0x100da9440?)
/Users/ckuehl/src/aerc/lib/ui/context.go:47 +0x160
git.sr.ht/~rjarry/aerc/lib/ui.(*Grid).Draw(0x1400013e420, 0x14000202360)
/Users/ckuehl/src/aerc/lib/ui/grid.go:143 +0x2bc
git.sr.ht/~rjarry/aerc/widgets.(*Aerc).Draw(0x1400013e4d0, 0x14000202360)
/Users/ckuehl/src/aerc/widgets/aerc.go:178 +0x30
git.sr.ht/~rjarry/aerc/lib/ui.(*UI).Tick(0x1400022bcc0)
/Users/ckuehl/src/aerc/lib/ui/ui.go:116 +0x248
main.main()
/Users/ckuehl/src/aerc/aerc.go:226 +0x9e8
I'm not entirely sure what the interactions are between the terminal
emulator, aerc's grid, and the space that moves around when the tab bar
disappears because there are no more tabs, but this fixes the issue 100%
of the time and I haven't noticed any issues.
Signed-off-by: Connor Kuehl <cipkuehl@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2022-04-28 00:46:03 +00:00
|
|
|
if x < 0 || y < 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-02-17 20:21:22 +00:00
|
|
|
width := 0
|
|
|
|
height := 0
|
|
|
|
for _, col := range cols {
|
2018-02-17 21:35:36 +00:00
|
|
|
width += col.Size
|
|
|
|
}
|
|
|
|
for _, row := range rows {
|
|
|
|
height += row.Size
|
2018-02-17 20:21:22 +00:00
|
|
|
}
|
2019-07-23 19:03:14 +00:00
|
|
|
if x+width > ctx.Width() {
|
|
|
|
width = ctx.Width() - x
|
|
|
|
}
|
|
|
|
if y+height > ctx.Height() {
|
|
|
|
height = ctx.Height() - y
|
|
|
|
}
|
|
|
|
if width <= 0 || height <= 0 {
|
|
|
|
continue
|
|
|
|
}
|
2018-02-17 20:21:22 +00:00
|
|
|
subctx := ctx.Subcontext(x, y, width, height)
|
|
|
|
cell.Content.Draw(subctx)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-05 22:32:36 +00:00
|
|
|
func (grid *Grid) MouseEvent(localX int, localY int, event tcell.Event) {
|
2022-07-31 12:32:48 +00:00
|
|
|
if event, ok := event.(*tcell.EventMouse); ok {
|
2019-09-05 22:32:36 +00:00
|
|
|
invalid := grid.invalid
|
|
|
|
|
|
|
|
grid.mutex.RLock()
|
|
|
|
defer grid.mutex.RUnlock()
|
|
|
|
|
|
|
|
for _, cell := range grid.cells {
|
|
|
|
cellInvalid := cell.invalid.Load().(bool)
|
|
|
|
if !cellInvalid && !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
|
|
|
|
}
|
|
|
|
if x <= localX && localX < x+width && y <= localY && localY < y+height {
|
|
|
|
switch content := cell.Content.(type) {
|
|
|
|
case MouseableDrawableInteractive:
|
|
|
|
content.MouseEvent(localX-x, localY-y, event)
|
|
|
|
case Mouseable:
|
|
|
|
content.MouseEvent(localX-x, localY-y, event)
|
|
|
|
case MouseHandler:
|
|
|
|
content.MouseEvent(localX-x, localY-y, event)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-17 20:21:22 +00:00
|
|
|
func (grid *Grid) reflow(ctx *Context) {
|
|
|
|
grid.rowLayout = nil
|
|
|
|
grid.columnLayout = nil
|
2018-02-18 01:11:58 +00:00
|
|
|
flow := func(specs *[]GridSpec, layouts *[]gridLayout, extent int) {
|
2018-02-17 20:21:22 +00:00
|
|
|
exact := 0
|
|
|
|
weight := 0
|
2018-02-17 21:35:36 +00:00
|
|
|
nweights := 0
|
2018-02-18 01:11:58 +00:00
|
|
|
for _, spec := range *specs {
|
|
|
|
if spec.Strategy == SIZE_EXACT {
|
2020-05-31 11:37:46 +00:00
|
|
|
exact += spec.Size()
|
2018-02-18 01:11:58 +00:00
|
|
|
} else if spec.Strategy == SIZE_WEIGHT {
|
2018-02-17 21:35:36 +00:00
|
|
|
nweights += 1
|
2020-05-31 11:37:46 +00:00
|
|
|
weight += spec.Size()
|
2018-02-17 20:21:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
offset := 0
|
2019-07-16 19:21:28 +00:00
|
|
|
remainingExact := 0
|
|
|
|
if weight > 0 {
|
|
|
|
remainingExact = (extent - exact) % weight
|
|
|
|
}
|
2018-02-18 01:11:58 +00:00
|
|
|
for _, spec := range *specs {
|
|
|
|
layout := gridLayout{Offset: offset}
|
|
|
|
if spec.Strategy == SIZE_EXACT {
|
2020-05-31 11:37:46 +00:00
|
|
|
layout.Size = spec.Size()
|
2018-02-18 01:11:58 +00:00
|
|
|
} else if spec.Strategy == SIZE_WEIGHT {
|
2020-05-31 11:37:46 +00:00
|
|
|
proportion := float64(spec.Size()) / float64(weight)
|
2019-07-16 19:21:28 +00:00
|
|
|
size := proportion * float64(extent-exact)
|
|
|
|
if remainingExact > 0 {
|
|
|
|
extraExact := int(math.Ceil(proportion * float64(remainingExact)))
|
|
|
|
layout.Size = int(math.Floor(size)) + extraExact
|
|
|
|
remainingExact -= extraExact
|
|
|
|
|
|
|
|
} else {
|
|
|
|
layout.Size = int(math.Floor(size))
|
|
|
|
}
|
2018-02-17 20:21:22 +00:00
|
|
|
}
|
2018-02-17 21:35:36 +00:00
|
|
|
offset += layout.Size
|
2018-02-17 20:21:22 +00:00
|
|
|
*layouts = append(*layouts, layout)
|
|
|
|
}
|
|
|
|
}
|
2018-02-18 01:11:58 +00:00
|
|
|
flow(&grid.rows, &grid.rowLayout, ctx.Height())
|
|
|
|
flow(&grid.columns, &grid.columnLayout, ctx.Width())
|
2018-02-17 20:21:22 +00:00
|
|
|
grid.invalid = false
|
|
|
|
}
|
|
|
|
|
2018-02-17 21:35:36 +00:00
|
|
|
func (grid *Grid) invalidateLayout() {
|
2018-02-17 20:21:22 +00:00
|
|
|
grid.invalid = true
|
2019-04-27 16:47:59 +00:00
|
|
|
grid.DoInvalidate(grid)
|
2018-02-17 21:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (grid *Grid) Invalidate() {
|
|
|
|
grid.invalidateLayout()
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.RLock()
|
2018-02-28 00:30:59 +00:00
|
|
|
for _, cell := range grid.cells {
|
2018-02-17 21:35:36 +00:00
|
|
|
cell.Content.Invalidate()
|
|
|
|
}
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.RUnlock()
|
2018-02-16 05:05:07 +00:00
|
|
|
}
|
|
|
|
|
2018-02-17 21:35:36 +00:00
|
|
|
func (grid *Grid) AddChild(content Drawable) *GridCell {
|
|
|
|
cell := &GridCell{
|
|
|
|
RowSpan: 1,
|
|
|
|
ColSpan: 1,
|
|
|
|
Content: content,
|
|
|
|
}
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.Lock()
|
2018-02-28 00:30:59 +00:00
|
|
|
grid.cells = append(grid.cells, cell)
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.Unlock()
|
2018-02-16 05:05:07 +00:00
|
|
|
cell.Content.OnInvalidate(grid.cellInvalidated)
|
2019-04-28 12:20:04 +00:00
|
|
|
cell.invalid.Store(true)
|
2018-02-17 21:35:36 +00:00
|
|
|
grid.invalidateLayout()
|
|
|
|
return cell
|
2018-02-16 05:05:07 +00:00
|
|
|
}
|
|
|
|
|
2019-05-13 20:24:05 +00:00
|
|
|
func (grid *Grid) RemoveChild(content Drawable) {
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.Lock()
|
2019-05-13 20:24:05 +00:00
|
|
|
for i, cell := range grid.cells {
|
|
|
|
if cell.Content == content {
|
2018-02-28 00:30:59 +00:00
|
|
|
grid.cells = append(grid.cells[:i], grid.cells[i+1:]...)
|
2018-02-16 05:05:07 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.Unlock()
|
2018-02-17 21:35:36 +00:00
|
|
|
grid.invalidateLayout()
|
2018-02-16 05:05:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (grid *Grid) cellInvalidated(drawable Drawable) {
|
|
|
|
var cell *GridCell
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.RLock()
|
2018-02-28 00:30:59 +00:00
|
|
|
for _, cell = range grid.cells {
|
2018-02-16 05:05:07 +00:00
|
|
|
if cell.Content == drawable {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
cell = nil
|
|
|
|
}
|
2019-05-04 14:13:57 +00:00
|
|
|
grid.mutex.RUnlock()
|
2018-02-16 05:05:07 +00:00
|
|
|
if cell == nil {
|
|
|
|
panic(fmt.Errorf("Attempted to invalidate unknown cell"))
|
|
|
|
}
|
2019-04-28 12:20:04 +00:00
|
|
|
cell.invalid.Store(true)
|
2019-04-27 16:47:59 +00:00
|
|
|
grid.DoInvalidate(grid)
|
2018-02-16 05:05:07 +00:00
|
|
|
}
|
2020-05-31 11:37:46 +00:00
|
|
|
|
|
|
|
func Const(i int) func() int {
|
|
|
|
return func() int { return i }
|
|
|
|
}
|