ted/ui/grid.go
2017-08-26 09:48:22 +10:00

401 lines
10 KiB
Go

/**
* The grid component. This is used for displaying the model.
*/
package ui
import "strconv"
/**
* An abstract display model.
*/
type GridModel interface {
/**
* Returns the size of the grid model (width x height)
*/
Dimensions() (int, int)
/**
* Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
*/
ColWidth(int) int
/**
* Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
*/
RowHeight(int) int
/**
* Returns the value of the cell a position X, Y
*/
CellValue(int, int) string
}
type gridPoint int
/**
* The grid component.
*/
type Grid struct {
model GridModel // The grid model
viewCellX int // Left most cell
viewCellY int // Top most cell
selCellX int // The currently selected cell
selCellY int
cellsWide int // Measured number of cells. Recalculated on redraw.
cellsHigh int
}
/**
* Clipping rectangle
*/
type gridRect struct {
x1 gridPoint
y1 gridPoint
x2 gridPoint
y2 gridPoint
}
/**
* Creates a new gridRect from integers.
*/
func newGridRect(x1, y1, x2, y2 int) gridRect {
return gridRect{gridPoint(x1), gridPoint(y1), gridPoint(x2), gridPoint(y2)}
}
/**
* Creates a new grid.
*/
func NewGrid(model GridModel) *Grid {
return &Grid{model, 0, 0, 0, 0, -1, -1}
}
// Returns the model
func (grid *Grid) Model() GridModel {
return grid.model
}
// Sets the model
func (grid *Grid) SetModel(model GridModel) {
grid.model = model
}
/**
* Shifts the viewport of the grid.
*/
func (grid *Grid) ShiftBy(x int, y int) {
grid.viewCellX += x
grid.viewCellY += y
}
// Returns the display value of the currently selected cell.
func (grid *Grid) CurrentCellDisplayValue() string {
if grid.isCellValid(grid.selCellX, grid.selCellY) {
return grid.model.CellValue(grid.selCellX, grid.selCellY)
} else {
return ""
}
}
// Moves the currently selected cell by a delta. This will be implemented as single stepped
// moveTo calls to handle invalid cells.
func (grid *Grid) MoveBy(x int, y int) {
grid.MoveTo(grid.selCellX+x, grid.selCellY+y)
}
// Moves the currently selected cell to a specific row. The row must be valid, otherwise the
// currently selected cell will not be changed. Returns true if the move was successful
func (grid *Grid) MoveTo(newX, newY int) {
maxX, maxY := grid.model.Dimensions()
newX = intMinMax(newX, 0, maxX-1)
newY = intMinMax(newY, 0, maxY-1)
if grid.isCellValid(newX, newY) {
grid.selCellX = newX
grid.selCellY = newY
grid.reposition()
}
}
// Returns the currently selected cell position.
func (grid *Grid) CellPosition() (int, int) {
return grid.selCellX, grid.selCellY
}
// Returns true if the user can enter the specific cell
func (grid *Grid) isCellValid(x int, y int) bool {
maxX, maxY := grid.model.Dimensions()
return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY)
}
// Determine the topmost cell based on the location of the currently selected cell
func (grid *Grid) reposition() {
// If we have no measurement information, forget it.
if (grid.cellsWide == -1) || (grid.cellsHigh == -1) {
return
}
if grid.selCellX < grid.viewCellX {
grid.viewCellX = grid.selCellX
} else if grid.selCellX >= (grid.viewCellX + grid.cellsWide - 3) {
grid.viewCellX = grid.selCellX - (grid.cellsWide - 3)
}
if grid.selCellY < grid.viewCellY {
grid.viewCellY = grid.selCellY
} else if grid.selCellY >= (grid.viewCellY + grid.cellsHigh - 3) {
grid.viewCellY = grid.selCellY - (grid.cellsHigh - 3)
}
}
// Gets the cell value and attributes of a particular cell
func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) {
// The fixed cells
modelCellX := cellX - 1 + grid.viewCellX
modelCellY := cellY - 1 + grid.viewCellY
modelMaxX, modelMaxY := grid.model.Dimensions()
if (cellX == 0) && (cellY == 0) {
return "", AttrBold, AttrBold
} else if cellX == 0 {
if modelCellY == grid.selCellY {
return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrReverse
} else {
return strconv.Itoa(modelCellY), AttrBold, 0
}
} else if cellY == 0 {
if modelCellX == grid.selCellX {
return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrReverse
} else {
return strconv.Itoa(modelCellX), AttrBold, 0
}
} else {
// The data from the model
if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) {
if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) {
return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse
} else {
return grid.model.CellValue(modelCellX, modelCellY), 0, 0
}
} else {
return "~", ColorBlue | AttrBold, 0
}
}
}
// Gets the cell dimensions
func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) {
var cellWidth, cellHeight int
modelCellX := cellX - 1 + grid.viewCellX
modelCellY := cellY - 1 + grid.viewCellY
modelMaxX, modelMaxY := grid.model.Dimensions()
// Get the cell width & height from model (if within range)
if (modelCellX >= 0) && (modelCellX < modelMaxX) {
cellWidth = grid.model.ColWidth(modelCellX)
} else {
cellWidth = 8
}
if (modelCellY >= 0) && (modelCellY < modelMaxY) {
cellHeight = grid.model.RowHeight(modelCellY)
} else {
cellHeight = 2
}
if (cellX == 0) && (cellY == 0) {
return 8, 1
} else if cellX == 0 {
return 8, cellHeight
} else if cellY == 0 {
return cellWidth, 1
} else {
return cellWidth, cellHeight
}
// XXX: Workaround for bug in compiler
panic("Unreachable code")
return 0, 0
}
/**
* Renders a cell which contains text. The clip rectangle defines the size of the cell, as well as the left offset
* of the cell. The sx and sy determine the screen position of the cell top-left.
*/
func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy int, text string, fg, bg Attribute) {
for x := cellClipRect.x1; x <= cellClipRect.x2; x++ {
for y := cellClipRect.y1; y <= cellClipRect.y2; y++ {
currRune := ' '
if y == 0 {
textPos := int(x)
if textPos < len(text) {
currRune = rune(text[textPos])
}
}
// TODO: This might be better if this wasn't so low-level
ctx.DrawRuneWithAttrs(int(x-cellClipRect.x1)+sx, int(y-cellClipRect.y1)+sy, currRune, fg, bg)
}
}
}
// Renders a column. The viewport determines the maximum position of the rendered cell. CellX and CellY are the
// cell indicies to render, cellOffset are the LOCAL offset of the cell.
// This function will return the new X position (gridRect.x1 + colWidth)
func (grid *Grid) renderColumn(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (gridPoint, int) {
// The top-left position of the column
screenX := int(screenViewPort.x1)
screenY := int(screenViewPort.y1)
screenWidth := int(screenViewPort.x2 - screenViewPort.x1)
screenHeight := int(screenViewPort.y2 - screenViewPort.y1)
// Work out the column width and cap it if it will spill over the edge of the viewport
colWidth, _ := grid.getCellDimensions(cellX, cellY)
colWidth -= cellOffsetX
if colWidth > screenWidth {
colWidth = screenHeight
}
// The maximum
maxScreenY := screenY + screenHeight
cellsHigh := 0
for screenY < maxScreenY {
// Cap the row height if it will go beyond the edge of the viewport.
_, rowHeight := grid.getCellDimensions(cellX, cellY)
if screenY+rowHeight > maxScreenY {
rowHeight = maxScreenY - screenY
}
cellText, cellFg, cellBg := grid.getCellData(cellX, cellY)
grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth-cellOffsetX, rowHeight),
screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse
cellY++
cellsHigh++
screenY = screenY + rowHeight - cellOffsetY
cellOffsetY = 0
}
return gridPoint(screenX + colWidth), cellsHigh
}
// Renders the grid. Returns the number of cells in the X and Y direction were rendered.
//
func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (int, int) {
var cellsHigh = 0
var cellsWide = 0
for screenViewPort.x1 < screenViewPort.x2 {
screenViewPort.x1, cellsHigh = grid.renderColumn(ctx, screenViewPort, cellX, cellY, cellOffsetX, cellOffsetY)
cellX = cellX + 1
cellsWide++
cellOffsetX = 0
}
return cellsWide, cellsHigh
}
/**
* Returns the cell of the particular point, along with the top-left position of the cell.
*/
func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, posY int) {
var wid, hei int = grid.model.Dimensions()
posX = 0
posY = 0
cellX = -1
cellY = -1
// Go through columns to locate the particular cellX
for cx := 0; cx < wid; cx++ {
if (x >= posX) && (x < posX+grid.model.ColWidth(cx)) {
// We found the X position
cellX = int(cx)
break
}
}
for cy := 0; cy < hei; cy++ {
if (y >= posY) && (y < posY+grid.model.RowHeight(cy)) {
// And the Y position
cellY = int(cy)
break
}
}
return
}
/**
* Returns the requested dimensions of a grid (as required by UiComponent)
*/
func (grid *Grid) Remeasure(w, h int) (int, int) {
return w, h
}
/**
* Redraws the grid.
*/
func (grid *Grid) Redraw(ctx *DrawContext) {
viewportRect := newGridRect(0, 0, ctx.W, ctx.H)
grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0)
}
// Called when the component has focus and a key has been pressed.
// This is the default behaviour of the grid, but it is not used by the main grid.
func (grid *Grid) KeyPressed(key rune, mod int) {
// TODO: Not sure if this would be better handled using commands
if (key == 'i') || (key == KeyArrowUp) {
grid.MoveBy(0, -1)
} else if (key == 'k') || (key == KeyArrowDown) {
grid.MoveBy(0, 1)
} else if (key == 'j') || (key == KeyArrowLeft) {
grid.MoveBy(-1, 0)
} else if (key == 'l') || (key == KeyArrowRight) {
grid.MoveBy(1, 0)
}
}
// --------------------------------------------------------------------------------------------
// Test Model
type TestModel struct {
thing int
}
/**
* Returns the size of the grid model (width x height)
*/
func (model *TestModel) Dimensions() (int, int) {
return 100, 100
}
/**
* Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
*/
func (model *TestModel) ColWidth(int) int {
return 16
}
/**
* Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
*/
func (model *TestModel) RowHeight(int) int {
return 1
}
/**
* Returns the value of the cell a position X, Y
*/
func (model *TestModel) CellValue(x int, y int) string {
return strconv.Itoa(x) + "," + strconv.Itoa(y)
}