Have added prompting.

This commit is contained in:
Leon Mika 2017-08-26 09:48:22 +10:00
parent 665bc8cdae
commit 33847a78c1
10 changed files with 847 additions and 774 deletions

View file

@ -6,87 +6,90 @@ package ui
// drawing within it's context.
type DrawContext struct {
// The left and top position of the context.
X, Y int
// The left and top position of the context.
X, Y int
// The width and height position of the context.
W, H int
// The width and height position of the context.
W, H int
// The current driver
driver Driver
// The current driver
driver Driver
// The current foregound and background attributes
fa, ba Attribute
// The current foregound and background attributes
fa, ba Attribute
}
// Returns a new subcontext. The sub-context must be an area within the current context.
func (dc *DrawContext) NewSubContext(offsetX, offsetY, width, height int) *DrawContext {
return &DrawContext{
X: intMax(dc.X + offsetX, dc.X),
Y: intMax(dc.Y + offsetY, dc.Y),
W: intMax(width, 0),
H: intMax(height, 0),
return &DrawContext{
X: intMax(dc.X+offsetX, dc.X),
Y: intMax(dc.Y+offsetY, dc.Y),
W: intMax(width, 0),
H: intMax(height, 0),
driver: dc.driver,
}
driver: dc.driver,
}
}
// Sets the foreground attribute
func (dc *DrawContext) SetFgAttr(attr Attribute) {
dc.fa = attr
dc.fa = attr
}
// Sets the background attribute
func (dc *DrawContext) SetBgAttr(attr Attribute) {
dc.ba = attr
dc.ba = attr
}
// Draws a horizontal rule with a specific rune.
func (dc *DrawContext) HorizRule(y int, ch rune) {
for x := 0; x < dc.W; x++ {
dc.DrawRune(x, y, ch)
}
for x := 0; x < dc.W; x++ {
dc.DrawRune(x, y, ch)
}
}
// Prints a string at a specific offset. This will be bounded by the size of the drawing context.
func (dc *DrawContext) Print(x, y int, str string) {
for _, ch := range str {
dc.DrawRune(x, y, ch)
x++
}
for _, ch := range str {
dc.DrawRune(x, y, ch)
x++
}
}
// Prints a right-justified string at a specific offset. This will be bounded by the size of the drawing context.
func (dc *DrawContext) PrintRight(x, y int, str string) {
l := len(str)
dc.Print(x - l, y, str)
l := len(str)
dc.Print(x-l, y, str)
}
// Draws a rune at a local point X, Y with the current foreground and background attributes
func (dc *DrawContext) DrawRune(x, y int, ch rune) {
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCell(rx, ry, ch, dc.fa, dc.ba)
}
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCell(rx, ry, ch, dc.fa, dc.ba)
}
}
// Draws a rune at a local point X, Y with specific foreground and background attributes
func (dc *DrawContext) DrawRuneWithAttrs(x, y int, ch rune, fa, ba Attribute) {
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCell(rx, ry, ch, fa, ba)
}
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCell(rx, ry, ch, fa, ba)
}
}
// Set the position of the cursor
func (dc *DrawContext) SetCursorPosition(x, y int) {
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCursor(rx, ry)
}
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCursor(rx, ry)
}
}
// HideCursor hides the cursor
func (dc *DrawContext) HideCursor() {
dc.driver.HideCursor()
}
// Converts a local point to a real point. If the point is within the context, also returns true
func (dc *DrawContext) localPointToRealPoint(x, y int) (int, int, bool) {
rx, ry := x + dc.X, y + dc.Y
return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X + dc.W) && (ry < dc.Y + dc.H)
}
rx, ry := x+dc.X, y+dc.Y
return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X+dc.W) && (ry < dc.Y+dc.H)
}

View file

