A lot of work. Started working on the session objects and added a new text display component and componant switcher.

This commit is contained in:
lmika 2015-01-06 22:34:06 +11:00
parent e86cb4fd63
commit 910bdbc854
7 changed files with 391 additions and 42 deletions

126
commandmap.go Normal file
View file

@ -0,0 +1,126 @@
package main
import (
"./ui"
)
const (
ModAlt rune = 1 << 31 - 1
)
// A command
type Command struct {
Name string
Doc string
Action func(ctx CommandContext) error
}
// Execute the command
func (cmd *Command) Do(ctx CommandContext) error {
return cmd.Action(ctx)
}
// The command context
type CommandContext interface {
// Returns the current frame. If no frame is defined, returns nil.
Frame() *Frame
}
// A command mapping
type CommandMapping struct {
Commands map[string]*Command
KeyMappings map[rune]*Command
}
// Creates a new, empty command mapping
func NewCommandMapping() *CommandMapping {
return &CommandMapping{make(map[string]*Command), make(map[rune]*Command)}
}
// Adds a new command
func (cm *CommandMapping) Define(name string, doc string, opts string, fn func(ctx CommandContext) error) {
cm.Commands[name] = &Command{name, doc, fn}
}
// Adds a key mapping
func (cm *CommandMapping) MapKey(key rune, cmd *Command) {
cm.KeyMappings[key] = cmd
}
// Searches for a command by name. Returns the command or null
func (cm *CommandMapping) Command(name string) *Command {
return cm.Commands[name]
}
// Searches for a command by key mapping
func (cm *CommandMapping) KeyMapping(key rune) *Command {
return cm.KeyMappings[key]
}
// Registers the standard view navigation commands. These commands require the frame
func (cm *CommandMapping) RegisterViewCommands() {
cm.Define("move-down", "Moves the cursor down one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 1) }))
cm.Define("move-up", "Moves the cursor up one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, -1) }))
cm.Define("move-left", "Moves the cursor left one column", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(-1, 0) }))
cm.Define("move-right", "Moves the cursor right one column", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(1, 0) }))
// TODO: Pages are just 25 rows and 15 columns at the moment
cm.Define("page-down", "Moves the cursor down one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 25) }))
cm.Define("page-up", "Moves the cursor up one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, -25) }))
cm.Define("page-left", "Moves the cursor left one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(-15, 0) }))
cm.Define("page-right", "Moves the cursor right one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(15, 0) }))
cm.Define("row-top", "Moves the cursor to the top of the row", "", gridNavOperation(func(grid *ui.Grid) {
cellX, _ := grid.CellPosition()
grid.MoveTo(cellX, 0)
}))
cm.Define("row-bottom", "Moves the cursor to the bottom of the row", "", gridNavOperation(func(grid *ui.Grid) {
cellX, _ := grid.CellPosition()
_, dimY := grid.Model().Dimensions()
grid.MoveTo(cellX, dimY - 1)
}))
cm.Define("col-left", "Moves the cursor to the left-most column", "", gridNavOperation(func(grid *ui.Grid) {
_, cellY := grid.CellPosition()
grid.MoveTo(0, cellY)
}))
cm.Define("col-right", "Moves the cursor to the right-most column", "", gridNavOperation(func(grid *ui.Grid) {
_, cellY := grid.CellPosition()
dimX, _ := grid.Model().Dimensions()
grid.MoveTo(dimX - 1, cellY)
}))
}
// Registers the standard view key bindings. These commands require the frame
func (cm *CommandMapping) RegisterViewKeyBindings() {
cm.MapKey('i', cm.Command("move-up"))
cm.MapKey('k', cm.Command("move-down"))
cm.MapKey('j', cm.Command("move-left"))
cm.MapKey('l', cm.Command("move-right"))
cm.MapKey('I', cm.Command("page-up"))
cm.MapKey('K', cm.Command("page-down"))
cm.MapKey('J', cm.Command("page-left"))
cm.MapKey('L', cm.Command("page-right"))
cm.MapKey(ui.KeyCtrlI, cm.Command("row-top"))
cm.MapKey(ui.KeyCtrlK, cm.Command("row-bottom"))
cm.MapKey(ui.KeyCtrlJ, cm.Command("col-left"))
cm.MapKey(ui.KeyCtrlL, cm.Command("col-right"))
cm.MapKey(ui.KeyArrowUp, cm.Command("move-up"))
cm.MapKey(ui.KeyArrowDown, cm.Command("move-down"))
cm.MapKey(ui.KeyArrowLeft, cm.Command("move-left"))
cm.MapKey(ui.KeyArrowRight, cm.Command("move-right"))
}
// A nativation command factory. This will perform the passed in operation with the current grid and
// will display the cell value in the message box.
func gridNavOperation(op func(grid *ui.Grid)) func(ctx CommandContext) error {
return func(ctx CommandContext) error {
op(ctx.Frame().Grid())
ctx.Frame().ShowCellValue()
return nil
}
}

84
frame.go Normal file
View file

@ -0,0 +1,84 @@
package main
import (
"./ui"
)
type Mode int
const (
// The grid is selectable
GridMode Mode = iota
)
// A frame is a UI instance.
type Frame struct {
Session *Session
uiManager *ui.Ui
clientArea *ui.RelativeLayout
grid *ui.Grid
messageView *ui.TextView
textEntry *ui.TextEntry
statusBar *ui.StatusBar
textEntrySwitch *ui.ProxyLayout
}
// Creates the UI and returns a new frame
func NewFrame(uiManager *ui.Ui) *Frame {
frame := &Frame{
uiManager: uiManager,
}
frame.grid = ui.NewGrid(&ui.TestModel{})
frame.messageView = &ui.TextView{"Hello"}
frame.statusBar = &ui.StatusBar{"Test", "Status"}
frame.textEntrySwitch = &ui.ProxyLayout{frame.messageView}
frame.textEntry = &ui.TextEntry{}
// Build the UI frame
statusLayout := &ui.VertLinearLayout{}
statusLayout.Append(frame.statusBar)
statusLayout.Append(frame.textEntrySwitch)
frame.clientArea = &ui.RelativeLayout{ Client: frame.grid, South: statusLayout }
return frame
}
// Returns the root component of the frame
func (frame *Frame) RootComponent() ui.UiComponent {
return frame.clientArea
}
// Returns the grid component
func (frame *Frame) Grid() *ui.Grid {
return frame.grid
}
// Sets the specific mode.
func (frame *Frame) EnterMode(mode Mode) {
switch mode {
case GridMode:
frame.uiManager.SetFocusedComponent(frame)
}
}
// Show a message. This will switch the bottom to the messageView and select the frame
func (frame *Frame) ShowMessage(msg string) {
frame.messageView.Text = msg
frame.textEntrySwitch.Component = frame.messageView
//frame.EnterMode(GridMode)
}
// Shows the value of the currently select grid cell
func (frame *Frame) ShowCellValue() {
displayValue := frame.grid.CurrentCellDisplayValue()
frame.ShowMessage(displayValue)
}
// Handle the main grid input as this is the "component" that handles command input.
func (frame *Frame) KeyPressed(key rune, mod int) {
if frame.Session != nil {
frame.Session.KeyPressed(key, mod)
}
}

12
main.go
View file

@ -11,7 +11,14 @@ func main() {
} }
defer uiManager.Close() defer uiManager.Close()
frame := NewFrame(uiManager)
NewSession(frame)
uiManager.SetRootComponent(frame.RootComponent())
frame.EnterMode(GridMode)
uiManager.Loop()
/*
cmdText := &ui.TextEntry{Prompt: "Enter: "} cmdText := &ui.TextEntry{Prompt: "Enter: "}
statusLayout := &ui.VertLinearLayout{} statusLayout := &ui.VertLinearLayout{}
@ -25,10 +32,11 @@ func main() {
clientArea := &ui.RelativeLayout{ Client: grid, South: statusLayout } clientArea := &ui.RelativeLayout{ Client: grid, South: statusLayout }
uiManager.SetRootComponent(clientArea) uiManager.SetRootComponent(clientArea)
//uiManager.SetFocusedComponent(grid) uiManager.SetFocusedComponent(grid)
uiManager.SetFocusedComponent(cmdText) //uiManager.SetFocusedComponent(cmdText)
uiManager.Loop() uiManager.Loop()
*/
/* /*
uiCtx, _ := NewUI() uiCtx, _ := NewUI()

51
session.go Normal file
View file

@ -0,0 +1,51 @@
package main
import "./ui"
// The session is responsible for managing the UI and the model and handling
// the interaction between the two and the user.
type Session struct {
Frame *Frame
Commands *CommandMapping
}
func NewSession(frame *Frame) *Session {
session := &Session{
Frame: frame,
Commands: NewCommandMapping(),
}
session.Commands.RegisterViewCommands()
session.Commands.RegisterViewKeyBindings()
// Also assign this session with the frame
frame.Session = session
return session
}
// Input from the frame
func (session *Session) KeyPressed(key rune, mod int) {
// Add the mod key modifier
if (mod & ui.ModKeyAlt != 0) {
key |= ModAlt
}
cmd := session.Commands.KeyMapping(key)
if cmd != nil {
err := cmd.Do(SessionCommandContext{session})
if err != nil {
session.Frame.ShowMessage(err.Error())
}
}
}
// The command context used by the session
type SessionCommandContext struct {
Session *Session
}
func (scc SessionCommandContext) Frame() *Frame {
return scc.Session.Frame
}

View file

@ -13,22 +13,22 @@ type GridModel interface {
/** /**
* Returns the size of the grid model (width x height) * Returns the size of the grid model (width x height)
*/ */
GetDimensions() (int, int) Dimensions() (int, int)
/** /**
* Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
*/ */
GetColWidth(int) int ColWidth(int) int
/** /**
* Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
*/ */
GetRowHeight(int) int RowHeight(int) int
/** /**
* Returns the value of the cell a position X, Y * Returns the value of the cell a position X, Y
*/ */
GetCellValue(int, int) string CellValue(int, int) string
} }
@ -38,7 +38,8 @@ type gridPoint int
* The grid component. * The grid component.
*/ */
type Grid struct { type Grid struct {
model GridModel model GridModel // The grid model
viewCellX int // Left most cell viewCellX int // Left most cell
viewCellY int // Top most cell viewCellY int // Top most cell
selCellX int // The currently selected cell selCellX int // The currently selected cell
@ -72,11 +73,9 @@ 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
* Returns the requested dimensions of a grid (as required by UiComponent) func (grid *Grid) Model() GridModel {
*/ return grid.model
func (grid *Grid) Remeasure(w, h int) (int, int) {
return w, h
} }
/** /**
@ -88,12 +87,45 @@ func (grid *Grid) ShiftBy(x int, y int) {
} }
// Moves the currently selected cell by a delta. // 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) { func (grid *Grid) MoveBy(x int, y int) {
grid.selCellX += x grid.MoveTo(grid.selCellX + x, grid.selCellY + y)
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() 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 // Determine the topmost cell based on the location of the currently selected cell
@ -118,17 +150,15 @@ func (grid *Grid) reposition() {
} }
// Gets the cell value and attributes of a particular cell // Gets the cell value and attributes of a particular cell
func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) { func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) {
// The fixed cells // The fixed cells
modelCellX := cellX - 1 + grid.viewCellX modelCellX := cellX - 1 + grid.viewCellX
modelCellY := cellY - 1 + grid.viewCellY modelCellY := cellY - 1 + grid.viewCellY
modelMaxX, modelMaxY := grid.model.GetDimensions() modelMaxX, modelMaxY := grid.model.Dimensions()
if (cellX == 0) && (cellY == 0) { if (cellX == 0) && (cellY == 0) {
return strconv.Itoa(grid.cellsWide), AttrBold, AttrBold return "", AttrBold, AttrBold
} else if (cellX == 0) { } else if (cellX == 0) {
if (modelCellY == grid.selCellY) { if (modelCellY == grid.selCellY) {
return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrBold | AttrReverse return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrBold | AttrReverse
@ -145,18 +175,14 @@ func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute)
// The data from the model // The data from the model
if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) { if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) {
if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) { if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) {
return grid.model.GetCellValue(modelCellX, modelCellY), AttrReverse, AttrReverse return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse
} else { } else {
return grid.model.GetCellValue(modelCellX, modelCellY), 0, 0 return grid.model.CellValue(modelCellX, modelCellY), 0, 0
} }
} else { } else {
return "~", 0, 0 return "~", ColorBlue | AttrBold, 0
} }
} }
// XXX: Workaround for bug in compiler
panic("Unreachable code")
return "", 0, 0
} }
// Gets the cell dimensions // Gets the cell dimensions
@ -166,17 +192,17 @@ func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) {
modelCellX := cellX - 1 + grid.viewCellX modelCellX := cellX - 1 + grid.viewCellX
modelCellY := cellY - 1 + grid.viewCellY modelCellY := cellY - 1 + grid.viewCellY
modelMaxX, modelMaxY := grid.model.GetDimensions() modelMaxX, modelMaxY := grid.model.Dimensions()
// Get the cell width & height from model (if within range) // Get the cell width & height from model (if within range)
if (modelCellX >= 0) && (modelCellX < modelMaxX) { if (modelCellX >= 0) && (modelCellX < modelMaxX) {
cellWidth = grid.model.GetColWidth(modelCellX) cellWidth = grid.model.ColWidth(modelCellX)
} else { } else {
cellWidth = 8 cellWidth = 8
} }
if (modelCellY >= 0) && (modelCellY < modelMaxY) { if (modelCellY >= 0) && (modelCellY < modelMaxY) {
cellHeight = grid.model.GetRowHeight(modelCellY) cellHeight = grid.model.RowHeight(modelCellY)
} else { } else {
cellHeight = 2 cellHeight = 2
} }
@ -212,8 +238,6 @@ func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy
} }
} }
//termbox.SetCell(int(x - cellClipRect.x1) + sx, int(y - cellClipRect.y1) + sy, currRune, fg, bg)
// TODO: This might be better if this wasn't so low-level // 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) ctx.DrawRuneWithAttrs(int(x - cellClipRect.x1) + sx, int(y - cellClipRect.y1) + sy, currRune, fg, bg)
} }
@ -288,7 +312,7 @@ func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX in
* Returns the cell of the particular point, along with the top-left position of the cell. * 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) { func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, posY int) {
var wid, hei int = grid.model.GetDimensions() var wid, hei int = grid.model.Dimensions()
posX = 0 posX = 0
posY = 0 posY = 0
@ -297,7 +321,7 @@ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, pos
// Go through columns to locate the particular cellX // Go through columns to locate the particular cellX
for cx := 0; cx < wid; cx++ { for cx := 0; cx < wid; cx++ {
if (x >= posX) && (x < posX + grid.model.GetColWidth(cx)) { if (x >= posX) && (x < posX + grid.model.ColWidth(cx)) {
// We found the X position // We found the X position
cellX = int(cx) cellX = int(cx)
break break
@ -305,7 +329,7 @@ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, pos
} }
for cy := 0; cy < hei; cy++ { for cy := 0; cy < hei; cy++ {
if (y >= posY) && (y < posY + grid.model.GetRowHeight(cy)) { if (y >= posY) && (y < posY + grid.model.RowHeight(cy)) {
// And the Y position // And the Y position
cellY = int(cy) cellY = int(cy)
break break
@ -315,6 +339,13 @@ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, pos
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
}
/** /**
* Redraws the grid. * Redraws the grid.
*/ */
@ -323,8 +354,10 @@ func (grid *Grid) Redraw(ctx *DrawContext) {
grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0) grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0)
} }
// Called when the component has focus and a key has been pressed // Called when the component has focus and a key has been pressed.
func (grid *Grid) KeyPressed(key rune) { // 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) { if (key == 'i') || (key == KeyArrowUp) {
grid.MoveBy(0, -1) grid.MoveBy(0, -1)
} else if (key == 'k') || (key == KeyArrowDown) { } else if (key == 'k') || (key == KeyArrowDown) {
@ -347,27 +380,27 @@ type TestModel struct {
/** /**
* Returns the size of the grid model (width x height) * Returns the size of the grid model (width x height)
*/ */
func (model *TestModel) GetDimensions() (int, int) { 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. * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
*/ */
func (model *TestModel) GetColWidth(int) int { 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. * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
*/ */
func (model *TestModel) GetRowHeight(int) int { func (model *TestModel) RowHeight(int) int {
return 1 return 1
} }
/** /**
* Returns the value of the cell a position X, Y * Returns the value of the cell a position X, Y
*/ */
func (model *TestModel) GetCellValue(x int, y int) string { 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

@ -61,6 +61,29 @@ func (vl *VertLinearLayout) Remeasure(w, h int) (int, int) {
} }
// A layout component which defers calls to the nested component. If no control is defined,
// will not draw itself. Used for switching controls dynamically.
type ProxyLayout struct {
Component UiComponent
}
func (pl *ProxyLayout) Remeasure(w, h int) (int, int) {
if pl.Component != nil {
return pl.Component.Remeasure(w, h)
} else {
return 0, 0
}
}
func (pl *ProxyLayout) Redraw(context *DrawContext) {
if pl.Component != nil {
pl.Component.Redraw(context)
}
}
// A relative layout component. This has a "client" component, bordered by a north, // A relative layout component. This has a "client" component, bordered by a north,
// south, east and west component. The N,S,E,W components will be provided with the full dimensions // south, east and west component. The N,S,E,W components will be provided with the full dimensions
// whereas the client component will be provided with the remaining size. Each one of the components // whereas the client component will be provided with the remaining size. Each one of the components

View file

@ -4,6 +4,30 @@ package ui
import "unicode" import "unicode"
// A text component. This simply renders a text string.
type TextView struct {
// The string to render
Text string
}
// Minimum dimensions
func (tv *TextView) Remeasure(w, h int) (int, int) {
return w, 1
}
// Status bar redraw
func (tv *TextView) Redraw(context *DrawContext) {
context.SetFgAttr(0)
context.SetBgAttr(0)
context.HorizRule(0, ' ')
context.Print(0, 0, tv.Text)
}
// Status bar component. This component displays text on the left and right of it's // Status bar component. This component displays text on the left and right of it's
// allocated space. // allocated space.
type StatusBar struct { type StatusBar struct {