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

@ -1,128 +1,148 @@
package main
import (
"./ui"
"bitbucket.org/lmika/ted-v2/ui"
)
const (
ModAlt rune = 1 << 31 - 1
ModAlt rune = 1<<31 - 1
)
// A command
type Command struct {
Name string
Doc string
Action func(ctx CommandContext) error
Name string
Doc string
// TODO: Add argument mapping which will fetch properties from the environment
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
func (cmd *Command) Do(ctx *CommandContext) error {
return cmd.Action(ctx)
}
// A command mapping
type CommandMapping struct {
Commands map[string]*Command
KeyMappings map[rune]*Command
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)}
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}
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
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]
return cm.Commands[name]
}
// Searches for a command by key mapping
func (cm *CommandMapping) KeyMapping(key rune) *Command {
return cm.KeyMappings[key]
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) }))
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) }))
// 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("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)
}))
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)
}))
cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error {
ctx.Frame().Prompt(": ", func(res string) {
ctx.Frame().Message("Command = " + res)
})
return nil
})
cm.Define("set-cell", "Change the value of the selected cell", "", func(ctx *CommandContext) error {
grid := ctx.Frame().Grid()
cellX, cellY := grid.CellPosition()
if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel {
ctx.Frame().Prompt("> ", func(res string) {
rwModel.SetCellValue(cellY, cellX, res)
})
}
return nil
})
cm.Define("quit", "Quit TED", "", func(ctx *CommandContext) error {
ctx.Session().UIManager.Shutdown()
return nil
})
}
// 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('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"))
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"))
cm.MapKey(':', cm.Command("enter-command"))
cm.MapKey('e', cm.Command("set-cell"))
cm.MapKey(':', cm.Command("enter-command"))
cm.MapKey('q', cm.Command("quit"))
}
// 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
}
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
}
}

130
frame.go
View file

@ -1,89 +1,133 @@
package main
import (
"./ui"
"bitbucket.org/lmika/ted-v2/ui"
)
type Mode int
type Mode int
const (
// The grid is selectable
GridMode Mode = iota
NilMode Mode = iota
// The grid is selectable
GridMode
// EntryMode is when the text entry is selected
EntryMode
)
// A frame is a UI instance.
type Frame struct {
Session *Session
Session *Session
uiManager *ui.Ui
clientArea *ui.RelativeLayout
grid *ui.Grid
messageView *ui.TextView
textEntry *ui.TextEntry
statusBar *ui.StatusBar
textEntrySwitch *ui.ProxyLayout
mode Mode
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 := &Frame{
uiManager: uiManager,
}
frame.grid = ui.NewGrid(nil)
frame.messageView = &ui.TextView{"Hello"}
frame.statusBar = &ui.StatusBar{"Test", "Status"}
frame.textEntrySwitch = &ui.ProxyLayout{frame.messageView}
frame.textEntry = &ui.TextEntry{}
frame.grid = ui.NewGrid(nil)
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)
// 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
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
return frame.clientArea
}
// Sets the current model of the frame
func (frame *Frame) SetModel(model ui.GridModel) {
frame.grid.SetModel(model)
frame.grid.SetModel(model)
}
// Returns the grid component
func (frame *Frame) Grid() *ui.Grid {
return frame.grid
return frame.grid
}
// Sets the specific mode.
func (frame *Frame) EnterMode(mode Mode) {
switch mode {
case GridMode:
frame.uiManager.SetFocusedComponent(frame)
}
// Enter the specific mode.
func (frame *Frame) enterMode(mode Mode) {
switch mode {
case GridMode:
frame.uiManager.SetFocusedComponent(frame)
case EntryMode:
frame.textEntrySwitch.Component = frame.textEntry
frame.uiManager.SetFocusedComponent(frame.textEntry)
}
}
// Exit the specific mode.
func (frame *Frame) exitMode(mode Mode) {
switch mode {
case EntryMode:
frame.textEntrySwitch.Component = frame.messageView
}
}
func (frame *Frame) setMode(mode Mode) {
frame.exitMode(frame.mode)
frame.mode = mode
frame.enterMode(frame.mode)
}
// Message sets the message view's message
func (frame *Frame) Message(s string) {
frame.messageView.Text = s
}
// Prompt the user for input. This switches the mode to entry mode.
func (frame *Frame) Prompt(prompt string, callback func(res string)) {
frame.textEntry.Prompt = prompt
frame.textEntry.SetValue("")
frame.textEntry.OnEntry = func(res string) {
frame.textEntry.OnEntry = nil
frame.setMode(GridMode)
callback(res)
}
frame.setMode(EntryMode)
}
// 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)
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)
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)
}
if frame.Session != nil {
frame.Session.KeyPressed(key, mod)
}
}