@ -3,140 +3,141 @@
package ui
// The set of attributes a specific cell can have
type Attribute uint16
type Attribute uint16
const (
// Can have only one of these
ColorDefault Attribute = iota
ColorBlack
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
// Can have only one of these
ColorDefault Attribute = iota
ColorBlack
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
)
// and zero or more of these (combined using OR '|')
const (
AttrBold Attribute = 1 << (iota + 9)
AttrUnderline
AttrReverse
AttrBold Attribute = 1 << (iota + 9)
AttrUnderline
AttrReverse
)
// Special keys
const (
KeyCtrlSpace rune = 0x8000 + iota
KeyCtrlA
KeyCtrlB
KeyCtrlC
KeyCtrlD
KeyCtrlE
KeyCtrlF
KeyCtrlG
KeyCtrlH
KeyCtrlI
KeyCtrlJ
KeyCtrlK
KeyCtrlL
KeyCtrlM
KeyCtrlN
KeyCtrlO
KeyCtrlP
KeyCtrlQ
KeyCtrlR
KeyCtrlS
KeyCtrlT
KeyCtrlU
KeyCtrlV
KeyCtrlW
KeyCtrlX
KeyCtrlY
KeyCtrlZ
KeyCtrl3
KeyCtrl4
KeyCtrl5
KeyCtrl6
KeyCtrl7
KeyCtrl8
KeyCtrlSpace rune = 0x8000 + iota
KeyCtrlA
KeyCtrlB
KeyCtrlC
KeyCtrlD
KeyCtrlE
KeyCtrlF
KeyCtrlG
KeyCtrlH
KeyCtrlI
KeyCtrlJ
KeyCtrlK
KeyCtrlL
KeyCtrlM
KeyCtrlN
KeyCtrlO
KeyCtrlP
KeyCtrlQ
KeyCtrlR
KeyCtrlS
KeyCtrlT
KeyCtrlU
KeyCtrlV
KeyCtrlW
KeyCtrlX
KeyCtrlY
KeyCtrlZ
KeyCtrl3
KeyCtrl4
KeyCtrl5
KeyCtrl6
KeyCtrl7
KeyCtrl8
KeyF1
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyInsert
KeyDelete
KeyHome
KeyEnd
KeyPgup
KeyPgdn
KeyArrowUp
KeyArrowDown
KeyArrowLeft
KeyArrowRight
KeyF1
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyInsert
KeyDelete
KeyHome
KeyEnd
KeyPgup
KeyPgdn
KeyArrowUp
KeyArrowDown
KeyArrowLeft
KeyArrowRight
KeyBackspace = KeyCtrlH
KeyBackspace2 = KeyCtrl8
KeyEnter = KeyCtrlM
KeyBackspace = KeyCtrlH
KeyBackspace2 = KeyCtrl8
KeyEnter = KeyCtrlM
)
// The type of events supported by the driver
type EventType int
type EventType int
const (
EventNone EventType = iota
EventNone EventType = iota
// Event when the window is resized
EventResize
// Event indicating a key press. The key is set in Ch and modifications
// are set in Or
EventKeyPress
// Event when the window is resized
EventResize
// Event indicating a key press. The key is set in Ch and modifications
// are set in Or
EventKeyPress
)
const (
ModKeyAlt int = (1 << iota)
ModKeyAlt int = (1 << iota)
)
// Data from an event callback.
type Event struct {
Type EventType
Par int
Ch rune
Type EventType
Par int
Ch rune
}
// The terminal driver interface.
type Driver interface {
// Initializes the driver. Returns an error if there was an error
Init() error
// Initializes the driver. Returns an error if there was an error
Init() error
// Closes the driver
Close()
// Closes the driver
Close()
// Returns the size of the window.
Size() (int, int)
// Returns the size of the window.
Size() (int, int)
// Sets the value of a specific cell
SetCell(x, y int, ch rune, fg, bg Attribute)
// Sets the value of a specific cell
SetCell(x, y int, ch rune, fg, bg Attribute)
// Synchronizes the internal buffer with the real buffer
Sync()
// Synchronizes the internal buffer with the real buffer
Sync()
// Wait for an event
WaitForEvent() Event
// Wait for an event
WaitForEvent() Event
// Move the position of the cursor
SetCursor(x, y int)
// Move the position of the cursor
SetCursor(x, y int)
// Hide the cursor
HideCursor()
}

