package cview import ( "math" "sync" "github.com/gdamore/tcell/v2" ) // 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. 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 sync.RWMutex } // NewGrid returns a new grid-based layout container with no initial primitives. // // Note that Grid will have a transparent background by default so that any // areas not covered by any primitives will show primitives behind the Grid. // To disable this transparency: // // grid.SetBackgroundTransparent(false) func NewGrid() *Grid { g := &Grid{ Box: NewBox(), bordersColor: Styles.GraphicsColor, } g.SetBackgroundTransparent(true) g.focus = g return g } // SetColumns defines how the columns of the grid are distributed. Each value // defines the size of one column, starting with the leftmost column. Values // greater 0 represent absolute column widths (gaps not included). Values less // or equal 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. // // 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. // // Assuming a total width of the grid of 100 cells and a minimum width of 0, the // following call will result in columns with widths of 30, 10, 15, 15, and 30 // cells: // // grid.Setcolumns(30, 10, -1, -1, -2) // // 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. // // 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) SetColumns(columns ...int) { g.Lock() defer g.Unlock() g.columns = columns } // SetRows defines how the rows of the grid are distributed. These values behave // the same as the column values provided with SetColumns(), see there for a // definition and examples. // // The provided values correspond to row heights, the first value defining // the height of the topmost row. func (g *Grid) SetRows(rows ...int) { g.Lock() defer g.Unlock() g.rows = rows } // SetSize is a shortcut for SetRows() and SetColumns() where all row and column // values are set to the given size values. See SetColumns() for details on sizes. func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) { g.Lock() defer g.Unlock() 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 } } // 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) { g.Lock() defer g.Unlock() if row < 0 || column < 0 { panic("Invalid minimum row/column size") } g.minHeight, g.minWidth = row, column } // 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) { g.Lock() defer g.Unlock() if row < 0 || column < 0 { panic("Invalid gap size") } g.gapRows, g.gapColumns = row, column } // 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) { g.Lock() defer g.Unlock() g.borders = borders } // SetBordersColor sets the color of the item borders. func (g *Grid) SetBordersColor(color tcell.Color) { g.Lock() defer g.Unlock() g.bordersColor = color } // 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 "rowSpan" rows and "colSpan" columns. // For example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6: // // grid.AddItem(p, 2, 5, 3, 2, 0, 0, true) // // If rowSpan or colSpan 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, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) { g.Lock() defer g.Unlock() g.items = append(g.items, &gridItem{ Item: p, Row: row, Column: column, Height: rowSpan, Width: colSpan, MinGridHeight: minGridHeight, MinGridWidth: minGridWidth, Focus: focus, }) } // 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) { g.Lock() defer g.Unlock() 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:]...) } } } // Clear removes all items from the grid. func (g *Grid) Clear() { g.Lock() defer g.Unlock() g.items = nil } // 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 remain visible. func (g *Grid) SetOffset(rows, columns int) { g.Lock() defer g.Unlock() g.rowOffset, g.columnOffset = rows, columns } // GetOffset returns the current row and column offset (see SetOffset() for // details). func (g *Grid) GetOffset() (rows, columns int) { g.RLock() defer g.RUnlock() return g.rowOffset, g.columnOffset } // Focus is called when this primitive receives focus. func (g *Grid) Focus(delegate func(p Primitive)) { g.Lock() items := g.items g.Unlock() for _, item := range items { if item.Focus { delegate(item.Item) return } } g.Lock() g.hasFocus = true g.Unlock() } // Blur is called when this primitive loses focus. func (g *Grid) Blur() { g.Lock() defer g.Unlock() g.hasFocus = false } // HasFocus returns whether or not this primitive has focus. func (g *Grid) HasFocus() bool { g.RLock() defer g.RUnlock() 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)) { g.Lock() defer g.Unlock() if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) { g.rowOffset, g.columnOffset = 0, 0 } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) { g.rowOffset = math.MaxInt32 } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) { g.rowOffset-- } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) { g.rowOffset++ } else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) { g.columnOffset-- } else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) { g.columnOffset++ } }) } // Draw draws this primitive onto the screen. func (g *Grid) Draw(screen tcell.Screen) { if !g.GetVisible() { return } g.Box.Draw(screen) g.Lock() defer g.Unlock() x, y, width, height := g.GetInnerRect() screenWidth, screenHeight := screen.Size() // 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.MinGridWidth < previousItem.MinGridWidth && item.MinGridHeight < previousItem.MinGridHeight { 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. 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 } 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 } 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 } 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 } // Calculate row/column positions. var columnX, rowY int 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 int add := 1 if !g.borders { add = g.gapRows } for index, height := range rowHeight { if index >= g.rowOffset { break } offsetY += height + add } if !g.borders { add = g.gapColumns } for index, width := range columnWidth { if index >= g.columnOffset { break } offsetX += width + add } // Line up the last row/column with the end of the available area. var border int if g.borders { border = 1 } last := len(rowPos) - 1 if rowPos[last]+rowHeight[last]+border-offsetY < height { offsetY = rowPos[last] - height + rowHeight[last] + border } last = len(columnPos) - 1 if columnPos[last]+columnWidth[last]+border-offsetX < width { offsetX = columnPos[last] - width + columnWidth[last] + border } // 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 } // 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 >= width || item.x+item.w <= 0 || item.y >= height || item.y+item.h <= 0 { item.visible = false continue } 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 } item.x += x item.y += y primitive.SetRect(item.x, 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 >= screenWidth { continue } by := item.y - 1 if by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor) } by = item.y + item.h if by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor) } } for by := item.y; by < item.y+item.h; by++ { // Left/right lines. if by < 0 || by >= screenHeight { continue } bx := item.x - 1 if bx >= 0 && bx < screenWidth { PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor) } bx = item.x + item.w if bx >= 0 && bx < screenWidth { PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor) } } bx, by := item.x-1, item.y-1 // Top-left corner. if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.TopLeft, g.bordersColor) } bx, by = item.x+item.w, item.y-1 // Top-right corner. if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.TopRight, g.bordersColor) } bx, by = item.x-1, item.y+item.h // Bottom-left corner. if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.BottomLeft, g.bordersColor) } bx, by = item.x+item.w, item.y+item.h // Bottom-right corner. if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.BottomRight, g.bordersColor) } } } } // MouseHandler returns the mouse handler for this primitive. 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) { if !g.InRect(event.Position()) { return false, nil } // Pass mouse events along to the first child item that takes it. for _, item := range g.items { consumed, capture = item.Item.MouseHandler()(action, event, setFocus) if consumed { return } } return }) }