2018-02-16 06:05:07 +01:00
|
|
|
package ui
|
|
|
|
|
2018-02-17 22:35:36 +01:00
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math"
|
2019-05-04 16:13:57 +02:00
|
|
|
"sync"
|
2019-04-28 14:20:04 +02:00
|
|
|
"sync/atomic"
|
2019-09-06 00:32:36 +02:00
|
|
|
|
|
|
|
"github.com/gdamore/tcell"
|
2018-02-17 22:35:36 +01:00
|
|
|
)
|
2018-02-16 06:05:07 +01:00
|
|
|
|
|
|
|
type Grid struct {
|
2019-04-27 18:47:59 +02:00
|
|
|
Invalidatable
|
2018-02-18 02:11:58 +01:00
|
|
|
rows []GridSpec
|
|
|
|
rowLayout []gridLayout
|
|
|
|
columns []GridSpec
|
|
|
|
columnLayout []gridLayout
|
2018-02-17 21:21:22 +01:00
|
|
|
invalid bool
|
2019-05-04 16:13:57 +02:00
|
|
|
|
|
|
|
// Protected by mutex
|
|
|
|
cells []*GridCell
|
|
|
|
mutex sync.RWMutex
|
2018-02-16 06:05:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
SIZE_EXACT = iota
|
|
|
|
SIZE_WEIGHT = iota
|
|
|
|
)
|
|
|
|
|
|
|
|
// Specifies the layout of a single row or column
|
2018-02-18 02:11:58 +01:00
|
|
|
type GridSpec struct {
|
2018-02-16 06:05:07 +01:00
|
|
|
// One of SIZE_EXACT or SIZE_WEIGHT
|
2018-02-17 21:21:22 +01:00
|
|
|
Strategy int
|
2018-02-18 02:11:58 +01:00
|
|
|
// 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.
|
2018-02-17 21:21:22 +01:00
|
|
|
Size int
|
|
|
|
}
|
|
|
|
|
|
|
|
// Used to cache layout of each row/column
|
2018-02-18 02:11:58 +01:00
|
|
|
type gridLayout struct {
|
2018-02-17 21:21:22 +01:00
|
|
|
Offset int
|
|
|
|
Size int
|
2018-02-16 06:05:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type GridCell struct {
|
2018-02-17 21:21:22 +01:00
|
|
|
Row int
|
|
|
|
Column int
|
|
|
|
RowSpan int
|
|
|
|
ColSpan int
|
2018-02-16 06:05:07 +01:00
|
|
|
Content Drawable
|
2019-04-28 14:20:04 +02:00
|
|
|
invalid atomic.Value // bool
|
2018-02-16 06:05:07 +01:00
|
|
|
}
|
|
|
|
|
2018-02-17 22:35:36 +01:00
|
|
|
func NewGrid() *Grid {
|
|
|
|
return &Grid{invalid: true}
|
|
|
|
}
|
|
|
|
|
2019-07-15 20:56:44 +02: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++ {
|
|
|
|
rows[i] = GridSpec{rowStrategy, 1}
|
|
|
|
}
|
|
|
|
cols := make([]GridSpec, numCols)
|
|
|
|
for i := 0; i < numCols; i++ {
|
|
|
|
cols[i] = GridSpec{colStrategy, 1}
|
|
|
|
}
|
|
|
|
return NewGrid().Rows(rows).Columns(cols)
|
|
|
|
}
|
|
|
|
|
2018-02-17 22:35:36 +01: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 02:11:58 +01: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 21:06:44 +01:00
|
|
|
func (grid *Grid) Children() []Drawable {
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.RLock()
|
|
|
|
defer grid.mutex.RUnlock()
|
|
|
|
|
2019-01-20 21:06:44 +01:00
|
|
|
children := make([]Drawable, len(grid.cells))
|
|
|
|
for i, cell := range grid.cells {
|
|
|
|
children[i] = cell.Content
|
|
|
|
}
|
|
|
|
return children
|
|
|
|
}
|
|
|
|
|
2018-02-17 21:21:22 +01:00
|
|
|
func (grid *Grid) Draw(ctx *Context) {
|
|
|
|
invalid := grid.invalid
|
|
|
|
if invalid {
|
|
|
|
grid.reflow(ctx)
|
|
|
|
}
|
2019-05-04 16:13:57 +02:00
|
|
|
|
|
|
|
grid.mutex.RLock()
|
|
|
|
defer grid.mutex.RUnlock()
|
|
|
|
|
2018-02-28 01:30:59 +01:00
|
|
|
for _, cell := range grid.cells {
|
2019-04-28 14:20:04 +02:00
|
|
|
cellInvalid := cell.invalid.Load().(bool)
|
|
|
|
if !cellInvalid && !invalid {
|
2018-02-17 21:21:22 +01:00
|
|
|
continue
|
|
|
|
}
|
2018-02-17 22:35:36 +01:00
|
|
|
rows := grid.rowLayout[cell.Row : cell.Row+cell.RowSpan]
|
|
|
|
cols := grid.columnLayout[cell.Column : cell.Column+cell.ColSpan]
|
2018-02-17 21:21:22 +01:00
|
|
|
x := cols[0].Offset
|
|
|
|
y := rows[0].Offset
|
|
|
|
width := 0
|
|
|
|
height := 0
|
|
|
|
for _, col := range cols {
|
2018-02-17 22:35:36 +01:00
|
|
|
width += col.Size
|
|
|
|
}
|
|
|
|
for _, row := range rows {
|
|
|
|
height += row.Size
|
2018-02-17 21:21:22 +01:00
|
|
|
}
|
2019-07-23 21:03:14 +02: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 21:21:22 +01:00
|
|
|
subctx := ctx.Subcontext(x, y, width, height)
|
|
|
|
cell.Content.Draw(subctx)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-06 00:32:36 +02:00
|
|
|
func (grid *Grid) MouseEvent(localX int, localY int, event tcell.Event) {
|
|
|
|
switch event := event.(type) {
|
|
|
|
case *tcell.EventMouse:
|
|
|
|
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 21:21:22 +01:00
|
|
|
func (grid *Grid) reflow(ctx *Context) {
|
|
|
|
grid.rowLayout = nil
|
|
|
|
grid.columnLayout = nil
|
2018-02-18 02:11:58 +01:00
|
|
|
flow := func(specs *[]GridSpec, layouts *[]gridLayout, extent int) {
|
2018-02-17 21:21:22 +01:00
|
|
|
exact := 0
|
|
|
|
weight := 0
|
2018-02-17 22:35:36 +01:00
|
|
|
nweights := 0
|
2018-02-18 02:11:58 +01:00
|
|
|
for _, spec := range *specs {
|
|
|
|
if spec.Strategy == SIZE_EXACT {
|
|
|
|
exact += spec.Size
|
|
|
|
} else if spec.Strategy == SIZE_WEIGHT {
|
2018-02-17 22:35:36 +01:00
|
|
|
nweights += 1
|
2018-02-18 02:11:58 +01:00
|
|
|
weight += spec.Size
|
2018-02-17 21:21:22 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
offset := 0
|
2019-07-16 21:21:28 +02:00
|
|
|
remainingExact := 0
|
|
|
|
if weight > 0 {
|
|
|
|
remainingExact = (extent - exact) % weight
|
|
|
|
}
|
2018-02-18 02:11:58 +01:00
|
|
|
for _, spec := range *specs {
|
|
|
|
layout := gridLayout{Offset: offset}
|
|
|
|
if spec.Strategy == SIZE_EXACT {
|
|
|
|
layout.Size = spec.Size
|
|
|
|
} else if spec.Strategy == SIZE_WEIGHT {
|
2019-07-16 21:21:28 +02:00
|
|
|
proportion := float64(spec.Size) / float64(weight)
|
|
|
|
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 21:21:22 +01:00
|
|
|
}
|
2018-02-17 22:35:36 +01:00
|
|
|
offset += layout.Size
|
2018-02-17 21:21:22 +01:00
|
|
|
*layouts = append(*layouts, layout)
|
|
|
|
}
|
|
|
|
}
|
2018-02-18 02:11:58 +01:00
|
|
|
flow(&grid.rows, &grid.rowLayout, ctx.Height())
|
|
|
|
flow(&grid.columns, &grid.columnLayout, ctx.Width())
|
2018-02-17 21:21:22 +01:00
|
|
|
grid.invalid = false
|
|
|
|
}
|
|
|
|
|
2018-02-17 22:35:36 +01:00
|
|
|
func (grid *Grid) invalidateLayout() {
|
2018-02-17 21:21:22 +01:00
|
|
|
grid.invalid = true
|
2019-04-27 18:47:59 +02:00
|
|
|
grid.DoInvalidate(grid)
|
2018-02-17 22:35:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (grid *Grid) Invalidate() {
|
|
|
|
grid.invalidateLayout()
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.RLock()
|
2018-02-28 01:30:59 +01:00
|
|
|
for _, cell := range grid.cells {
|
2018-02-17 22:35:36 +01:00
|
|
|
cell.Content.Invalidate()
|
|
|
|
}
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.RUnlock()
|
2018-02-16 06:05:07 +01:00
|
|
|
}
|
|
|
|
|
2018-02-17 22:35:36 +01:00
|
|
|
func (grid *Grid) AddChild(content Drawable) *GridCell {
|
|
|
|
cell := &GridCell{
|
|
|
|
RowSpan: 1,
|
|
|
|
ColSpan: 1,
|
|
|
|
Content: content,
|
|
|
|
}
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.Lock()
|
2018-02-28 01:30:59 +01:00
|
|
|
grid.cells = append(grid.cells, cell)
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.Unlock()
|
2018-02-16 06:05:07 +01:00
|
|
|
cell.Content.OnInvalidate(grid.cellInvalidated)
|
2019-04-28 14:20:04 +02:00
|
|
|
cell.invalid.Store(true)
|
2018-02-17 22:35:36 +01:00
|
|
|
grid.invalidateLayout()
|
|
|
|
return cell
|
2018-02-16 06:05:07 +01:00
|
|
|
}
|
|
|
|
|
2019-05-13 22:24:05 +02:00
|
|
|
func (grid *Grid) RemoveChild(content Drawable) {
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.Lock()
|
2019-05-13 22:24:05 +02:00
|
|
|
for i, cell := range grid.cells {
|
|
|
|
if cell.Content == content {
|
2018-02-28 01:30:59 +01:00
|
|
|
grid.cells = append(grid.cells[:i], grid.cells[i+1:]...)
|
2018-02-16 06:05:07 +01:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.Unlock()
|
2018-02-17 22:35:36 +01:00
|
|
|
grid.invalidateLayout()
|
2018-02-16 06:05:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (grid *Grid) cellInvalidated(drawable Drawable) {
|
|
|
|
var cell *GridCell
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.RLock()
|
2018-02-28 01:30:59 +01:00
|
|
|
for _, cell = range grid.cells {
|
2018-02-16 06:05:07 +01:00
|
|
|
if cell.Content == drawable {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
cell = nil
|
|
|
|
}
|
2019-05-04 16:13:57 +02:00
|
|
|
grid.mutex.RUnlock()
|
2018-02-16 06:05:07 +01:00
|
|
|
if cell == nil {
|
|
|
|
panic(fmt.Errorf("Attempted to invalidate unknown cell"))
|
|
|
|
}
|
2019-04-28 14:20:04 +02:00
|
|
|
cell.invalid.Store(true)
|
2019-04-27 18:47:59 +02:00
|
|
|
grid.DoInvalidate(grid)
|
2018-02-16 06:05:07 +01:00
|
|
|
}
|