View file

@ -5,407 +5,396 @@ 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 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 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 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
/**
* Returns the value of the cell a position X, Y
*/
CellValue(int, int) string
}
type gridPoint int
type gridPoint int
/**
* The grid component.
*/
type Grid struct {
model GridModel // The grid model
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
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
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)}
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}
return &Grid{model, 0, 0, 0, 0, -1, -1}
}
// Returns the model
func (grid *Grid) Model() GridModel {
return grid.model
return grid.model
}
// Sets the model
func (grid *Grid) SetModel(model GridModel) {
grid.model = model
grid.model = model
}
/**
* Shifts the viewport of the grid.
*/
func (grid *Grid) ShiftBy(x int, y int) {
grid.viewCellX += x
grid.viewCellY += y
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 ""
}
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
// 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)
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)
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()
}
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
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)
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 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.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)
}
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, AttrBold | AttrReverse
} else {
return strconv.Itoa(modelCellY), AttrBold, AttrBold
}
} else if (cellY == 0) {
if (modelCellX == grid.selCellX) {
return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrBold | AttrReverse
} else {
return strconv.Itoa(modelCellX), AttrBold, AttrBold
}
} 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
}
}
// 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
}
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)
}
}
}
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)
// 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
}
// 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
// The maximum
maxScreenY := screenY + screenHeight
cellsHigh := 0
for screenY < maxScreenY {
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)
// 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
}
grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth - cellOffsetX, rowHeight),
screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse
cellText, cellFg, cellBg := grid.getCellData(cellX, cellY)
cellY++
cellsHigh++
screenY = screenY + rowHeight - cellOffsetY
cellOffsetY = 0
}
grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth-cellOffsetX, rowHeight),
screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse
return gridPoint(screenX + colWidth), cellsHigh
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
}
var cellsHigh = 0
var cellsWide = 0
return cellsWide, cellsHigh
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
var wid, hei int = grid.model.Dimensions()
posX = 0
posY = 0
cellX = -1
cellY = -1
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
}
}
// 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
}
}
for cy := 0; cy < hei; cy++ {
if (y >= posY) && (y < posY+grid.model.RowHeight(cy)) {
// And the Y position
cellY = int(cy)
break
}
}
return
return
}
/**
* Returns the requested dimensions of a grid (as required by UiComponent)
*/
func (grid *Grid) Remeasure(w, h int) (int, int) {
return w, h
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)
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)
}
// 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
thing int
}
/**
* Returns the size of the grid model (width x height)
*/
func (model *TestModel) Dimensions() (int, int) {
return 100, 100
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
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
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)
return strconv.Itoa(x) + "," + strconv.Itoa(y)
}

View file

