aboutsummaryrefslogblamecommitdiffstats
path: root/lib/ui/grid.go
blob: 00f759bf9cc3d784b14c131df56c285b36a9b4e2 (plain) (tree)
1
2
3
4
5
6
7
8
9

          
        
              
              
 
                                     
                                     
 

                  



                                 



                             







                                                 
                      
                                           
                    





                                                                              


                                          
                        

                  


                      



                   
                        

 
                      
                      

 




                                                                              
                                                         


                                         
                                                         



                                                 











                                                      









                                                  
                                      
                        



                                  
                                         

                                                                                 

                                   



                                   

                           
                                          



                                          
                 








                                                 
                                                             


                                                 


         
                                                                         
                                                       




                                                 

























                                                                                                


                                        
                                                                            

                           
                             

                                                        
                                                    
                                                                
                                             
                                                     


                           



                                                                  


                                                            
                                                         
                                                                
                                                                                    








                                                                                                          
                         
                                             


                                                           

                                                            


                                
                    

 




                                                        
         
                         
                                             
                           
                         
                   

 
                                                 
                         

                                            
                                                                                


                             
                           
                         
 
 

















                                                            


                                      
package ui

import (
	"math"
	"sync"

	"git.sr.ht/~rockorager/vaxis"
	"github.com/gdamore/tcell/v2"
)

type Grid struct {
	rows         []GridSpec
	rowLayout    []gridLayout
	columns      []GridSpec
	columnLayout []gridLayout

	// 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 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
}

// 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
}

func NewGrid() *Grid {
	return &Grid{}
}

// 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, Const(1)}
	}
	cols := make([]GridSpec, numCols)
	for i := 0; i < numCols; i++ {
		cols[i] = GridSpec{colStrategy, Const(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) Draw(ctx *Context) {
	grid.reflow(ctx)

	grid.mutex.RLock()
	defer grid.mutex.RUnlock()

	for _, cell := range grid.cells {
		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
		if x < 0 || y < 0 {
			continue
		}

		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)
		if cell.Content != nil {
			cell.Content.Draw(subctx)
		}
	}
}

func (grid *Grid) MouseEvent(localX int, localY int, event vaxis.Event) {
	if event, ok := event.(*tcell.EventMouse); ok {

		grid.mutex.RLock()
		defer grid.mutex.RUnlock()

		for _, cell := range grid.cells {
			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())
}

func (grid *Grid) Invalidate() {
	Invalidate()
}

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()
	grid.Invalidate()
	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.Invalidate()
}

func (grid *Grid) ReplaceChild(old Drawable, new Drawable) {
	grid.mutex.Lock()
	for i, cell := range grid.cells {
		if cell.Content == old {
			grid.cells[i] = &GridCell{
				RowSpan: cell.RowSpan,
				ColSpan: cell.ColSpan,
				Row:     cell.Row,
				Column:  cell.Column,
				Content: new,
			}
			break
		}
	}
	grid.mutex.Unlock()
	grid.Invalidate()
}

func Const(i int) func() int {
	return func() int { return i }
}