2018-02-16 07:37:25 +00:00
|
|
|
package tview
|
|
|
|
|
|
|
|
import (
|
|
|
|
"math"
|
|
|
|
|
2020-10-18 12:15:57 +00:00
|
|
|
"github.com/gdamore/tcell/v2"
|
2018-02-16 07:37:25 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// gridItem represents one primitive and its possible position on a grid.
|
|
|
|
type gridItem struct {
|
|
|
|
Item Primitive // The item to be positioned. May be nil for an empty item.
|
|
|
|
Row, Column int // The top-left grid cell where the item is placed.
|
|
|
|
Width, Height int // The number of rows and columns the item occupies.
|
|
|
|
MinGridWidth, MinGridHeight int // The minimum grid width/height for which this item is visible.
|
|
|
|
Focus bool // Whether or not this item attracts the layout's focus.
|
|
|
|
|
|
|
|
visible bool // Whether or not this item was visible the last time the grid was drawn.
|
|
|
|
x, y, w, h int // The last position of the item relative to the top-left corner of the grid. Undefined if visible is false.
|
|
|
|
}
|
|
|
|
|
|
|
|
// Grid is an implementation of a grid-based layout. It works by defining the
|
|
|
|
// size of the rows and columns, then placing primitives into the grid.
|
|
|
|
//
|
|
|
|
// Some settings can lead to the grid exceeding its available space. SetOffset()
|
|
|
|
// can then be used to scroll in steps of rows and columns. These offset values
|
2018-02-20 10:19:30 +00:00
|
|
|
// can also be controlled with the arrow keys (or the "g","G", "j", "k", "h",
|
|
|
|
// and "l" keys) while the grid has focus and none of its contained primitives
|
|
|
|
// do.
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
|
|
|
// See https://github.com/rivo/tview/wiki/Grid for an example.
|
|
|
|
type Grid struct {
|
|
|
|
*Box
|
|
|
|
|
|
|
|
// The items to be positioned.
|
|
|
|
items []*gridItem
|
|
|
|
|
|
|
|
// The definition of the rows and columns of the grid. See
|
2023-01-29 23:42:27 +00:00
|
|
|
// [Grid.SetRows] / [Grid.SetColumns] for details.
|
2018-02-16 07:37:25 +00:00
|
|
|
rows, columns []int
|
|
|
|
|
|
|
|
// The minimum sizes for rows and columns.
|
|
|
|
minWidth, minHeight int
|
|
|
|
|
|
|
|
// The size of the gaps between neighboring primitives. This is automatically
|
|
|
|
// set to 1 if borders is true.
|
|
|
|
gapRows, gapColumns int
|
|
|
|
|
|
|
|
// The number of rows and columns skipped before drawing the top-left corner
|
|
|
|
// of the grid.
|
|
|
|
rowOffset, columnOffset int
|
|
|
|
|
|
|
|
// Whether or not borders are drawn around grid items. If this is set to true,
|
|
|
|
// a gap size of 1 is automatically assumed (which is filled with the border
|
|
|
|
// graphics).
|
|
|
|
borders bool
|
2018-02-20 10:19:30 +00:00
|
|
|
|
|
|
|
// The color of the borders around grid items.
|
|
|
|
bordersColor tcell.Color
|
2018-02-16 07:37:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewGrid returns a new grid-based layout container with no initial primitives.
|
2018-05-02 15:02:52 +00:00
|
|
|
//
|
2021-03-12 17:48:52 +00:00
|
|
|
// Note that Box, the superclass of Grid, will be transparent so that any grid
|
|
|
|
// areas not covered by any primitives will leave their background unchanged. To
|
|
|
|
// clear a Grid's background before any items are drawn, reset its Box to one
|
|
|
|
// with the desired color:
|
2018-05-02 15:02:52 +00:00
|
|
|
//
|
2022-08-28 10:54:24 +00:00
|
|
|
// grid.Box = NewBox()
|
2018-02-16 07:37:25 +00:00
|
|
|
func NewGrid() *Grid {
|
|
|
|
g := &Grid{
|
2018-02-20 10:19:30 +00:00
|
|
|
bordersColor: Styles.GraphicsColor,
|
2018-02-16 07:37:25 +00:00
|
|
|
}
|
2021-03-12 17:48:52 +00:00
|
|
|
g.Box = NewBox()
|
|
|
|
g.Box.dontClear = true
|
2018-02-16 07:37:25 +00:00
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2019-01-24 12:01:53 +00:00
|
|
|
// SetColumns defines how the columns of the grid are distributed. Each value
|
|
|
|
// defines the size of one column, starting with the leftmost column. Values
|
2023-09-27 06:44:22 +00:00
|
|
|
// greater than 0 represent absolute column widths (gaps not included). Values
|
|
|
|
// less than or equal to 0 represent proportional column widths or fractions of
|
|
|
|
// the remaining free space, where 0 is treated the same as -1. That is, a
|
|
|
|
// column with a value of -3 will have three times the width of a column with a
|
|
|
|
// value of -1 (or 0). The minimum width set with SetMinSize() is always
|
|
|
|
// observed.
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
2019-01-24 12:01:53 +00:00
|
|
|
// Primitives may extend beyond the columns defined explicitly with this
|
|
|
|
// function. A value of 0 is assumed for any undefined column. In fact, if you
|
|
|
|
// never call this function, all columns occupied by primitives will have the
|
|
|
|
// same width. On the other hand, unoccupied columns defined with this function
|
|
|
|
// will always take their place.
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
|
|
|
// Assuming a total width of the grid of 100 cells and a minimum width of 0, the
|
2019-01-24 12:01:53 +00:00
|
|
|
// following call will result in columns with widths of 30, 10, 15, 15, and 30
|
2018-02-16 07:37:25 +00:00
|
|
|
// cells:
|
|
|
|
//
|
2022-08-28 10:54:24 +00:00
|
|
|
// grid.SetColumns(30, 10, -1, -1, -2)
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
2019-01-24 12:01:53 +00:00
|
|
|
// If a primitive were then placed in the 6th and 7th column, the resulting
|
|
|
|
// widths would be: 30, 10, 10, 10, 20, 10, and 10 cells.
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
|
|
|
// If you then called SetMinSize() as follows:
|
|
|
|
//
|
2022-08-28 10:54:24 +00:00
|
|
|
// grid.SetMinSize(15, 20)
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
|
|
|
// The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total
|
|
|
|
// of 125 cells, 25 cells wider than the available grid width.
|
2019-01-24 12:01:53 +00:00
|
|
|
func (g *Grid) SetColumns(columns ...int) *Grid {
|
|
|
|
g.columns = columns
|
2018-02-16 07:37:25 +00:00
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2019-01-24 12:01:53 +00:00
|
|
|
// SetRows defines how the rows of the grid are distributed. These values behave
|
2023-01-29 23:42:27 +00:00
|
|
|
// the same as the column values provided with [Grid.SetColumns], see there
|
2022-08-28 10:54:24 +00:00
|
|
|
// for a definition and examples.
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
2019-01-24 12:01:53 +00:00
|
|
|
// The provided values correspond to row heights, the first value defining
|
|
|
|
// the height of the topmost row.
|
|
|
|
func (g *Grid) SetRows(rows ...int) *Grid {
|
|
|
|
g.rows = rows
|
2018-02-16 07:37:25 +00:00
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2023-01-29 23:42:27 +00:00
|
|
|
// SetSize is a shortcut for [Grid.SetRows] and [Grid.SetColumns] where
|
2022-08-28 10:54:24 +00:00
|
|
|
// all row and column values are set to the given size values. See
|
2023-01-29 23:42:27 +00:00
|
|
|
// [Grid.SetColumns] for details on sizes.
|
2018-02-20 15:47:11 +00:00
|
|
|
func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) *Grid {
|
|
|
|
g.rows = make([]int, numRows)
|
2018-02-20 10:56:44 +00:00
|
|
|
for index := range g.rows {
|
|
|
|
g.rows[index] = rowSize
|
|
|
|
}
|
2018-02-20 15:47:11 +00:00
|
|
|
g.columns = make([]int, numColumns)
|
2018-02-20 10:56:44 +00:00
|
|
|
for index := range g.columns {
|
|
|
|
g.columns[index] = columnSize
|
|
|
|
}
|
2018-02-19 19:23:01 +00:00
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2018-02-16 07:37:25 +00:00
|
|
|
// SetMinSize sets an absolute minimum width for rows and an absolute minimum
|
2018-02-19 19:23:01 +00:00
|
|
|
// height for columns. Panics if negative values are provided.
|
2018-02-16 07:37:25 +00:00
|
|
|
func (g *Grid) SetMinSize(row, column int) *Grid {
|
2018-02-19 19:23:01 +00:00
|
|
|
if row < 0 || column < 0 {
|
|
|
|
panic("Invalid minimum row/column size")
|
|
|
|
}
|
2018-02-20 10:19:30 +00:00
|
|
|
g.minHeight, g.minWidth = row, column
|
2018-02-16 07:37:25 +00:00
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetGap sets the size of the gaps between neighboring primitives on the grid.
|
|
|
|
// If borders are drawn (see SetBorders()), these values are ignored and a gap
|
2018-02-19 19:23:01 +00:00
|
|
|
// of 1 is assumed. Panics if negative values are provided.
|
2018-02-16 07:37:25 +00:00
|
|
|
func (g *Grid) SetGap(row, column int) *Grid {
|
2018-02-19 19:23:01 +00:00
|
|
|
if row < 0 || column < 0 {
|
|
|
|
panic("Invalid gap size")
|
|
|
|
}
|
2018-02-16 07:37:25 +00:00
|
|
|
g.gapRows, g.gapColumns = row, column
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetBorders sets whether or not borders are drawn around grid items. Setting
|
|
|
|
// this value to true will cause the gap values (see SetGap()) to be ignored and
|
|
|
|
// automatically assumed to be 1 where the border graphics are drawn.
|
|
|
|
func (g *Grid) SetBorders(borders bool) *Grid {
|
|
|
|
g.borders = borders
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2018-02-20 10:19:30 +00:00
|
|
|
// SetBordersColor sets the color of the item borders.
|
|
|
|
func (g *Grid) SetBordersColor(color tcell.Color) *Grid {
|
|
|
|
g.bordersColor = color
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2018-02-16 07:37:25 +00:00
|
|
|
// AddItem adds a primitive and its position to the grid. The top-left corner
|
|
|
|
// of the primitive will be located in the top-left corner of the grid cell at
|
2019-01-22 19:23:31 +00:00
|
|
|
// the given row and column and will span "rowSpan" rows and "colSpan" columns.
|
|
|
|
// For example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6:
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
2022-08-28 10:54:24 +00:00
|
|
|
// grid.AddItem(p, 2, 5, 3, 2, 0, 0, true)
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
2019-01-22 19:23:31 +00:00
|
|
|
// If rowSpan or colSpan is 0, the primitive will not be drawn.
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
|
|
|
// You can add the same primitive multiple times with different grid positions.
|
|
|
|
// The minGridWidth and minGridHeight values will then determine which of those
|
|
|
|
// positions will be used. This is similar to CSS media queries. These minimum
|
|
|
|
// values refer to the overall size of the grid. If multiple items for the same
|
2023-11-26 15:24:17 +00:00
|
|
|
// primitive apply, the one with the highest minimum value (width or height,
|
|
|
|
// whatever is higher) will be used, or the primitive added last if those values
|
|
|
|
// are the same. Example:
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
2022-08-28 10:54:24 +00:00
|
|
|
// grid.AddItem(p, 0, 0, 0, 0, 0, 0, true). // Hide in small grids.
|
|
|
|
// AddItem(p, 0, 0, 1, 2, 100, 0, true). // One-column layout for medium grids.
|
|
|
|
// AddItem(p, 1, 1, 3, 2, 300, 0, true) // Multi-column layout for large grids.
|
2018-02-16 07:37:25 +00:00
|
|
|
//
|
|
|
|
// To use the same grid layout for all sizes, simply set minGridWidth and
|
|
|
|
// minGridHeight to 0.
|
|
|
|
//
|
|
|
|
// If the item's focus is set to true, it will receive focus when the grid
|
|
|
|
// receives focus. If there are multiple items with a true focus flag, the last
|
|
|
|
// visible one that was added will receive focus.
|
2019-01-22 19:23:31 +00:00
|
|
|
func (g *Grid) AddItem(p Primitive, row, column, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) *Grid {
|
2018-02-19 19:23:01 +00:00
|
|
|
g.items = append(g.items, &gridItem{
|
|
|
|
Item: p,
|
|
|
|
Row: row,
|
|
|
|
Column: column,
|
2019-01-22 19:23:31 +00:00
|
|
|
Height: rowSpan,
|
|
|
|
Width: colSpan,
|
2018-02-19 19:23:01 +00:00
|
|
|
MinGridHeight: minGridHeight,
|
|
|
|
MinGridWidth: minGridWidth,
|
|
|
|
Focus: focus,
|
|
|
|
})
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2018-03-02 16:07:46 +00:00
|
|
|
// RemoveItem removes all items for the given primitive from the grid, keeping
|
|
|
|
// the order of the remaining items intact.
|
|
|
|
func (g *Grid) RemoveItem(p Primitive) *Grid {
|
|
|
|
for index := len(g.items) - 1; index >= 0; index-- {
|
|
|
|
if g.items[index].Item == p {
|
|
|
|
g.items = append(g.items[:index], g.items[index+1:]...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2018-02-19 19:23:01 +00:00
|
|
|
// Clear removes all items from the grid.
|
|
|
|
func (g *Grid) Clear() *Grid {
|
|
|
|
g.items = nil
|
2018-02-16 07:37:25 +00:00
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetOffset sets the number of rows and columns which are skipped before
|
|
|
|
// drawing the first grid cell in the top-left corner. As the grid will never
|
|
|
|
// completely move off the screen, these values may be adjusted the next time
|
|
|
|
// the grid is drawn. The actual position of the grid may also be adjusted such
|
2019-07-17 16:46:45 +00:00
|
|
|
// that contained primitives that have focus remain visible.
|
2018-02-16 07:37:25 +00:00
|
|
|
func (g *Grid) SetOffset(rows, columns int) *Grid {
|
|
|
|
g.rowOffset, g.columnOffset = rows, columns
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetOffset returns the current row and column offset (see SetOffset() for
|
|
|
|
// details).
|
|
|
|
func (g *Grid) GetOffset() (rows, columns int) {
|
|
|
|
return g.rowOffset, g.columnOffset
|
|
|
|
}
|
|
|
|
|
|
|
|
// Focus is called when this primitive receives focus.
|
|
|
|
func (g *Grid) Focus(delegate func(p Primitive)) {
|
|
|
|
for _, item := range g.items {
|
2018-02-20 10:19:30 +00:00
|
|
|
if item.Focus {
|
2018-02-16 07:37:25 +00:00
|
|
|
delegate(item.Item)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2021-11-08 16:05:49 +00:00
|
|
|
g.Box.Focus(delegate)
|
2018-02-16 07:37:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// HasFocus returns whether or not this primitive has focus.
|
|
|
|
func (g *Grid) HasFocus() bool {
|
|
|
|
for _, item := range g.items {
|
2020-11-17 18:33:25 +00:00
|
|
|
if item.visible && item.Item.HasFocus() {
|
2018-02-16 07:37:25 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2021-11-08 16:05:49 +00:00
|
|
|
return g.Box.HasFocus()
|
2018-02-16 07:37:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Draw draws this primitive onto the screen.
|
|
|
|
func (g *Grid) Draw(screen tcell.Screen) {
|
2020-11-17 18:33:25 +00:00
|
|
|
g.Box.DrawForSubclass(screen, g)
|
2018-02-19 19:23:01 +00:00
|
|
|
x, y, width, height := g.GetInnerRect()
|
2018-12-03 09:22:43 +00:00
|
|
|
screenWidth, screenHeight := screen.Size()
|
2018-02-19 19:23:01 +00:00
|
|
|
|
|
|
|
// Make a list of items which apply.
|
2023-11-26 15:24:17 +00:00
|
|
|
items := make([]*gridItem, 0, len(g.items))
|
|
|
|
ItemLoop:
|
2018-02-19 19:23:01 +00:00
|
|
|
for _, item := range g.items {
|
|
|
|
item.visible = false
|
2023-11-26 15:24:17 +00:00
|
|
|
if item.Item == nil || item.Width <= 0 || item.Height <= 0 || width < item.MinGridWidth || height < item.MinGridHeight {
|
|
|
|
continue // Disqualified.
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
2023-11-26 15:24:17 +00:00
|
|
|
|
|
|
|
// Check for overlaps.
|
|
|
|
for index, existing := range items {
|
|
|
|
// Do they overlap?
|
|
|
|
if item.Row >= existing.Row+existing.Height || item.Row+item.Height <= existing.Row ||
|
|
|
|
item.Column >= existing.Column+existing.Width || item.Column+item.Width <= existing.Column {
|
|
|
|
break // They don't.
|
|
|
|
}
|
|
|
|
|
|
|
|
// What's their minimum size?
|
|
|
|
itemMin := item.MinGridWidth
|
|
|
|
if item.MinGridHeight > itemMin {
|
|
|
|
itemMin = item.MinGridHeight
|
|
|
|
}
|
|
|
|
existingMin := existing.MinGridWidth
|
|
|
|
if existing.MinGridHeight > existingMin {
|
|
|
|
existingMin = existing.MinGridHeight
|
|
|
|
}
|
|
|
|
|
|
|
|
// Which one is more important?
|
|
|
|
if itemMin < existingMin {
|
|
|
|
continue ItemLoop // This one isn't. Drop it.
|
|
|
|
}
|
|
|
|
items[index] = item
|
|
|
|
continue ItemLoop
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
2023-11-26 15:24:17 +00:00
|
|
|
|
|
|
|
// This item will be visible.
|
|
|
|
items = append(items, item)
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// How many rows and columns do we have?
|
|
|
|
rows := len(g.rows)
|
|
|
|
columns := len(g.columns)
|
|
|
|
for _, item := range items {
|
|
|
|
rowEnd := item.Row + item.Height
|
|
|
|
if rowEnd > rows {
|
|
|
|
rows = rowEnd
|
|
|
|
}
|
|
|
|
columnEnd := item.Column + item.Width
|
|
|
|
if columnEnd > columns {
|
|
|
|
columns = columnEnd
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if rows == 0 || columns == 0 {
|
|
|
|
return // No content.
|
|
|
|
}
|
|
|
|
|
|
|
|
// Where are they located?
|
|
|
|
rowPos := make([]int, rows)
|
|
|
|
rowHeight := make([]int, rows)
|
|
|
|
columnPos := make([]int, columns)
|
|
|
|
columnWidth := make([]int, columns)
|
|
|
|
|
|
|
|
// How much space do we distribute?
|
|
|
|
remainingWidth := width
|
|
|
|
remainingHeight := height
|
|
|
|
proportionalWidth := 0
|
|
|
|
proportionalHeight := 0
|
|
|
|
for index, row := range g.rows {
|
|
|
|
if row > 0 {
|
|
|
|
if row < g.minHeight {
|
|
|
|
row = g.minHeight
|
|
|
|
}
|
|
|
|
remainingHeight -= row
|
|
|
|
rowHeight[index] = row
|
|
|
|
} else if row == 0 {
|
|
|
|
proportionalHeight++
|
|
|
|
} else {
|
|
|
|
proportionalHeight += -row
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for index, column := range g.columns {
|
|
|
|
if column > 0 {
|
|
|
|
if column < g.minWidth {
|
|
|
|
column = g.minWidth
|
|
|
|
}
|
|
|
|
remainingWidth -= column
|
|
|
|
columnWidth[index] = column
|
|
|
|
} else if column == 0 {
|
|
|
|
proportionalWidth++
|
|
|
|
} else {
|
|
|
|
proportionalWidth += -column
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if g.borders {
|
|
|
|
remainingHeight -= rows + 1
|
|
|
|
remainingWidth -= columns + 1
|
|
|
|
} else {
|
|
|
|
remainingHeight -= (rows - 1) * g.gapRows
|
|
|
|
remainingWidth -= (columns - 1) * g.gapColumns
|
|
|
|
}
|
|
|
|
if rows > len(g.rows) {
|
|
|
|
proportionalHeight += rows - len(g.rows)
|
|
|
|
}
|
|
|
|
if columns > len(g.columns) {
|
|
|
|
proportionalWidth += columns - len(g.columns)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Distribute proportional rows/columns.
|
|
|
|
for index := 0; index < rows; index++ {
|
|
|
|
row := 0
|
|
|
|
if index < len(g.rows) {
|
|
|
|
row = g.rows[index]
|
|
|
|
}
|
|
|
|
if row > 0 {
|
|
|
|
continue // Not proportional. We already know the width.
|
|
|
|
} else if row == 0 {
|
|
|
|
row = 1
|
|
|
|
} else {
|
|
|
|
row = -row
|
|
|
|
}
|
2018-02-20 10:19:30 +00:00
|
|
|
rowAbs := row * remainingHeight / proportionalHeight
|
|
|
|
remainingHeight -= rowAbs
|
|
|
|
proportionalHeight -= row
|
|
|
|
if rowAbs < g.minHeight {
|
|
|
|
rowAbs = g.minHeight
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
2018-02-20 10:19:30 +00:00
|
|
|
rowHeight[index] = rowAbs
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
|
|
|
for index := 0; index < columns; index++ {
|
|
|
|
column := 0
|
|
|
|
if index < len(g.columns) {
|
|
|
|
column = g.columns[index]
|
|
|
|
}
|
|
|
|
if column > 0 {
|
|
|
|
continue // Not proportional. We already know the height.
|
|
|
|
} else if column == 0 {
|
|
|
|
column = 1
|
|
|
|
} else {
|
|
|
|
column = -column
|
|
|
|
}
|
2018-02-20 10:19:30 +00:00
|
|
|
columnAbs := column * remainingWidth / proportionalWidth
|
|
|
|
remainingWidth -= columnAbs
|
|
|
|
proportionalWidth -= column
|
|
|
|
if columnAbs < g.minWidth {
|
|
|
|
columnAbs = g.minWidth
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
2018-02-20 10:19:30 +00:00
|
|
|
columnWidth[index] = columnAbs
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate row/column positions.
|
2019-07-17 16:46:45 +00:00
|
|
|
var columnX, rowY int
|
2018-02-19 19:23:01 +00:00
|
|
|
if g.borders {
|
|
|
|
columnX++
|
|
|
|
rowY++
|
|
|
|
}
|
|
|
|
for index, row := range rowHeight {
|
|
|
|
rowPos[index] = rowY
|
|
|
|
gap := g.gapRows
|
|
|
|
if g.borders {
|
|
|
|
gap = 1
|
|
|
|
}
|
|
|
|
rowY += row + gap
|
|
|
|
}
|
|
|
|
for index, column := range columnWidth {
|
|
|
|
columnPos[index] = columnX
|
|
|
|
gap := g.gapColumns
|
|
|
|
if g.borders {
|
|
|
|
gap = 1
|
|
|
|
}
|
|
|
|
columnX += column + gap
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate primitive positions.
|
2018-02-20 10:19:30 +00:00
|
|
|
var focus *gridItem // The item which has focus.
|
2023-11-26 15:24:17 +00:00
|
|
|
for _, item := range items {
|
2018-02-19 19:23:01 +00:00
|
|
|
px := columnPos[item.Column]
|
|
|
|
py := rowPos[item.Row]
|
|
|
|
var pw, ph int
|
|
|
|
for index := 0; index < item.Height; index++ {
|
|
|
|
ph += rowHeight[item.Row+index]
|
|
|
|
}
|
|
|
|
for index := 0; index < item.Width; index++ {
|
|
|
|
pw += columnWidth[item.Column+index]
|
|
|
|
}
|
|
|
|
if g.borders {
|
|
|
|
pw += item.Width - 1
|
|
|
|
ph += item.Height - 1
|
|
|
|
} else {
|
|
|
|
pw += (item.Width - 1) * g.gapColumns
|
|
|
|
ph += (item.Height - 1) * g.gapRows
|
|
|
|
}
|
|
|
|
item.x, item.y, item.w, item.h = px, py, pw, ph
|
|
|
|
item.visible = true
|
2023-11-26 15:24:17 +00:00
|
|
|
if item.Item.HasFocus() {
|
2018-02-19 19:23:01 +00:00
|
|
|
focus = item
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate screen offsets.
|
2019-07-17 16:46:45 +00:00
|
|
|
var offsetX, offsetY int
|
|
|
|
add := 1
|
|
|
|
if !g.borders {
|
|
|
|
add = g.gapRows
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
2019-07-17 16:46:45 +00:00
|
|
|
for index, height := range rowHeight {
|
|
|
|
if index >= g.rowOffset {
|
2018-02-20 10:19:30 +00:00
|
|
|
break
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
2019-07-17 16:46:45 +00:00
|
|
|
offsetY += height + add
|
|
|
|
}
|
|
|
|
if !g.borders {
|
|
|
|
add = g.gapColumns
|
|
|
|
}
|
|
|
|
for index, width := range columnWidth {
|
|
|
|
if index >= g.columnOffset {
|
2018-02-20 10:19:30 +00:00
|
|
|
break
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
2019-07-17 16:46:45 +00:00
|
|
|
offsetX += width + add
|
|
|
|
}
|
|
|
|
|
|
|
|
// The focused item must be within the visible area.
|
|
|
|
if focus != nil {
|
|
|
|
if focus.y+focus.h-offsetY >= height {
|
|
|
|
offsetY = focus.y - height + focus.h
|
|
|
|
}
|
|
|
|
if focus.y-offsetY < 0 {
|
|
|
|
offsetY = focus.y
|
|
|
|
}
|
|
|
|
if focus.x+focus.w-offsetX >= width {
|
|
|
|
offsetX = focus.x - width + focus.w
|
|
|
|
}
|
|
|
|
if focus.x-offsetX < 0 {
|
|
|
|
offsetX = focus.x
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Adjust row/column offsets based on this value.
|
|
|
|
var from, to int
|
|
|
|
for index, pos := range rowPos {
|
|
|
|
if pos-offsetY < 0 {
|
|
|
|
from = index + 1
|
|
|
|
}
|
|
|
|
if pos-offsetY < height {
|
|
|
|
to = index
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if g.rowOffset < from {
|
|
|
|
g.rowOffset = from
|
|
|
|
}
|
|
|
|
if g.rowOffset > to {
|
|
|
|
g.rowOffset = to
|
|
|
|
}
|
|
|
|
from, to = 0, 0
|
|
|
|
for index, pos := range columnPos {
|
|
|
|
if pos-offsetX < 0 {
|
|
|
|
from = index + 1
|
|
|
|
}
|
|
|
|
if pos-offsetX < width {
|
|
|
|
to = index
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if g.columnOffset < from {
|
|
|
|
g.columnOffset = from
|
|
|
|
}
|
|
|
|
if g.columnOffset > to {
|
|
|
|
g.columnOffset = to
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
|
|
|
|
2018-02-20 10:19:30 +00:00
|
|
|
// Draw primitives and borders.
|
2021-03-12 17:48:52 +00:00
|
|
|
borderStyle := tcell.StyleDefault.Background(g.backgroundColor).Foreground(g.bordersColor)
|
2023-11-26 15:24:17 +00:00
|
|
|
for _, item := range items {
|
2018-02-20 10:19:30 +00:00
|
|
|
// Final primitive position.
|
2018-02-19 19:23:01 +00:00
|
|
|
if !item.visible {
|
|
|
|
continue
|
|
|
|
}
|
2018-02-20 10:19:30 +00:00
|
|
|
item.x -= offsetX
|
|
|
|
item.y -= offsetY
|
2019-07-17 16:46:45 +00:00
|
|
|
if item.x >= width || item.x+item.w <= 0 || item.y >= height || item.y+item.h <= 0 {
|
|
|
|
item.visible = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if item.x+item.w > width {
|
2018-02-19 19:23:01 +00:00
|
|
|
item.w = width - item.x
|
|
|
|
}
|
2019-07-17 16:46:45 +00:00
|
|
|
if item.y+item.h > height {
|
2018-02-19 19:23:01 +00:00
|
|
|
item.h = height - item.y
|
|
|
|
}
|
2018-02-20 10:19:30 +00:00
|
|
|
if item.x < 0 {
|
|
|
|
item.w += item.x
|
|
|
|
item.x = 0
|
|
|
|
}
|
|
|
|
if item.y < 0 {
|
|
|
|
item.h += item.y
|
|
|
|
item.y = 0
|
|
|
|
}
|
2018-02-19 19:23:01 +00:00
|
|
|
if item.w <= 0 || item.h <= 0 {
|
|
|
|
item.visible = false
|
|
|
|
continue
|
|
|
|
}
|
2019-07-17 16:46:45 +00:00
|
|
|
item.x += x
|
|
|
|
item.y += y
|
2023-11-26 15:24:17 +00:00
|
|
|
item.Item.SetRect(item.x, item.y, item.w, item.h)
|
2018-02-20 10:19:30 +00:00
|
|
|
|
|
|
|
// Draw primitive.
|
2018-02-19 19:23:01 +00:00
|
|
|
if item == focus {
|
2023-11-26 15:24:17 +00:00
|
|
|
defer item.Item.Draw(screen)
|
2018-02-19 19:23:01 +00:00
|
|
|
} else {
|
2023-11-26 15:24:17 +00:00
|
|
|
item.Item.Draw(screen)
|
2018-02-19 19:23:01 +00:00
|
|
|
}
|
|
|
|
|
2018-02-20 10:19:30 +00:00
|
|
|
// Draw border around primitive.
|
|
|
|
if g.borders {
|
|
|
|
for bx := item.x; bx < item.x+item.w; bx++ { // Top/bottom lines.
|
2018-12-03 09:22:43 +00:00
|
|
|
if bx < 0 || bx >= screenWidth {
|
2018-02-20 10:19:30 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
by := item.y - 1
|
2018-12-03 09:22:43 +00:00
|
|
|
if by >= 0 && by < screenHeight {
|
2021-03-12 17:48:52 +00:00
|
|
|
PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, borderStyle)
|
2018-02-20 10:19:30 +00:00
|
|
|
}
|
|
|
|
by = item.y + item.h
|
2018-12-03 09:22:43 +00:00
|
|
|
if by >= 0 && by < screenHeight {
|
2021-03-12 17:48:52 +00:00
|
|
|
PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, borderStyle)
|
2018-02-20 10:19:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for by := item.y; by < item.y+item.h; by++ { // Left/right lines.
|
2018-12-03 09:22:43 +00:00
|
|
|
if by < 0 || by >= screenHeight {
|
2018-02-20 10:19:30 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
bx := item.x - 1
|
2018-12-03 09:22:43 +00:00
|
|
|
if bx >= 0 && bx < screenWidth {
|
2021-03-12 17:48:52 +00:00
|
|
|
PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, borderStyle)
|
2018-02-20 10:19:30 +00:00
|
|
|
}
|
|
|
|
bx = item.x + item.w
|
2018-12-03 09:22:43 +00:00
|
|
|
if bx >= 0 && bx < screenWidth {
|
2021-03-12 17:48:52 +00:00
|
|
|
PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, borderStyle)
|
2018-02-20 10:19:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
bx, by := item.x-1, item.y-1 // Top-left corner.
|
2018-12-03 09:22:43 +00:00
|
|
|
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
|
2021-03-12 17:48:52 +00:00
|
|
|
PrintJoinedSemigraphics(screen, bx, by, Borders.TopLeft, borderStyle)
|
2018-02-20 10:19:30 +00:00
|
|
|
}
|
|
|
|
bx, by = item.x+item.w, item.y-1 // Top-right corner.
|
2018-12-03 09:22:43 +00:00
|
|
|
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
|
2021-03-12 17:48:52 +00:00
|
|
|
PrintJoinedSemigraphics(screen, bx, by, Borders.TopRight, borderStyle)
|
2018-02-20 10:19:30 +00:00
|
|
|
}
|
|
|
|
bx, by = item.x-1, item.y+item.h // Bottom-left corner.
|
2018-12-03 09:22:43 +00:00
|
|
|
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
|
2021-03-12 17:48:52 +00:00
|
|
|
PrintJoinedSemigraphics(screen, bx, by, Borders.BottomLeft, borderStyle)
|
2018-02-20 10:19:30 +00:00
|
|
|
}
|
|
|
|
bx, by = item.x+item.w, item.y+item.h // Bottom-right corner.
|
2018-12-03 09:22:43 +00:00
|
|
|
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
|
2021-03-12 17:48:52 +00:00
|
|
|
PrintJoinedSemigraphics(screen, bx, by, Borders.BottomRight, borderStyle)
|
2018-02-20 10:19:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-02-16 07:37:25 +00:00
|
|
|
}
|
2019-11-04 05:34:46 +00:00
|
|
|
|
2020-01-24 20:40:34 +00:00
|
|
|
// MouseHandler returns the mouse handler for this primitive.
|
2020-02-14 02:09:09 +00:00
|
|
|
func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
|
|
|
return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
2020-01-24 20:40:34 +00:00
|
|
|
if !g.InRect(event.Position()) {
|
2020-02-14 02:09:09 +00:00
|
|
|
return false, nil
|
2020-01-24 20:40:34 +00:00
|
|
|
}
|
2020-03-27 17:41:44 +00:00
|
|
|
|
|
|
|
// Pass mouse events along to the first child item that takes it.
|
2020-01-24 20:40:34 +00:00
|
|
|
for _, item := range g.items {
|
2020-04-04 20:19:02 +00:00
|
|
|
if item.Item == nil {
|
|
|
|
continue
|
|
|
|
}
|
2020-02-14 02:09:09 +00:00
|
|
|
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
|
2020-01-24 20:40:34 +00:00
|
|
|
if consumed {
|
2020-03-27 17:41:44 +00:00
|
|
|
return
|
2020-01-24 20:40:34 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-27 17:41:44 +00:00
|
|
|
|
|
|
|
return
|
2020-01-24 20:40:34 +00:00
|
|
|
})
|
2019-11-04 05:34:46 +00:00
|
|
|
}
|
2024-02-04 15:11:39 +00:00
|
|
|
|
|
|
|
// InputHandler returns the handler for this primitive.
|
|
|
|
func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
|
|
|
|
return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
|
|
|
|
if !g.hasFocus {
|
|
|
|
// Pass event on to child primitive.
|
|
|
|
for _, item := range g.items {
|
|
|
|
if item != nil && item.Item.HasFocus() {
|
|
|
|
if handler := item.Item.InputHandler(); handler != nil {
|
|
|
|
handler(event, setFocus)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Process our own key events if we have direct focus.
|
|
|
|
switch event.Key() {
|
|
|
|
case tcell.KeyRune:
|
|
|
|
switch event.Rune() {
|
|
|
|
case 'g':
|
|
|
|
g.rowOffset, g.columnOffset = 0, 0
|
|
|
|
case 'G':
|
|
|
|
g.rowOffset = math.MaxInt32
|
|
|
|
case 'j':
|
|
|
|
g.rowOffset++
|
|
|
|
case 'k':
|
|
|
|
g.rowOffset--
|
|
|
|
case 'h':
|
|
|
|
g.columnOffset--
|
|
|
|
case 'l':
|
|
|
|
g.columnOffset++
|
|
|
|
}
|
|
|
|
case tcell.KeyHome:
|
|
|
|
g.rowOffset, g.columnOffset = 0, 0
|
|
|
|
case tcell.KeyEnd:
|
|
|
|
g.rowOffset = math.MaxInt32
|
|
|
|
case tcell.KeyUp:
|
|
|
|
g.rowOffset--
|
|
|
|
case tcell.KeyDown:
|
|
|
|
g.rowOffset++
|
|
|
|
case tcell.KeyLeft:
|
|
|
|
g.columnOffset--
|
|
|
|
case tcell.KeyRight:
|
|
|
|
g.columnOffset++
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// PasteHandler returns the handler for this primitive.
|
|
|
|
func (g *Grid) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
|
|
|
|
return g.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) {
|
|
|
|
for _, item := range g.items {
|
|
|
|
if item != nil && item.Item.HasFocus() {
|
|
|
|
if handler := item.Item.PasteHandler(); handler != nil {
|
|
|
|
handler(pastedText, setFocus)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|