@ -3,84 +3,86 @@
package ui
// The UI manager
type Ui struct {
// The root component
rootComponent UiComponent
focusedComponent FocusableComponent
// The root component
rootComponent UiComponent
focusedComponent FocusableComponent
drawContext *DrawContext
driver Driver
drawContext *DrawContext
driver Driver
shutdown bool
}
// Creates a new UI context. This also initializes the UI state.
// Returns the context and an error.
func NewUI() (*Ui, error) {
driver := &TermboxDriver{}
err := driver.Init()
driver := &TermboxDriver{}
err := driver.Init()
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
drawContext := &DrawContext{ driver: driver }
ui := &Ui{ drawContext: drawContext, driver: driver }
drawContext := &DrawContext{driver: driver}
ui := &Ui{drawContext: drawContext, driver: driver}
return ui, nil
return ui, nil
}
// Closes the UI context.
func (ui *Ui) Close() {
ui.driver.Close()
ui.driver.Close()
}
// Sets the root component
func (ui *Ui) SetRootComponent(comp UiComponent) {
ui.rootComponent = comp
ui.Remeasure()
ui.rootComponent = comp
ui.Remeasure()
}
// Sets the focused component
func (ui *Ui) SetFocusedComponent(newFocused FocusableComponent) {
ui.focusedComponent = newFocused
ui.focusedComponent = newFocused
}
// Remeasures the UI
func (ui *Ui) Remeasure() {
ui.drawContext.X = 0
ui.drawContext.Y = 0
ui.drawContext.W, ui.drawContext.H = ui.driver.Size()
ui.drawContext.X = 0
ui.drawContext.Y = 0
ui.drawContext.W, ui.drawContext.H = ui.driver.Size()
ui.rootComponent.Remeasure(ui.drawContext.W, ui.drawContext.H)
ui.rootComponent.Remeasure(ui.drawContext.W, ui.drawContext.H)
}
// Redraws the UI.
func (ui *Ui) Redraw() {
ui.Remeasure()
ui.Remeasure()
ui.rootComponent.Redraw(ui.drawContext)
ui.driver.Sync()
ui.rootComponent.Redraw(ui.drawContext)
ui.driver.Sync()
}
// Quit indicates to the UI that it should shutdown
func (ui *Ui) Shutdown() {
ui.shutdown = true
}
// Enter the UI loop
func (ui *Ui) Loop() {
for {
ui.Redraw()
event := ui.driver.WaitForEvent()
for !ui.shutdown {
ui.Redraw()
event := ui.driver.WaitForEvent()
// TODO: If the event is a key-press, do something.
if event.Type == EventKeyPress {
if ui.focusedComponent != nil {
ui.focusedComponent.KeyPressed(event.Ch, event.Par)
}
} else if event.Type == EventResize {
// TODO: If the event is a key-press, do something.
if event.Type == EventKeyPress {
if ui.focusedComponent != nil {
ui.focusedComponent.KeyPressed(event.Ch, event.Par)
}
} else if event.Type == EventResize {
// HACK: Find another way to refresh the size of the screen to prevent a full redraw.
ui.driver.Sync()
}
}
}
// HACK: Find another way to refresh the size of the screen to prevent a full redraw.
ui.driver.Sync()
}
}
}

View file

