Progressing with Grid.

This commit is contained in:
Oliver 2018-02-19 20:23:01 +01:00
parent 3625d3b928
commit c7b3072f7e
4 changed files with 332 additions and 9 deletions

View File

@ -64,6 +64,8 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio
(There are no corresponding tags in the project. I only keep such a history in this README.)
- v0.9 (2018-02-21)
- Introduced `Grid` layout.
- v0.8 (2018-01-17)
- Color tags can now be used almost everywhere.
- v0.7 (2018-01-16)

4
box.go
View File

@ -77,8 +77,8 @@ func (b *Box) GetRect() (int, int, int, int) {
return b.x, b.y, b.width, b.height
}
// GetInnerRect returns the position of the inner rectangle, without the border
// and without any padding.
// GetInnerRect returns the position of the inner rectangle (x, y, width,
// height), without the border and without any padding.
func (b *Box) GetInnerRect() (int, int, int, int) {
x, y, width, height := b.GetRect()
if b.border {

14
demos/grid/main.go Normal file
View File

@ -0,0 +1,14 @@
package main
import "github.com/rivo/tview"
func main() {
grid := tview.NewGrid().
AddItem(tview.NewBox().SetBorder(true).SetTitle("Top"), 0, 0, 1, 2, 0, 0, false).
AddItem(tview.NewBox().SetBorder(true).SetTitle("Left"), 1, 0, 1, 1, 0, 0, true).
AddItem(tview.NewBox().SetBorder(true).SetTitle("Right"), 1, 1, 1, 1, 0, 0, false).
AddItem(tview.NewBox().SetBorder(true).SetTitle("Bottom"), 2, 0, 1, 2, 0, 0, false)
if err := tview.NewApplication().SetRoot(grid, true).SetFocus(grid).Run(); err != nil {
panic(err)
}
}

321
grid.go
View File

@ -108,17 +108,32 @@ func (g *Grid) SetColumns(columns ...int) *Grid {
return g
}
// SetSize is a shortcut for SetRows() and SetColumns() where all row and column
// values are set to a value of 0. The cells of the resulting grid will
// therefore be evenly distributed.
func (g *Grid) SetSize(rows, columns int) *Grid {
g.rows = make([]int, rows)
g.columns = make([]int, columns)
return g
}
// SetMinSize sets an absolute minimum width for rows and an absolute minimum
// height for columns.
// 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.minWidth, g.minHeight = 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.
// 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
}
@ -144,9 +159,8 @@ func (g *Grid) SetBorders(borders bool) *Grid {
// 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 with the highest minimum values (with a preference
// for the minimum width) will be used, or the primitive added last if those
// values are the same. Example:
// 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.
@ -158,7 +172,23 @@ func (g *Grid) SetBorders(borders bool) *Grid {
// 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, width, height, minGridWidth, minGridHeight int, focus bool) *Grid {
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
}
// Clear removes all items from the grid.
func (g *Grid) Clear() *Grid {
g.items = nil
return g
}
@ -242,5 +272,282 @@ func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
// Draw draws this primitive onto the screen.
func (g *Grid) Draw(screen tcell.Screen) {
g.Box.Draw(screen)
//TODO
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
}
row = row * remainingHeight / proportionalHeight
if row < g.minHeight {
row = g.minHeight
}
rowHeight[index] = row
gridHeight += row
}
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
}
column = column * remainingWidth / proportionalWidth
if column < g.minWidth {
column = g.minWidth
}
columnWidth[index] = column
gridWidth += column
}
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.
rightmost *gridItem // The rightmost item.
lowest *gridItem // The bottom item.
rightBorder, bottomBorder int
)
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
}
if px+pw > rightBorder {
rightmost = item
rightBorder = px + pw
}
if py+ph > bottomBorder {
lowest = item
bottomBorder = py + ph
}
}
// Calculate screen offsets.
var offsetX, offsetY int
if g.rowOffset >= rows {
g.rowOffset = rows - 1
} else if g.rowOffset < 0 {
g.rowOffset = 0
}
if g.columnOffset >= columns {
g.columnOffset = columns - 1
} else if g.columnOffset < 0 {
g.columnOffset = 0
}
add := 0
if g.borders {
add = 1
}
if gridHeight > height && g.rowOffset > 0 {
offsetY = -rowPos[g.rowOffset]
if focus != nil {
if offsetY+focus.y+focus.h+add > height {
offsetY -= offsetY + focus.y + focus.h + add - height
}
if offsetY+focus.y-add < 0 {
offsetY -= offsetY + focus.y - add
}
}
if lowest != nil {
if offsetY+lowest.y+lowest.h+add > height {
offsetY -= offsetY + lowest.y + lowest.h + add - height
}
}
}
if gridWidth > width && g.columnOffset > 0 {
offsetX = -columnPos[g.columnOffset]
if focus != nil {
if offsetX+focus.x+focus.w+add > width {
offsetX -= offsetX + focus.x + focus.w + add - width
}
if offsetX+focus.x-add < 0 {
offsetX -= offsetX + focus.x - add
}
}
if rightmost != nil {
if offsetX+rightmost.x+rightmost.w+add > width {
offsetX -= offsetX + rightmost.x + rightmost.w + add - width
}
}
}
// Draw primitives.
for primitive, item := range items {
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.w <= 0 || item.h <= 0 {
item.visible = false
continue
}
primitive.SetRect(x+item.x, y+item.y, item.w, item.h)
if item == focus {
defer primitive.Draw(screen)
} else {
primitive.Draw(screen)
}
}
// Draw borders.
}