25
main.go
View file

@ -1,23 +1,24 @@
package main
import (
"./ui"
"bitbucket.org/lmika/ted-v2/ui"
)
func main() {
uiManager, err := ui.NewUI()
if err != nil {
panic(err)
}
defer uiManager.Close()
uiManager, err := ui.NewUI()
if err != nil {
panic(err)
}
defer uiManager.Close()
model := &StdModel{}
model := &StdModel{}
model.Resize(5, 5)
frame := NewFrame(uiManager)
NewSession(frame, model)
frame := NewFrame(uiManager)
NewSession(uiManager, frame, model)
uiManager.SetRootComponent(frame.RootComponent())
frame.EnterMode(GridMode)
uiManager.SetRootComponent(frame.RootComponent())
frame.enterMode(GridMode)
uiManager.Loop()
uiManager.Loop()
}

View file

@ -1,83 +1,86 @@
package main
import "./ui"
import "bitbucket.org/lmika/ted-v2/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 {
Model Model
Frame *Frame
Commands *CommandMapping
Model Model
Frame *Frame
Commands *CommandMapping
UIManager *ui.Ui
}
func NewSession(frame *Frame, model Model) *Session {
session := &Session{
Model: model,
Frame: frame,
Commands: NewCommandMapping(),
}
func NewSession(uiManager *ui.Ui, frame *Frame, model Model) *Session {
session := &Session{
Model: model,
Frame: frame,
Commands: NewCommandMapping(),
UIManager: uiManager,
}
frame.SetModel(&SessionGridModel{session})
frame.SetModel(&SessionGridModel{session})
session.Commands.RegisterViewCommands()
session.Commands.RegisterViewKeyBindings()
session.Commands.RegisterViewCommands()
session.Commands.RegisterViewKeyBindings()
// Also assign this session with the frame
frame.Session = session
// Also assign this session with the frame
frame.Session = session
return 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
}
// 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())
}
}
cmd := session.Commands.KeyMapping(key)
if cmd != nil {
err := cmd.Do(&CommandContext{session})
if err != nil {
session.Frame.ShowMessage(err.Error())
}
}
}
// The command context used by the session
type SessionCommandContext struct {
Session *Session
type CommandContext struct {
session *Session
}
func (scc SessionCommandContext) Frame() *Frame {
return scc.Session.Frame
func (scc *CommandContext) Session() *Session {
return scc.session
}
func (scc *CommandContext) Frame() *Frame {
return scc.session.Frame
}
// Session grid model
type SessionGridModel struct {
Session *Session
Session *Session
}
// Returns the size of the grid model (width x height)
func (sgm *SessionGridModel) Dimensions() (int, int) {
rs, cs := sgm.Session.Model.Dimensions()
return cs, rs
rs, cs := sgm.Session.Model.Dimensions()
return cs, rs
}
// Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
func (sgm *SessionGridModel) ColWidth(int) int {
return 24
return 24
}
// Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
func (sgm *SessionGridModel) RowHeight(int) int {
return 1
return 1
}
// Returns the value of the cell a position X, Y
func (sgm *SessionGridModel) CellValue(x int, y int) string {
return sgm.Session.Model.CellValue(y, x)
return sgm.Session.Model.CellValue(y, x)
}

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 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 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
// 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()
// 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
}
}
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
var cellWidth, cellHeight int
modelCellX := cellX - 1 + grid.viewCellX
modelCellY := cellY - 1 + grid.viewCellY
modelMaxX, modelMaxY := grid.model.Dimensions()
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
}
// 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 (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
}
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
// 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])
}
}
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)
}
}
// 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
}
// 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)
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
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
}
cellY++
cellsHigh++
screenY = screenY + rowHeight - cellOffsetY
cellOffsetY = 0
}
return gridPoint(screenX + colWidth), cellsHigh
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
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
}
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
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,
}