@ -4,178 +4,187 @@ package ui
import "unicode"
// A text component. This simply renders a text string.
type TextView struct {
// The string to render
Text string
// The string to render
Text string
}
// Minimum dimensions
func (tv *TextView) Remeasure(w, h int) (int, int) {
return w, 1
return w, 1
}
// Status bar redraw
func (tv *TextView) Redraw(context *DrawContext) {
context.SetFgAttr(0)
context.SetBgAttr(0)
context.SetFgAttr(0)
context.SetBgAttr(0)
context.HorizRule(0, ' ')
context.Print(0, 0, tv.Text)
context.HorizRule(0, ' ')
context.Print(0, 0, tv.Text)
context.HideCursor()
}
// Status bar component. This component displays text on the left and right of it's
// allocated space.
type StatusBar struct {
Left string // Left aligned string
Right string // Right aligned string
Left string // Left aligned string
Right string // Right aligned string
}
// Minimum dimensions
func (sbar *StatusBar) Remeasure(w, h int) (int, int) {
return w, 1
return w, 1
}
// Status bar redraw
func (sbar *StatusBar) Redraw(context *DrawContext) {
context.SetFgAttr(AttrReverse)
context.SetBgAttr(AttrReverse)
context.SetFgAttr(AttrReverse)
context.SetBgAttr(AttrReverse)
context.HorizRule(0, ' ')
context.Print(0, 0, sbar.Left)
context.PrintRight(context.W, 0, sbar.Right)
context.HorizRule(0, ' ')
context.Print(0, 0, sbar.Left)
context.PrintRight(context.W, 0, sbar.Right)
}
// A single-text entry component.
type TextEntry struct {
Prompt string
Prompt string
value string
cursorOffset int
displayOffset int
value string
cursorOffset int
displayOffset int
// Called when the user presses Enter
OnEntry func(val string)
}
func (te *TextEntry) Remeasure(w, h int) (int, int) {
return w, 1
return w, 1
}
func (te *TextEntry) Redraw(context *DrawContext) {
context.HorizRule(0, ' ')
valueOffsetX := 0
displayOffsetX := te.calculateDisplayOffset(context.W)
context.HorizRule(0, ' ')
valueOffsetX := 0
displayOffsetX := te.calculateDisplayOffset(context.W)
if te.Prompt != "" {
context.SetFgAttr(ColorDefault | AttrBold)
context.Print(0, 0, te.Prompt)
context.SetFgAttr(ColorDefault)
if te.Prompt != "" {
context.SetFgAttr(ColorDefault | AttrBold)
context.Print(0, 0, te.Prompt)
context.SetFgAttr(ColorDefault)
valueOffsetX = len(te.Prompt)
}
valueOffsetX = len(te.Prompt)
}
context.Print(valueOffsetX, 0, te.value[displayOffsetX:intMin(displayOffsetX + context.W,len(te.value))])
context.SetCursorPosition(te.cursorOffset + valueOffsetX - displayOffsetX, 0)
context.Print(valueOffsetX, 0, te.value[displayOffsetX:intMin(displayOffsetX+context.W, len(te.value))])
context.SetCursorPosition(te.cursorOffset+valueOffsetX-displayOffsetX, 0)
//context.Print(0, 0, fmt.Sprintf("%d,%d", te.cursorOffset, displayOffsetX))
//context.Print(0, 0, fmt.Sprintf("%d,%d", te.cursorOffset, displayOffsetX))
}
func (te *TextEntry) calculateDisplayOffset(displayWidth int) int {
if te.Prompt != "" {
displayWidth -= len(te.Prompt)
}
virtualCursorOffset := te.cursorOffset - te.displayOffset
if te.Prompt != "" {
displayWidth -= len(te.Prompt)
}
virtualCursorOffset := te.cursorOffset - te.displayOffset
if virtualCursorOffset >= displayWidth {
te.displayOffset = te.cursorOffset - displayWidth + 10
} else if (virtualCursorOffset < 0) {
te.displayOffset = intMax(te.cursorOffset - displayWidth + 1, 0)
}
if virtualCursorOffset >= displayWidth {
te.displayOffset = te.cursorOffset - displayWidth + 10
} else if virtualCursorOffset < 0 {
te.displayOffset = intMax(te.cursorOffset-displayWidth+1, 0)
}
return te.displayOffset
return te.displayOffset
}
// SetValue sets the value of the text entry
func (te *TextEntry) SetValue(val string) {
te.value = val
te.cursorOffset = len(val)
}
func (te *TextEntry) KeyPressed(key rune, mod int) {
if (key >= ' ') && (key <= '~') {
te.insertRune(key)
} else if (key == KeyArrowLeft) {
te.moveCursorBy(-1)
} else if (key == KeyArrowRight) {
te.moveCursorBy(1)
} else if (key == KeyHome) {
te.moveCursorTo(0)
} else if (key == KeyEnd) {
te.moveCursorTo(len(te.value))
} else if (key == KeyBackspace) || (key == KeyBackspace2) {
if (mod & ModKeyAlt != 0) {
te.backspaceWhile(unicode.IsSpace)
te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) })
} else {
te.backspace()
}
} else if (key == KeyCtrlK) {
te.killLine()
} else if (key == KeyDelete) {
te.removeCharAtPos(te.cursorOffset)
} else if (key == KeyEnter) {
panic("Entered text: '" + te.value + "'")
}
if (key >= ' ') && (key <= '~') {
te.insertRune(key)
} else if key == KeyArrowLeft {
te.moveCursorBy(-1)
} else if key == KeyArrowRight {
te.moveCursorBy(1)
} else if key == KeyHome {
te.moveCursorTo(0)
} else if key == KeyEnd {
te.moveCursorTo(len(te.value))
} else if (key == KeyBackspace) || (key == KeyBackspace2) {
if mod&ModKeyAlt != 0 {
te.backspaceWhile(unicode.IsSpace)
te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) })
} else {
te.backspace()
}
} else if key == KeyCtrlK {
te.killLine()
} else if key == KeyDelete {
te.removeCharAtPos(te.cursorOffset)
} else if key == KeyEnter {
//panic("Entered text: '" + te.value + "'")
if te.OnEntry != nil {
te.OnEntry(te.value)
}
}
}
// Backspace
func (te *TextEntry) backspace() {
te.removeCharAtPos(te.cursorOffset - 1)
te.moveCursorBy(-1)
te.removeCharAtPos(te.cursorOffset - 1)
te.moveCursorBy(-1)
}
// Backspace while the character underneith the cursor matches the guard
func (te *TextEntry) backspaceWhile(guard func(r rune) bool) {
for (te.cursorOffset > 0) {
ch := rune(te.value[te.cursorOffset - 1])
if guard(ch) {
te.backspace()
} else {
break
}
}
for te.cursorOffset > 0 {
ch := rune(te.value[te.cursorOffset-1])
if guard(ch) {
te.backspace()
} else {
break
}
}
}
// Kill the line. If the cursor is at the end of the line, kill to the start.
// Otherwise, trim the line.
func (te *TextEntry) killLine() {
if (te.cursorOffset < len(te.value)) {
te.value = te.value[:te.cursorOffset]
} else {
te.value = ""
te.cursorOffset = 0
}
if te.cursorOffset < len(te.value) {
te.value = te.value[:te.cursorOffset]
} else {
te.value = ""
te.cursorOffset = 0
}
}
// Inserts a rune at the cursor position
func (te *TextEntry) insertRune(key rune) {
if (te.cursorOffset >= len(te.value)) {
te.value += string(key)
} else {
te.value = te.value[:te.cursorOffset] + string(key) + te.value[te.cursorOffset:]
}
te.moveCursorBy(1)
if te.cursorOffset >= len(te.value) {
te.value += string(key)
} else {
te.value = te.value[:te.cursorOffset] + string(key) + te.value[te.cursorOffset:]
}
te.moveCursorBy(1)
}
// Remove the character at a specific position
func (te *TextEntry) removeCharAtPos(pos int) {
if (pos >= 0) && (pos < len(te.value)) {
te.value = te.value[:pos] + te.value[pos+1:]
}
if (pos >= 0) && (pos < len(te.value)) {
te.value = te.value[:pos] + te.value[pos+1:]
}
}
// Move the cursor
func (te *TextEntry) moveCursorBy(byX int) {
te.moveCursorTo(te.cursorOffset + byX)
te.moveCursorTo(te.cursorOffset + byX)
}
func (te *TextEntry) moveCursorTo(toX int) {
te.cursorOffset = intMinMax(toX, 0, len(te.value))
}
te.cursorOffset = intMinMax(toX, 0, len(te.value))
}

