package tview import ( "math" "maunium.net/go/tcell" ) // 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 // 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. // // 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 // SetRows()/SetColumns() for details. 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 // The color of the borders around grid items. bordersColor tcell.Color } // NewGrid returns a new grid-based layout container with no initial primitives. func NewGrid() *Grid { g := &Grid{ Box: NewBox(), bordersColor: Styles.GraphicsColor, } g.focus = g return g } // SetRows defines how the rows of the grid are distributed. Each value defines // the size of one row, starting with the leftmost row. Values greater 0 // represent absolute row widths (gaps not included). Values less or equal 0 // represent proportional row widths or fractions of the remaining free space, // where 0 is treated the same as -1. That is, a row with a value of -3 will // have three times the width of a row with a value of -1 (or 0). The minimum // width set with SetMinSize() is always observed. // // Primitives may extend beyond the rows defined explicitly with this function. // A value of 0 is assumed for any undefined row. In fact, if you never call // this function, all rows occupied by primitives will have the same width. // On the other hand, unoccupied rows defined with this function will always // take their place. // // Assuming a total width of the grid of 100 cells and a minimum width of 0, the // following call will result in rows with widths of 30, 10, 15, 15, and 30 // cells: // // grid.SetRows(30, 10, -1, -1, -2) // // If a primitive were then placed in the 6th and 7th row, the resulting widths // would be: 30, 10, 10, 10, 20, 10, and 10 cells. // // If you then called SetMinSize() as follows: // // grid.SetMinSize(15, 20) // // 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. func (g *Grid) SetRows(rows ...int) *Grid { g.rows = rows return g } // SetColumns defines how the columns of the grid are distributed. These values // behave the same as the row values provided with SetRows(), see there for // a definition and examples. // // The provided values correspond to column heights, the first value defining // the height of the topmost column. func (g *Grid) SetColumns(columns ...int) *Grid { g.columns = columns return g } // SetSize is a shortcut for SetRows() and SetColumns() where all row and column // values are set to the given size values. See SetRows() for details on sizes. func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) *Grid { g.rows = make([]int, numRows) for index := range g.rows { g.rows[index] = rowSize } g.columns = make([]int, numColumns) for index := range g.columns { g.columns[index] = columnSize } return g } // SetMinSize sets an absolute minimum width for rows and an absolute minimum // height for columns. Panics if negative values are provided. func (g *Grid) SetMinSize(row, column int) *Grid { if row < 0 || column < 0 { panic("Invalid minimum row/column size") } g.minHeight, g.minWidth = row, column 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 // of 1 is assumed. Panics if negative values are provided. func (g *Grid) SetGap(row, column int) *Grid { if row < 0 || column < 0 { panic("Invalid gap size") } 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 } // SetBordersColor sets the color of the item borders. func (g *Grid) SetBordersColor(color tcell.Color) *Grid { g.bordersColor = color return g } // 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 // the given row and column and will span "width" rows and "height" columns. For // example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6: // // grid.AddItem(p, 2, 4, 3, 2, true) // // If width or height is 0, the primitive will not be drawn. // // 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 // primitive apply, the one that has at least one highest minimum value will be // used, or the primitive added last if those values are the same. Example: // // 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. // // 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. func (g *Grid) AddItem(p Primitive, row, column, height, width, minGridHeight, minGridWidth int, focus bool) *Grid { g.items = append(g.items, &gridItem{ Item: p, Row: row, Column: column, Height: height, Width: width, MinGridHeight: minGridHeight, MinGridWidth: minGridWidth, Focus: focus, }) return g } // 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 } // Clear removes all items from the grid. func (g *Grid) Clear() *Grid { g.items = nil 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 // that contained primitives that have focus are visible. 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 { if item.Focus { delegate(item.Item) return } } g.hasFocus = true } // Blur is called when this primitive loses focus. func (g *Grid) Blur() { g.hasFocus = false } // HasFocus returns whether or not this primitive has focus. func (g *Grid) HasFocus() bool { for _, item := range g.items { if item.visible && item.Item.GetFocusable().HasFocus() { return true } } return g.hasFocus } // 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)) { 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++ } }) } // Draw draws this primitive onto the screen. func (g *Grid) Draw(screen tcell.Screen) { g.Box.Draw(screen) x, y, width, height := g.GetInnerRect() // Make a list of items which apply. items := make(map[Primitive]*gridItem) for _, item := range g.items { item.visible = false if item.Width <= 0 || item.Height <= 0 || width < item.MinGridWidth || height < item.MinGridHeight { continue } previousItem, ok := items[item.Item] if ok && item.Width < previousItem.Width && item.Height < previousItem.Height { continue } items[item.Item] = item } // 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. gridWidth := 0 gridHeight := 0 for index := 0; index < rows; index++ { row := 0 if index < len(g.rows) { row = g.rows[index] } if row > 0 { if row < g.minHeight { row = g.minHeight } gridHeight += row continue // Not proportional. We already know the width. } else if row == 0 { row = 1 } else { row = -row } rowAbs := row * remainingHeight / proportionalHeight remainingHeight -= rowAbs proportionalHeight -= row if rowAbs < g.minHeight { rowAbs = g.minHeight } rowHeight[index] = rowAbs gridHeight += rowAbs } for index := 0; index < columns; index++ { column := 0 if index < len(g.columns) { column = g.columns[index] } if column > 0 { if column < g.minWidth { column = g.minWidth } gridWidth += column continue // Not proportional. We already know the height. } else if column == 0 { column = 1 } else { column = -column } columnAbs := column * remainingWidth / proportionalWidth remainingWidth -= columnAbs proportionalWidth -= column if columnAbs < g.minWidth { columnAbs = g.minWidth } columnWidth[index] = columnAbs gridWidth += columnAbs } if g.borders { gridHeight += rows + 1 gridWidth += columns + 1 } else { gridHeight += (rows - 1) * g.gapRows gridWidth += (columns - 1) * g.gapColumns } // Calculate row/column positions. columnX, rowY := x, y 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. var focus *gridItem // The item which has focus. for primitive, item := range items { 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 if primitive.GetFocusable().HasFocus() { focus = item } } // Calculate screen offsets. var offsetX, offsetY, add int if g.rowOffset < 0 { g.rowOffset = 0 } if g.columnOffset < 0 { g.columnOffset = 0 } if g.borders { add = 1 } for row := 0; row < rows-1; row++ { remainingHeight := gridHeight - offsetY if focus != nil && focus.y-add <= offsetY || // Don't let the focused item move out of screen. row >= g.rowOffset && (focus == nil || focus != nil && focus.y-offsetY < height) || // We've reached the requested offset. remainingHeight <= height { // We have enough space to show the rest. if row > 0 { if focus != nil && focus.y+focus.h+add-offsetY > height { offsetY += focus.y + focus.h + add - offsetY - height } if remainingHeight < height { offsetY = gridHeight - height } } g.rowOffset = row break } offsetY = rowPos[row+1] - add } for column := 0; column < columns-1; column++ { remainingWidth := gridWidth - offsetX if focus != nil && focus.x-add <= offsetX || // Don't let the focused item move out of screen. column >= g.columnOffset && (focus == nil || focus != nil && focus.x-offsetX < width) || // We've reached the requested offset. remainingWidth <= width { // We have enough space to show the rest. if column > 0 { if focus != nil && focus.x+focus.w+add-offsetX > width { offsetX += focus.x + focus.w + add - offsetX - width } else if remainingWidth < width { offsetX = gridWidth - width } } g.columnOffset = column break } offsetX = columnPos[column+1] - add } // Draw primitives and borders. for primitive, item := range items { // Final primitive position. if !item.visible { continue } item.x -= offsetX item.y -= offsetY if item.x+item.w > width { item.w = width - item.x } if item.y+item.h > height { item.h = height - item.y } if item.x < 0 { item.w += item.x item.x = 0 } if item.y < 0 { item.h += item.y item.y = 0 } if item.w <= 0 || item.h <= 0 { item.visible = false continue } primitive.SetRect(x+item.x, y+item.y, item.w, item.h) // Draw primitive. if item == focus { defer primitive.Draw(screen) } else { primitive.Draw(screen) } // Draw border around primitive. if g.borders { for bx := item.x; bx < item.x+item.w; bx++ { // Top/bottom lines. if bx < 0 || bx >= width { continue } by := item.y - 1 if by >= 0 && by < height { PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor) } by = item.y + item.h if by >= 0 && by < height { PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor) } } for by := item.y; by < item.y+item.h; by++ { // Left/right lines. if by < 0 || by >= height { continue } bx := item.x - 1 if bx >= 0 && bx < width { PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor) } bx = item.x + item.w if bx >= 0 && bx < width { PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor) } } bx, by := item.x-1, item.y-1 // Top-left corner. if bx >= 0 && bx < width && by >= 0 && by < height { PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopLeftCorner, g.bordersColor) } bx, by = item.x+item.w, item.y-1 // Top-right corner. if bx >= 0 && bx < width && by >= 0 && by < height { PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopRightCorner, g.bordersColor) } bx, by = item.x-1, item.y+item.h // Bottom-left corner. if bx >= 0 && bx < width && by >= 0 && by < height { PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomLeftCorner, g.bordersColor) } bx, by = item.x+item.w, item.y+item.h // Bottom-right corner. if bx >= 0 && bx < width && by >= 0 && by < height { PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomRightCorner, g.bordersColor) } } } }