From e86cb4fd63774c842713aef4dfc06b74c924a305 Mon Sep 17 00:00:00 2001 From: lmika Date: Mon, 5 Jan 2015 22:50:02 +1100 Subject: [PATCH] Implemented a single-line text box. This implements many of the features missing from the text entry used in version 1. --- main.go | 10 ++- ui/component.go | 46 +------------- ui/drawable.go | 7 +++ ui/driver.go | 66 +++++++++++--------- ui/manager.go | 64 +------------------ ui/stdcomps.go | 146 +++++++++++++++++++++++++++++++++++++++----- ui/termboxdriver.go | 31 +++++++--- ui/utils.go | 20 ++++++ 8 files changed, 229 insertions(+), 161 deletions(-) diff --git a/main.go b/main.go index 0095489..eb21a70 100644 --- a/main.go +++ b/main.go @@ -12,17 +12,21 @@ func main() { defer uiManager.Close() + cmdText := &ui.TextEntry{ Prompt: "Enter: " } + statusLayout := &ui.VertLinearLayout{} statusLayout.Append(&ui.StatusBar{"Test", "Component"}) - statusLayout.Append(&ui.StatusBar{"Another", "Component"}) - statusLayout.Append(&ui.StatusBar{"Third", "Test"}) + statusLayout.Append(cmdText) + //statusLayout.Append(&ui.StatusBar{"Another", "Component"}) + //statusLayout.Append(&ui.StatusBar{"Third", "Test"}) grid := ui.NewGrid(&ui.TestModel{}) clientArea := &ui.RelativeLayout{ Client: grid, South: statusLayout } uiManager.SetRootComponent(clientArea) - uiManager.SetFocusedComponent(grid) + //uiManager.SetFocusedComponent(grid) + uiManager.SetFocusedComponent(cmdText) uiManager.Loop() /* diff --git a/ui/component.go b/ui/component.go index 9fe5e7d..0bf2ef9 100644 --- a/ui/component.go +++ b/ui/component.go @@ -3,11 +3,6 @@ package ui -// The set of event types supported by the UI package. - - - - // An interface of a UI component. type UiComponent interface { @@ -24,44 +19,5 @@ type UiComponent interface { type FocusableComponent interface { // 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) - } -} -*/ \ No newline at end of file diff --git a/ui/drawable.go b/ui/drawable.go index ea2e87f..78260ef 100644 --- a/ui/drawable.go +++ b/ui/drawable.go @@ -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 func (dc *DrawContext) localPointToRealPoint(x, y int) (int, int, bool) { rx, ry := x + dc.X, y + dc.Y diff --git a/ui/driver.go b/ui/driver.go index 4823780..97190a5 100644 --- a/ui/driver.go +++ b/ui/driver.go @@ -28,30 +28,7 @@ const ( // Special keys const ( - KeyF1 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 + KeyCtrlSpace rune = 0x8000 + iota KeyCtrlA KeyCtrlB KeyCtrlC @@ -84,7 +61,33 @@ const ( KeyCtrl6 KeyCtrl7 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 @@ -96,13 +99,19 @@ const ( // Event when the window is resized 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 ) +const ( + ModKeyAlt int = (1 << iota) +) + // Data from an event callback. type Event struct { Type EventType + Par int Ch rune } @@ -127,6 +136,7 @@ type Driver interface { // Wait for an event WaitForEvent() Event -} -// \ No newline at end of file + // Move the position of the cursor + SetCursor(x, y int) +} diff --git a/ui/manager.go b/ui/manager.go index 7caaa7a..b9174b8 100644 --- a/ui/manager.go +++ b/ui/manager.go @@ -6,9 +6,6 @@ package ui // The UI manager type Ui struct { - //grid *Grid - //statusBar *UiStatusBar - // The root component rootComponent UiComponent focusedComponent FocusableComponent @@ -31,22 +28,6 @@ func NewUI() (*Ui, error) { 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 } @@ -84,23 +65,6 @@ func (ui *Ui) Redraw() { 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 func (ui *Ui) Loop() { @@ -111,38 +75,12 @@ func (ui *Ui) Loop() { // TODO: If the event is a key-press, do something. if event.Type == EventKeyPress { if ui.focusedComponent != nil { - ui.focusedComponent.KeyPressed(event.Ch) + 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() } - - /* - 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 } \ No newline at end of file diff --git a/ui/stdcomps.go b/ui/stdcomps.go index ab7a0d2..12ad103 100644 --- a/ui/stdcomps.go +++ b/ui/stdcomps.go @@ -2,6 +2,7 @@ package ui +import "unicode" // Status bar component. This component displays text on the left and right of it's // allocated space. @@ -23,19 +24,134 @@ func (sbar *StatusBar) Redraw(context *DrawContext) { context.HorizRule(0, ' ') context.Print(0, 0, sbar.Left) context.PrintRight(context.W, 0, sbar.Right) - /* - 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) - } - */ } + + +// A single-text entry component. +type TextEntry struct { + Prompt string + + value string + cursorOffset int + displayOffset int +} + +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)) +} \ No newline at end of file diff --git a/ui/termboxdriver.go b/ui/termboxdriver.go index b9f8761..088c995 100644 --- a/ui/termboxdriver.go +++ b/ui/termboxdriver.go @@ -13,7 +13,13 @@ type TermboxDriver struct { // Initializes the driver. Returns an error if there was an 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 @@ -42,23 +48,35 @@ func (td *TermboxDriver) WaitForEvent() Event { switch tev.Type { case termbox.EventResize: - return Event{EventResize, 0} + 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, tev.Ch} + return Event{EventKeyPress, mod, tev.Ch} } else if spec, hasSpec := termboxKeysToSpecialKeys[tev.Key] ; hasSpec { - return Event{EventKeyPress, spec} + return Event{EventKeyPress, mod, spec} } else { - return Event{EventNone, 0} + return Event{EventNone, mod, 0} } 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 var termboxKeysToSpecialKeys = map[termbox.Key]rune { + termbox.KeySpace: ' ', + termbox.KeyF1: KeyF1, termbox.KeyF2: KeyF2, termbox.KeyF3: KeyF3, @@ -114,6 +132,5 @@ var termboxKeysToSpecialKeys = map[termbox.Key]rune { termbox.KeyCtrl5: KeyCtrl5, termbox.KeyCtrl6: KeyCtrl6, termbox.KeyCtrl7: KeyCtrl7, - termbox.KeySpace: KeySpace, termbox.KeyCtrl8: KeyCtrl8, } diff --git a/ui/utils.go b/ui/utils.go index 12ff8cc..9c5089c 100644 --- a/ui/utils.go +++ b/ui/utils.go @@ -10,4 +10,24 @@ func intMax(x, y int) int { } else { 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 + } } \ No newline at end of file