View file

@ -3,134 +3,135 @@
package ui
import (
"github.com/nsf/termbox-go"
"github.com/nsf/termbox-go"
)
type TermboxDriver struct {
}
// Initializes the driver. Returns an error if there was an error
func (td *TermboxDriver) Init() error {
err := termbox.Init()
if err != nil {
return err
}
err := termbox.Init()
if err != nil {
return err
}
termbox.SetInputMode(termbox.InputAlt)
return nil
termbox.SetInputMode(termbox.InputAlt)
return nil
}
// Closes the driver
func (td *TermboxDriver) Close() {
termbox.Close()
termbox.Close()
}
// Returns the size of the window.
func (td *TermboxDriver) Size() (int, int) {
return termbox.Size()
return termbox.Size()
}
// Sets the value of a specific cell
func (td *TermboxDriver) SetCell(x, y int, ch rune, fg, bg Attribute) {
termbox.SetCell(x, y, ch, termbox.Attribute(fg), termbox.Attribute(bg))
termbox.SetCell(x, y, ch, termbox.Attribute(fg), termbox.Attribute(bg))
}
// Hide the cursor
func (td *TermboxDriver) HideCursor() {
termbox.HideCursor()
}
// Synchronizes the internal buffer with the real buffer
func (td *TermboxDriver) Sync() {
termbox.Flush()
termbox.Flush()
}
// Wait for an event
func (td *TermboxDriver) WaitForEvent() Event {
tev := termbox.PollEvent()
tev := termbox.PollEvent()
switch tev.Type {
case termbox.EventResize:
return Event{EventResize, 0, 0}
case termbox.EventKey:
mod := 0
if tev.Mod & termbox.ModAlt != 0 {
mod = ModKeyAlt
}
if tev.Ch != 0 {
return Event{EventKeyPress, mod, tev.Ch}
} else if spec, hasSpec := termboxKeysToSpecialKeys[tev.Key] ; hasSpec {
return Event{EventKeyPress, mod, spec}
} else {
return Event{EventNone, mod, 0}
}
default:
return Event{EventNone, 0, 0}
}
switch tev.Type {
case termbox.EventResize:
return Event{EventResize, 0, 0}
case termbox.EventKey:
mod := 0
if tev.Mod&termbox.ModAlt != 0 {
mod = ModKeyAlt
}
if tev.Ch != 0 {
return Event{EventKeyPress, mod, tev.Ch}
} else if spec, hasSpec := termboxKeysToSpecialKeys[tev.Key]; hasSpec {
return Event{EventKeyPress, mod, spec}
} else {
return Event{EventNone, mod, 0}
}
default:
return Event{EventNone, 0, 0}
}
}
// Move the position of the cursor
func (td *TermboxDriver) SetCursor(x, y int) {
termbox.SetCursor(x, y)
termbox.SetCursor(x, y)
}
// Map from termbox Keys to driver key runes
var termboxKeysToSpecialKeys = map[termbox.Key]rune {
termbox.KeySpace: ' ',
var termboxKeysToSpecialKeys = map[termbox.Key]rune{
termbox.KeySpace: ' ',
termbox.KeyF1: KeyF1,
termbox.KeyF2: KeyF2,
termbox.KeyF3: KeyF3,
termbox.KeyF4: KeyF4,
termbox.KeyF5: KeyF5,
termbox.KeyF6: KeyF6,
termbox.KeyF7: KeyF7,
termbox.KeyF8: KeyF8,
termbox.KeyF9: KeyF9,
termbox.KeyF10: KeyF10,
termbox.KeyF11: KeyF11,
termbox.KeyF12: KeyF12,
termbox.KeyInsert: KeyInsert,
termbox.KeyDelete: KeyDelete,
termbox.KeyHome: KeyHome,
termbox.KeyEnd: KeyEnd,
termbox.KeyPgup: KeyPgup,
termbox.KeyPgdn: KeyPgdn,
termbox.KeyArrowUp: KeyArrowUp,
termbox.KeyArrowDown: KeyArrowDown,
termbox.KeyArrowLeft: KeyArrowLeft,
termbox.KeyArrowRight: KeyArrowRight,
termbox.KeyF1: KeyF1,
termbox.KeyF2: KeyF2,
termbox.KeyF3: KeyF3,
termbox.KeyF4: KeyF4,
termbox.KeyF5: KeyF5,
termbox.KeyF6: KeyF6,
termbox.KeyF7: KeyF7,
termbox.KeyF8: KeyF8,
termbox.KeyF9: KeyF9,
termbox.KeyF10: KeyF10,
termbox.KeyF11: KeyF11,
termbox.KeyF12: KeyF12,
termbox.KeyInsert: KeyInsert,
termbox.KeyDelete: KeyDelete,
termbox.KeyHome: KeyHome,
termbox.KeyEnd: KeyEnd,
termbox.KeyPgup: KeyPgup,
termbox.KeyPgdn: KeyPgdn,
termbox.KeyArrowUp: KeyArrowUp,
termbox.KeyArrowDown: KeyArrowDown,
termbox.KeyArrowLeft: KeyArrowLeft,
termbox.KeyArrowRight: KeyArrowRight,
termbox.KeyCtrlSpace: KeyCtrlSpace,
termbox.KeyCtrlA: KeyCtrlA,
termbox.KeyCtrlB: KeyCtrlB,
termbox.KeyCtrlC: KeyCtrlC,
termbox.KeyCtrlD: KeyCtrlD,
termbox.KeyCtrlE: KeyCtrlE,
termbox.KeyCtrlF: KeyCtrlF,
termbox.KeyCtrlG: KeyCtrlG,
termbox.KeyCtrlH: KeyCtrlH,
termbox.KeyCtrlI: KeyCtrlI,
termbox.KeyCtrlJ: KeyCtrlJ,
termbox.KeyCtrlK: KeyCtrlK,
termbox.KeyCtrlL: KeyCtrlL,
termbox.KeyCtrlM: KeyCtrlM,
termbox.KeyCtrlN: KeyCtrlN,
termbox.KeyCtrlO: KeyCtrlO,
termbox.KeyCtrlP: KeyCtrlP,
termbox.KeyCtrlQ: KeyCtrlQ,
termbox.KeyCtrlR: KeyCtrlR,
termbox.KeyCtrlS: KeyCtrlS,
termbox.KeyCtrlT: KeyCtrlT,
termbox.KeyCtrlU: KeyCtrlU,
termbox.KeyCtrlV: KeyCtrlV,
termbox.KeyCtrlW: KeyCtrlW,
termbox.KeyCtrlX: KeyCtrlX,
termbox.KeyCtrlY: KeyCtrlY,
termbox.KeyCtrlZ: KeyCtrlZ,
termbox.KeyCtrl3: KeyCtrl3,
termbox.KeyCtrl4: KeyCtrl4,
termbox.KeyCtrl5: KeyCtrl5,
termbox.KeyCtrl6: KeyCtrl6,
termbox.KeyCtrl7: KeyCtrl7,
termbox.KeyCtrl8: KeyCtrl8,
termbox.KeyCtrlSpace: KeyCtrlSpace,
termbox.KeyCtrlA: KeyCtrlA,
termbox.KeyCtrlB: KeyCtrlB,
termbox.KeyCtrlC: KeyCtrlC,
termbox.KeyCtrlD: KeyCtrlD,
termbox.KeyCtrlE: KeyCtrlE,
termbox.KeyCtrlF: KeyCtrlF,
termbox.KeyCtrlG: KeyCtrlG,
termbox.KeyCtrlH: KeyCtrlH,
termbox.KeyCtrlI: KeyCtrlI,
termbox.KeyCtrlJ: KeyCtrlJ,
termbox.KeyCtrlK: KeyCtrlK,
termbox.KeyCtrlL: KeyCtrlL,
termbox.KeyCtrlM: KeyCtrlM,
termbox.KeyCtrlN: KeyCtrlN,
termbox.KeyCtrlO: KeyCtrlO,
termbox.KeyCtrlP: KeyCtrlP,
termbox.KeyCtrlQ: KeyCtrlQ,
termbox.KeyCtrlR: KeyCtrlR,
termbox.KeyCtrlS: KeyCtrlS,
termbox.KeyCtrlT: KeyCtrlT,
termbox.KeyCtrlU: KeyCtrlU,
termbox.KeyCtrlV: KeyCtrlV,
termbox.KeyCtrlW: KeyCtrlW,
termbox.KeyCtrlX: KeyCtrlX,
termbox.KeyCtrlY: KeyCtrlY,
termbox.KeyCtrlZ: KeyCtrlZ,
termbox.KeyCtrl3: KeyCtrl3,
termbox.KeyCtrl4: KeyCtrl4,
termbox.KeyCtrl5: KeyCtrl5,
termbox.KeyCtrl6: KeyCtrl6,
termbox.KeyCtrl7: KeyCtrl7,
termbox.KeyCtrl8: KeyCtrl8,
}