Implemented a single-line text box. This implements many of the features missing from the text entry used in version 1.

This commit is contained in:
lmika 2015-01-05 22:50:02 +11:00
parent 6227b76b86
commit e86cb4fd63
8 changed files with 229 additions and 161 deletions

10
main.go
View file

@ -12,17 +12,21 @@ func main() {
defer uiManager.Close() defer uiManager.Close()
cmdText := &ui.TextEntry{ Prompt: "Enter: " }
statusLayout := &ui.VertLinearLayout{} statusLayout := &ui.VertLinearLayout{}
statusLayout.Append(&ui.StatusBar{"Test", "Component"}) statusLayout.Append(&ui.StatusBar{"Test", "Component"})
statusLayout.Append(&ui.StatusBar{"Another", "Component"}) statusLayout.Append(cmdText)
statusLayout.Append(&ui.StatusBar{"Third", "Test"}) //statusLayout.Append(&ui.StatusBar{"Another", "Component"})
//statusLayout.Append(&ui.StatusBar{"Third", "Test"})
grid := ui.NewGrid(&ui.TestModel{}) grid := ui.NewGrid(&ui.TestModel{})
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.Loop() uiManager.Loop()
/* /*

View file

@ -3,11 +3,6 @@
package ui package ui
// The set of event types supported by the UI package.
// An interface of a UI component. // An interface of a UI component.
type UiComponent interface { type UiComponent interface {
@ -24,44 +19,5 @@ type UiComponent interface {
type FocusableComponent interface { type FocusableComponent interface {
// Called when the component has focus and a key has been pressed // Called when the component has focus and a key has been pressed
KeyPressed(key rune) KeyPressed(key rune, mod int)
} }
// ==========================================================================
// UI context.
// ==========================================================================
// Status bar component
/*
type UiStatusBar struct {
left string // Left aligned string
right string // Right aligned string
}
// Minimum dimensions
func (sbar *UiStatusBar) RequestDims() (int, int) {
return -1, 2
}
// Status bar redraw
func (sbar *UiStatusBar) Redraw(x int, y int, w int, h int) {
leftLen := len(sbar.left)
rightLen := len(sbar.right)
rightPos := w - rightLen
for x1 := 0; x1 < w; x1++ {
var runeToPrint rune = ' '
if x1 < leftLen {
runeToPrint = rune(sbar.left[x1])
} else if x1 >= rightPos {
runeToPrint = rune(sbar.right[x1 - rightPos])
}
termbox.SetCell(x1, y, runeToPrint, termbox.AttrReverse, termbox.AttrReverse)
}
}
*/

View file

@ -78,6 +78,13 @@ func (dc *DrawContext) DrawRuneWithAttrs(x, y int, ch rune, fa, ba Attribute) {
} }
} }
// 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)
}
}
// Converts a local point to a real point. If the point is within the context, also returns true // 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) { func (dc *DrawContext) localPointToRealPoint(x, y int) (int, int, bool) {
rx, ry := x + dc.X, y + dc.Y rx, ry := x + dc.X, y + dc.Y

View file

@ -28,30 +28,7 @@ const (
// Special keys // Special keys
const ( const (
KeyF1 rune = 0x8000 + iota KeyCtrlSpace rune = 0x8000 + iota
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyInsert
KeyDelete
KeyHome
KeyEnd
KeyPgup
KeyPgdn
KeyArrowUp
KeyArrowDown
KeyArrowLeft
KeyArrowRight
KeyCtrlSpace
KeyCtrlA KeyCtrlA
KeyCtrlB KeyCtrlB
KeyCtrlC KeyCtrlC
@ -84,7 +61,33 @@ const (
KeyCtrl6 KeyCtrl6
KeyCtrl7 KeyCtrl7
KeyCtrl8 KeyCtrl8
KeySpace
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
) )
// The type of events supported by the driver // The type of events supported by the driver
@ -96,13 +99,19 @@ const (
// Event when the window is resized // Event when the window is resized
EventResize EventResize
// Event indicating a key press. The key is set in Ch // Event indicating a key press. The key is set in Ch and modifications
// are set in Or
EventKeyPress EventKeyPress
) )
const (
ModKeyAlt int = (1 << iota)
)
// Data from an event callback. // Data from an event callback.
type Event struct { type Event struct {
Type EventType Type EventType
Par int
Ch rune Ch rune
} }
@ -127,6 +136,7 @@ type Driver interface {
// Wait for an event // Wait for an event
WaitForEvent() Event WaitForEvent() Event
}
// // Move the position of the cursor
SetCursor(x, y int)
}

