f6216bb621
This adds the Mouseable interface. When this is implemented for a component that item can accept and process mouseevents. At the top level when a mouse event is received it is passed to the grid's handler and then it trickles down until it reaches a component that can actually handle it, such as the tablist, dirlist or msglist. A mouse event is passed so that components can handle other things such as scrolling with the mousewheel. The components themselves then perform the necessary actions. Clicking emails in the messagelist opens them in a new tab. Textinputs can be clicked to position the cursor inside them. Mouseevents are not forwarded to the terminal at the moment. Elements which do not handle mouse events are not required to implement the Mouseable interface.
286 lines
6.2 KiB
Go
286 lines
6.2 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/gdamore/tcell"
|
|
)
|
|
|
|
type Grid struct {
|
|
Invalidatable
|
|
rows []GridSpec
|
|
rowLayout []gridLayout
|
|
columns []GridSpec
|
|
columnLayout []gridLayout
|
|
invalid bool
|
|
|
|
// Protected by mutex
|
|
cells []*GridCell
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
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 atomic.Value // bool
|
|
}
|
|
|
|
func NewGrid() *Grid {
|
|
return &Grid{invalid: true}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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 {
|
|
grid.mutex.RLock()
|
|
defer grid.mutex.RUnlock()
|
|
|
|
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)
|
|
}
|
|
|
|
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+width > ctx.Width() {
|
|
width = ctx.Width() - x
|
|
}
|
|
if y+height > ctx.Height() {
|
|
height = ctx.Height() - y
|
|
}
|
|
if width <= 0 || height <= 0 {
|
|
continue
|
|
}
|
|
subctx := ctx.Subcontext(x, y, width, height)
|
|
cell.Content.Draw(subctx)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
remainingExact := 0
|
|
if weight > 0 {
|
|
remainingExact = (extent - exact) % weight
|
|
}
|
|
for _, spec := range *specs {
|
|
layout := gridLayout{Offset: offset}
|
|
if spec.Strategy == SIZE_EXACT {
|
|
layout.Size = spec.Size
|
|
} else if spec.Strategy == SIZE_WEIGHT {
|
|
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))
|
|
}
|
|
}
|
|
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()
|
|
grid.mutex.RLock()
|
|
for _, cell := range grid.cells {
|
|
cell.Content.Invalidate()
|
|
}
|
|
grid.mutex.RUnlock()
|
|
}
|
|
|
|
func (grid *Grid) AddChild(content Drawable) *GridCell {
|
|
cell := &GridCell{
|
|
RowSpan: 1,
|
|
ColSpan: 1,
|
|
Content: content,
|
|
}
|
|
grid.mutex.Lock()
|
|
grid.cells = append(grid.cells, cell)
|
|
grid.mutex.Unlock()
|
|
cell.Content.OnInvalidate(grid.cellInvalidated)
|
|
cell.invalid.Store(true)
|
|
grid.invalidateLayout()
|
|
return cell
|
|
}
|
|
|
|
func (grid *Grid) RemoveChild(content Drawable) {
|
|
grid.mutex.Lock()
|
|
for i, cell := range grid.cells {
|
|
if cell.Content == content {
|
|
grid.cells = append(grid.cells[:i], grid.cells[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
grid.mutex.Unlock()
|
|
grid.invalidateLayout()
|
|
}
|
|
|
|
func (grid *Grid) cellInvalidated(drawable Drawable) {
|
|
var cell *GridCell
|
|
grid.mutex.RLock()
|
|
for _, cell = range grid.cells {
|
|
if cell.Content == drawable {
|
|
break
|
|
}
|
|
cell = nil
|
|
}
|
|
grid.mutex.RUnlock()
|
|
if cell == nil {
|
|
panic(fmt.Errorf("Attempted to invalidate unknown cell"))
|
|
}
|
|
cell.invalid.Store(true)
|
|
grid.DoInvalidate(grid)
|
|
}
|