Finished implementation of grid layout.

pull/59/merge
Oliver 6 years ago
parent c7b3072f7e
commit 91a6ff44b6

@ -13,7 +13,7 @@ Among these components are:
- Navigable multi-color __text views__
- Sophisticated navigable __table views__
- Selectable __lists__
- __Flexbox__ and __page layouts__
- __Grid__, __Flexbox__ and __page layouts__
- Modal __message windows__
- An __application__ wrapper

@ -1,13 +1,36 @@
package main
import "github.com/rivo/tview"
import (
"github.com/rivo/tview"
)
func main() {
newPrimitive := func(text string) tview.Primitive {
return tview.NewTextView().
SetTextAlign(tview.AlignCenter).
SetText(text)
}
menu := newPrimitive("Menu")
main := newPrimitive("Main content")
sideBar := newPrimitive("Side Bar")
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)
SetRows(3, 0, 3).
SetColumns(30, 0, 30).
SetBorders(true).
AddItem(newPrimitive("Header"), 0, 0, 1, 3, 0, 0, false).
AddItem(newPrimitive("Footer"), 2, 0, 1, 3, 0, 0, false)
// Layout for screens narrower than 100 cells (menu and side bar are hidden).
grid.AddItem(menu, 0, 0, 0, 0, 0, 0, false).
AddItem(main, 1, 0, 1, 3, 0, 0, false).
AddItem(sideBar, 0, 0, 0, 0, 0, 0, false)
// Layout for screens wider than 100 cells.
grid.AddItem(menu, 1, 0, 1, 1, 0, 100, false).
AddItem(main, 1, 1, 1, 1, 0, 100, false).
AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)
if err := tview.NewApplication().SetRoot(grid, true).SetFocus(grid).Run(); err != nil {
panic(err)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@ -23,8 +23,9 @@ type gridItem struct {
//
// 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 while the grid has focus and none
// of its contained primitives do.
// 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 {
@ -52,12 +53,16 @@ type Grid struct {
// 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(),
Box: NewBox(),
bordersColor: Styles.GraphicsColor,
}
g.focus = g
return g
@ -123,7 +128,7 @@ 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
g.minHeight, g.minWidth = row, column
return g
}
@ -146,6 +151,12 @@ func (g *Grid) SetBorders(borders bool) *Grid {
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
@ -211,7 +222,7 @@ func (g *Grid) GetOffset() (rows, columns int) {
// Focus is called when this primitive receives focus.
func (g *Grid) Focus(delegate func(p Primitive)) {
for _, item := range g.items {
if item.visible && item.Focus {
if item.Focus {
delegate(item.Item)
return
}
@ -375,12 +386,14 @@ func (g *Grid) Draw(screen tcell.Screen) {
} else {
row = -row
}
row = row * remainingHeight / proportionalHeight
if row < g.minHeight {
row = g.minHeight
rowAbs := row * remainingHeight / proportionalHeight
remainingHeight -= rowAbs
proportionalHeight -= row
if rowAbs < g.minHeight {
rowAbs = g.minHeight
}
rowHeight[index] = row
gridHeight += row
rowHeight[index] = rowAbs
gridHeight += rowAbs
}
for index := 0; index < columns; index++ {
column := 0
@ -398,12 +411,14 @@ func (g *Grid) Draw(screen tcell.Screen) {
} else {
column = -column
}
column = column * remainingWidth / proportionalWidth
if column < g.minWidth {
column = g.minWidth
columnAbs := column * remainingWidth / proportionalWidth
remainingWidth -= columnAbs
proportionalWidth -= column
if columnAbs < g.minWidth {
columnAbs = g.minWidth
}
columnWidth[index] = column
gridWidth += column
columnWidth[index] = columnAbs
gridWidth += columnAbs
}
if g.borders {
gridHeight += rows + 1
@ -437,12 +452,7 @@ func (g *Grid) Draw(screen tcell.Screen) {
}
// Calculate primitive positions.
var (
focus *gridItem // The item which has focus.
rightmost *gridItem // The rightmost item.
lowest *gridItem // The bottom item.
rightBorder, bottomBorder int
)
var focus *gridItem // The item which has focus.
for primitive, item := range items {
px := columnPos[item.Column]
py := rowPos[item.Row]
@ -465,89 +475,134 @@ func (g *Grid) Draw(screen tcell.Screen) {
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 {
var offsetX, offsetY, add int
if g.rowOffset < 0 {
g.rowOffset = 0
}
if g.columnOffset >= columns {
g.columnOffset = columns - 1
} else if g.columnOffset < 0 {
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
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
}
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
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.
// Draw primitives and borders.
for primitive, item := range items {
// Final primitive position.
if !item.visible {
continue
}
item.x += offsetX
item.y += offsetY
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 borders.
// 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)
}
}
}
}

@ -2,6 +2,7 @@ package tview
import (
"bytes"
"fmt"
"regexp"
"sync"
"unicode/utf8"
@ -222,6 +223,14 @@ func (t *TextView) SetTextColor(color tcell.Color) *TextView {
return t
}
// SetText sets the text of this text view to the provided string. Previously
// contained text will be removed.
func (t *TextView) SetText(text string) *TextView {
t.Clear()
fmt.Fprint(t, text)
return t
}
// SetDynamicColors sets the flag that allows the text color to be changed
// dynamically. See class description for details.
func (t *TextView) SetDynamicColors(dynamic bool) *TextView {

@ -23,22 +23,84 @@ const (
GraphicsVertBar = '\u2502'
GraphicsTopLeftCorner = '\u250c'
GraphicsTopRightCorner = '\u2510'
GraphicsBottomRightCorner = '\u2518'
GraphicsBottomLeftCorner = '\u2514'
GraphicsBottomRightCorner = '\u2518'
GraphicsLeftT = '\u251c'
GraphicsRightT = '\u2524'
GraphicsTopT = '\u252c'
GraphicsBottomT = '\u2534'
GraphicsCross = '\u253c'
GraphicsDbVertBar = '\u2550'
GraphicsDbHorBar = '\u2551'
GraphicsDbTopLeftCorner = '\u2554'
GraphicsDbTopRightCorner = '\u2557'
GraphicsDbBottomRightCorner = '\u255d'
GraphicsDbBottomLeftCorner = '\u255a'
GraphicsRightT = '\u2524'
GraphicsLeftT = '\u251c'
GraphicsTopT = '\u252c'
GraphicsBottomT = '\u2534'
GraphicsCross = '\u253c'
GraphicsEllipsis = '\u2026'
)
// joints maps combinations of two graphical runes to the rune that results
// when joining the two in the same screen cell. The keys of this map are
// two-rune strings where the value of the first rune is lower than the value
// of the second rune. Identical runes are not contained.
var joints = map[string]rune{
"\u2500\u2502": GraphicsCross,
"\u2500\u250c": GraphicsTopT,
"\u2500\u2510": GraphicsTopT,
"\u2500\u2514": GraphicsBottomT,
"\u2500\u2518": GraphicsBottomT,
"\u2500\u251c": GraphicsCross,
"\u2500\u2524": GraphicsCross,
"\u2500\u252c": GraphicsTopT,
"\u2500\u2534": GraphicsBottomT,
"\u2500\u253c": GraphicsCross,
"\u2502\u250c": GraphicsLeftT,
"\u2502\u2510": GraphicsRightT,
"\u2502\u2514": GraphicsLeftT,
"\u2502\u2518": GraphicsRightT,
"\u2502\u251c": GraphicsLeftT,
"\u2502\u2524": GraphicsRightT,
"\u2502\u252c": GraphicsCross,
"\u2502\u2534": GraphicsCross,
"\u2502\u253c": GraphicsCross,
"\u250c\u2510": GraphicsTopT,
"\u250c\u2514": GraphicsLeftT,
"\u250c\u2518": GraphicsCross,
"\u250c\u251c": GraphicsLeftT,
"\u250c\u2524": GraphicsCross,
"\u250c\u252c": GraphicsTopT,
"\u250c\u2534": GraphicsCross,
"\u250c\u253c": GraphicsCross,
"\u2510\u2514": GraphicsCross,
"\u2510\u2518": GraphicsRightT,
"\u2510\u251c": GraphicsCross,
"\u2510\u2524": GraphicsRightT,
"\u2510\u252c": GraphicsTopT,
"\u2510\u2534": GraphicsCross,
"\u2510\u253c": GraphicsCross,
"\u2514\u2518": GraphicsBottomT,
"\u2514\u251c": GraphicsLeftT,
"\u2514\u2524": GraphicsCross,
"\u2514\u252c": GraphicsCross,
"\u2514\u2534": GraphicsBottomT,
"\u2514\u253c": GraphicsCross,
"\u2518\u251c": GraphicsCross,
"\u2518\u2524": GraphicsRightT,
"\u2518\u252c": GraphicsCross,
"\u2518\u2534": GraphicsBottomT,
"\u2518\u253c": GraphicsCross,
"\u251c\u2524": GraphicsCross,
"\u251c\u252c": GraphicsCross,
"\u251c\u2534": GraphicsCross,
"\u251c\u253c": GraphicsCross,
"\u2524\u252c": GraphicsCross,
"\u2524\u2534": GraphicsCross,
"\u2524\u253c": GraphicsCross,
"\u252c\u2534": GraphicsCross,
"\u252c\u253c": GraphicsCross,
"\u2534\u253c": GraphicsCross,
}
// Common regular expressions.
var (
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})\]`)
@ -344,3 +406,29 @@ func WordWrap(text string, width int) (lines []string) {
return
}
// PrintJoinedBorder prints a border graphics rune into the screen at the given
// position with the given color, joining it with any existing border graphics
// rune. Background colors are preserved. At this point, only regular single
// line borders are supported.
func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color) {
previous, _, style, _ := screen.GetContent(x, y)
style = style.Foreground(color)
// What's the resulting rune?
var result rune
if ch == previous {
result = ch
} else {
if ch < previous {
previous, ch = ch, previous
}
result = joints[string(previous)+string(ch)]
}
if result == 0 {
result = ch
}
// We only print something if we have something.
screen.SetContent(x, y, result, nil, style)
}

Loading…
Cancel
Save