View file

@ -6,9 +6,6 @@ package ui
// The UI manager // The UI manager
type Ui struct { type Ui struct {
//grid *Grid
//statusBar *UiStatusBar
// The root component // The root component
rootComponent UiComponent rootComponent UiComponent
focusedComponent FocusableComponent focusedComponent FocusableComponent
@ -31,22 +28,6 @@ func NewUI() (*Ui, error) {
drawContext := &DrawContext{ driver: driver } drawContext := &DrawContext{ driver: driver }
ui := &Ui{ drawContext: drawContext, driver: driver } ui := &Ui{ drawContext: drawContext, driver: driver }
/*
termboxError := termbox.Init()
if termboxError != nil {
return nil, termboxError
} else {
uiCtx := new(Ui) // &Ui{&UiStatusBar{"Hello", "World"}}
uiCtx.grid = NewGrid(&TestModel{})
uiCtx.statusBar = &UiStatusBar{"Hello", "World"}
return uiCtx, nil
}
// XXX: Workaround for bug in compiler
panic("Unreachable code")
return nil, nil
*/
return ui, nil return ui, nil
} }
@ -84,23 +65,6 @@ func (ui *Ui) Redraw() {
ui.driver.Sync() ui.driver.Sync()
} }
/**
* Internal redraw function which does not query the terminal size.
*/
/*
func (ui *Ui) redrawInternal(width, height int) {
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
// TODO: This will eventually offload to UI "components"
ui.grid.Redraw(0, 0, width, height - 2)
// Draws the status bar
ui.statusBar.Redraw(0, height - 2, width, 2)
termbox.Flush()
}
*/
// Enter the UI loop // Enter the UI loop
func (ui *Ui) Loop() { func (ui *Ui) Loop() {
@ -111,38 +75,12 @@ func (ui *Ui) Loop() {
// TODO: If the event is a key-press, do something. // TODO: If the event is a key-press, do something.
if event.Type == EventKeyPress { if event.Type == EventKeyPress {
if ui.focusedComponent != nil { if ui.focusedComponent != nil {
ui.focusedComponent.KeyPressed(event.Ch) ui.focusedComponent.KeyPressed(event.Ch, event.Par)
} }
} else if event.Type == EventResize { } else if event.Type == EventResize {
// HACK: Find another way to refresh the size of the screen to prevent a full redraw. // HACK: Find another way to refresh the size of the screen to prevent a full redraw.
ui.driver.Sync() ui.driver.Sync()
} }
/*
if event.Type == termbox.EventResize {
ui.redrawInternal(event.Width, event.Height)
} else {
// !!TEMP!!
if (event.Ch == 'i') {
ui.grid.MoveBy(0, -1)
} else if (event.Ch == 'k') {
ui.grid.MoveBy(0, 1)
} else if (event.Ch == 'j') {
ui.grid.MoveBy(-1, 0)
} else if (event.Ch == 'l') {
ui.grid.MoveBy(1, 0)
} else {
return UiEvent{EventKeyPress, 0}
}
// !!END TEMP!!
ui.Redraw()
//return UiEvent{EventKeyPress, 0}
}
*/
} }
// XXX: Workaround for bug in compiler
} }

View file

