mirror of
https://github.com/rivo/tview.git
synced 2024-11-07 03:20:39 +00:00
Finished implementation of grid layout.
This commit is contained in:
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)
|
||||
}
|
||||
|
BIN
demos/grid/screenshot.png
Normal file
BIN
demos/grid/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
187
grid.go
187
grid.go
@ -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(),
|
||||
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
|
||||
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 offsetY+focus.y-add < 0 {
|
||||
offsetY -= offsetY + focus.y - add
|
||||
if remainingHeight < height {
|
||||
offsetY = gridHeight - height
|
||||
}
|
||||
}
|
||||
if lowest != nil {
|
||||
if offsetY+lowest.y+lowest.h+add > height {
|
||||
offsetY -= offsetY + lowest.y + lowest.h + add - 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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
100
util.go
100
util.go
@ -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…
Reference in New Issue
Block a user