@ -2,6 +2,7 @@
package ui package ui
import "unicode"
// 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.
@ -23,19 +24,134 @@ func (sbar *StatusBar) Redraw(context *DrawContext) {
context.HorizRule(0, ' ') context.HorizRule(0, ' ')
context.Print(0, 0, sbar.Left) context.Print(0, 0, sbar.Left)
context.PrintRight(context.W, 0, sbar.Right) context.PrintRight(context.W, 0, sbar.Right)
/* }
leftLen := len(sbar.left)
rightLen := len(sbar.right)
rightPos := w - rightLen // A single-text entry component.
type TextEntry struct {
for x1 := 0; x1 < w; x1++ { Prompt string
var runeToPrint rune = ' '
if x1 < leftLen { value string
runeToPrint = rune(sbar.left[x1]) cursorOffset int
} else if x1 >= rightPos { displayOffset int
runeToPrint = rune(sbar.right[x1 - rightPos]) }
}
termbox.SetCell(x1, y, runeToPrint, termbox.AttrReverse, termbox.AttrReverse) func (te *TextEntry) Remeasure(w, h int) (int, int) {
} return w, 1
*/ }
func (te *TextEntry) Redraw(context *DrawContext) {
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)
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(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 virtualCursorOffset >= displayWidth {
te.displayOffset = te.cursorOffset - displayWidth + 10
} else if (virtualCursorOffset < 0) {
te.displayOffset = intMax(te.cursorOffset - displayWidth + 1, 0)
}
return te.displayOffset
}
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 + "'")
}
}
// Backspace
func (te *TextEntry) backspace() {
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
}
}
}
// 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
}
}
// 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)
}
// 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:]
}
}
// Move the cursor
func (te *TextEntry) moveCursorBy(byX int) {
te.moveCursorTo(te.cursorOffset + byX)
}
func (te *TextEntry) moveCursorTo(toX int) {
te.cursorOffset = intMinMax(toX, 0, len(te.value))
} }

View file

@ -13,7 +13,13 @@ type TermboxDriver struct {
// Initializes the driver. Returns an error if there was an error // Initializes the driver. Returns an error if there was an error
func (td *TermboxDriver) Init() error { func (td *TermboxDriver) Init() error {
return termbox.Init() err := termbox.Init()
if err != nil {
return err
}
termbox.SetInputMode(termbox.InputAlt)
return nil
} }
// Closes the driver // Closes the driver
@ -42,23 +48,35 @@ func (td *TermboxDriver) WaitForEvent() Event {
switch tev.Type { switch tev.Type {
case termbox.EventResize: case termbox.EventResize:
return Event{EventResize, 0} return Event{EventResize, 0, 0}
case termbox.EventKey: case termbox.EventKey:
mod := 0
if tev.Mod & termbox.ModAlt != 0 {
mod = ModKeyAlt
}
if tev.Ch != 0 { if tev.Ch != 0 {
return Event{EventKeyPress, tev.Ch} return Event{EventKeyPress, mod, tev.Ch}
} else if spec, hasSpec := termboxKeysToSpecialKeys[tev.Key] ; hasSpec { } else if spec, hasSpec := termboxKeysToSpecialKeys[tev.Key] ; hasSpec {
return Event{EventKeyPress, spec} return Event{EventKeyPress, mod, spec}
} else { } else {
return Event{EventNone, 0} return Event{EventNone, mod, 0}
} }
default: default:
return Event{EventNone, 0} return Event{EventNone, 0, 0}
} }
} }
// Move the position of the cursor
func (td *TermboxDriver) SetCursor(x, y int) {
termbox.SetCursor(x, y)
}
// Map from termbox Keys to driver key runes // Map from termbox Keys to driver key runes
var termboxKeysToSpecialKeys = map[termbox.Key]rune { var termboxKeysToSpecialKeys = map[termbox.Key]rune {
termbox.KeySpace: ' ',
termbox.KeyF1: KeyF1, termbox.KeyF1: KeyF1,
termbox.KeyF2: KeyF2, termbox.KeyF2: KeyF2,
termbox.KeyF3: KeyF3, termbox.KeyF3: KeyF3,
@ -114,6 +132,5 @@ var termboxKeysToSpecialKeys = map[termbox.Key]rune {
termbox.KeyCtrl5: KeyCtrl5, termbox.KeyCtrl5: KeyCtrl5,
termbox.KeyCtrl6: KeyCtrl6, termbox.KeyCtrl6: KeyCtrl6,
termbox.KeyCtrl7: KeyCtrl7, termbox.KeyCtrl7: KeyCtrl7,
termbox.KeySpace: KeySpace,
termbox.KeyCtrl8: KeyCtrl8, termbox.KeyCtrl8: KeyCtrl8,
} }

View file

@ -11,3 +11,23 @@ func intMax(x, y int) int {
return x return x
} }
} }
// Returns the minimum value of either x or y.
func intMin(x, y int) int {
if (x > y) {
return y
} else {
return x
}
}
// Returns the value capped between two limits.
func intMinMax(x, min, max int) int {
if (x < min) {
return min
} else if (x > max) {
return max
} else {
return x
}
}