From 33847a78c14cf586ec2033165362867f118d7390 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 26 Aug 2017 09:48:22 +1000 Subject: [PATCH] Have added prompting. --- commandmap.go | 170 +++++++++-------- frame.go | 132 ++++++++----- main.go | 25 +-- session.go | 81 ++++---- ui/drawable.go | 83 ++++---- ui/driver.go | 201 +++++++++---------- ui/grid.go | 455 +++++++++++++++++++++----------------------- ui/manager.go | 84 ++++---- ui/stdcomps.go | 205 ++++++++++---------- ui/termboxdriver.go | 185 +++++++++--------- 10 files changed, 847 insertions(+), 774 deletions(-) diff --git a/commandmap.go b/commandmap.go index 2ceeec4..bf96b76 100644 --- a/commandmap.go +++ b/commandmap.go @@ -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 - } -} \ No newline at end of file +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 + } +} diff --git a/frame.go b/frame.go index ba24516..da0321d 100644 --- a/frame.go +++ b/frame.go @@ -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) - } -} \ No newline at end of file + if frame.Session != nil { + frame.Session.KeyPressed(key, mod) + } +} diff --git a/main.go b/main.go index df39621..4c09459 100644 --- a/main.go +++ b/main.go @@ -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() } diff --git a/session.go b/session.go index fc374ba..0fb33b9 100644 --- a/session.go +++ b/session.go @@ -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) } diff --git a/ui/drawable.go b/ui/drawable.go index 78260ef..0480892 100644 --- a/ui/drawable.go +++ b/ui/drawable.go @@ -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) -} \ No newline at end of file + 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) +} diff --git a/ui/driver.go b/ui/driver.go index 97190a5..a43b860 100644 --- a/ui/driver.go +++ b/ui/driver.go @@ -3,140 +3,141 @@ package ui // The set of attributes a specific cell can have -type Attribute uint16 +type Attribute uint16 const ( - // Can have only one of these - ColorDefault Attribute = iota - ColorBlack - ColorRed - ColorGreen - ColorYellow - ColorBlue - ColorMagenta - ColorCyan - ColorWhite + // Can have only one of these + ColorDefault Attribute = iota + ColorBlack + ColorRed + ColorGreen + ColorYellow + ColorBlue + ColorMagenta + ColorCyan + ColorWhite ) // and zero or more of these (combined using OR '|') const ( - AttrBold Attribute = 1 << (iota + 9) - AttrUnderline - AttrReverse + AttrBold Attribute = 1 << (iota + 9) + AttrUnderline + AttrReverse ) - // Special keys const ( - KeyCtrlSpace rune = 0x8000 + iota - KeyCtrlA - KeyCtrlB - KeyCtrlC - KeyCtrlD - KeyCtrlE - KeyCtrlF - KeyCtrlG - KeyCtrlH - KeyCtrlI - KeyCtrlJ - KeyCtrlK - KeyCtrlL - KeyCtrlM - KeyCtrlN - KeyCtrlO - KeyCtrlP - KeyCtrlQ - KeyCtrlR - KeyCtrlS - KeyCtrlT - KeyCtrlU - KeyCtrlV - KeyCtrlW - KeyCtrlX - KeyCtrlY - KeyCtrlZ - KeyCtrl3 - KeyCtrl4 - KeyCtrl5 - KeyCtrl6 - KeyCtrl7 - KeyCtrl8 + KeyCtrlSpace rune = 0x8000 + iota + KeyCtrlA + KeyCtrlB + KeyCtrlC + KeyCtrlD + KeyCtrlE + KeyCtrlF + KeyCtrlG + KeyCtrlH + KeyCtrlI + KeyCtrlJ + KeyCtrlK + KeyCtrlL + KeyCtrlM + KeyCtrlN + KeyCtrlO + KeyCtrlP + KeyCtrlQ + KeyCtrlR + KeyCtrlS + KeyCtrlT + KeyCtrlU + KeyCtrlV + KeyCtrlW + KeyCtrlX + KeyCtrlY + KeyCtrlZ + KeyCtrl3 + KeyCtrl4 + KeyCtrl5 + KeyCtrl6 + KeyCtrl7 + KeyCtrl8 - KeyF1 - KeyF2 - KeyF3 - KeyF4 - KeyF5 - KeyF6 - KeyF7 - KeyF8 - KeyF9 - KeyF10 - KeyF11 - KeyF12 - KeyInsert - KeyDelete - KeyHome - KeyEnd - KeyPgup - KeyPgdn - KeyArrowUp - KeyArrowDown - KeyArrowLeft - KeyArrowRight + KeyF1 + KeyF2 + KeyF3 + KeyF4 + KeyF5 + KeyF6 + KeyF7 + KeyF8 + KeyF9 + KeyF10 + KeyF11 + KeyF12 + KeyInsert + KeyDelete + KeyHome + KeyEnd + KeyPgup + KeyPgdn + KeyArrowUp + KeyArrowDown + KeyArrowLeft + KeyArrowRight - KeyBackspace = KeyCtrlH - KeyBackspace2 = KeyCtrl8 - KeyEnter = KeyCtrlM + KeyBackspace = KeyCtrlH + KeyBackspace2 = KeyCtrl8 + KeyEnter = KeyCtrlM ) // The type of events supported by the driver -type EventType int +type EventType int const ( - EventNone EventType = iota + EventNone EventType = iota - // Event when the window is resized - EventResize - - // Event indicating a key press. The key is set in Ch and modifications - // are set in Or - EventKeyPress + // Event when the window is resized + EventResize + + // Event indicating a key press. The key is set in Ch and modifications + // are set in Or + EventKeyPress ) const ( - ModKeyAlt int = (1 << iota) + ModKeyAlt int = (1 << iota) ) // Data from an event callback. type Event struct { - Type EventType - Par int - Ch rune + Type EventType + Par int + Ch rune } - // The terminal driver interface. type Driver interface { - // Initializes the driver. Returns an error if there was an error - Init() error + // Initializes the driver. Returns an error if there was an error + Init() error - // Closes the driver - Close() + // Closes the driver + Close() - // Returns the size of the window. - Size() (int, int) + // Returns the size of the window. + Size() (int, int) - // Sets the value of a specific cell - SetCell(x, y int, ch rune, fg, bg Attribute) + // Sets the value of a specific cell + SetCell(x, y int, ch rune, fg, bg Attribute) - // Synchronizes the internal buffer with the real buffer - Sync() + // Synchronizes the internal buffer with the real buffer + Sync() - // Wait for an event - WaitForEvent() Event + // Wait for an event + WaitForEvent() Event - // Move the position of the cursor - SetCursor(x, y int) + // Move the position of the cursor + SetCursor(x, y int) + + // Hide the cursor + HideCursor() } diff --git a/ui/grid.go b/ui/grid.go index 92ead9a..4ff0c8a 100644 --- a/ui/grid.go +++ b/ui/grid.go @@ -5,407 +5,396 @@ package ui import "strconv" - /** * An abstract display model. */ type GridModel interface { - /** - * Returns the size of the grid model (width x height) - */ - Dimensions() (int, int) + /** + * Returns the size of the grid model (width x height) + */ + Dimensions() (int, int) - /** - * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. - */ - ColWidth(int) int + /** + * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. + */ + ColWidth(int) int - /** - * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. - */ - RowHeight(int) int + /** + * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. + */ + RowHeight(int) int - /** - * Returns the value of the cell a position X, Y - */ - CellValue(int, int) string + /** + * Returns the value of the cell a position X, Y + */ + CellValue(int, int) string } - -type gridPoint int +type gridPoint int /** * The grid component. */ type Grid struct { - model GridModel // The grid model + model GridModel // The grid model - viewCellX int // Left most cell - viewCellY int // Top most cell - selCellX int // The currently selected cell - selCellY int - cellsWide int // Measured number of cells. Recalculated on redraw. - cellsHigh int + viewCellX int // Left most cell + viewCellY int // Top most cell + selCellX int // The currently selected cell + selCellY int + cellsWide int // Measured number of cells. Recalculated on redraw. + cellsHigh int } /** * Clipping rectangle */ type gridRect struct { - x1 gridPoint - y1 gridPoint - x2 gridPoint - y2 gridPoint + x1 gridPoint + y1 gridPoint + x2 gridPoint + y2 gridPoint } /** * Creates a new gridRect from integers. */ func newGridRect(x1, y1, x2, y2 int) gridRect { - return gridRect{gridPoint(x1), gridPoint(y1), gridPoint(x2), gridPoint(y2)} + return gridRect{gridPoint(x1), gridPoint(y1), gridPoint(x2), gridPoint(y2)} } - /** * Creates a new grid. */ func NewGrid(model GridModel) *Grid { - return &Grid{model, 0, 0, 0, 0, -1, -1} + return &Grid{model, 0, 0, 0, 0, -1, -1} } // Returns the model func (grid *Grid) Model() GridModel { - return grid.model + return grid.model } // Sets the model func (grid *Grid) SetModel(model GridModel) { - grid.model = model + grid.model = model } /** * Shifts the viewport of the grid. */ func (grid *Grid) ShiftBy(x int, y int) { - grid.viewCellX += x - grid.viewCellY += y + grid.viewCellX += x + grid.viewCellY += y } - // Returns the display value of the currently selected cell. func (grid *Grid) CurrentCellDisplayValue() string { - if grid.isCellValid(grid.selCellX, grid.selCellY) { - return grid.model.CellValue(grid.selCellX, grid.selCellY) - } else { - return "" - } + if grid.isCellValid(grid.selCellX, grid.selCellY) { + return grid.model.CellValue(grid.selCellX, grid.selCellY) + } else { + return "" + } } -// Moves the currently selected cell by a delta. This will be implemented as single stepped +// Moves the currently selected cell by a delta. This will be implemented as single stepped // moveTo calls to handle invalid cells. func (grid *Grid) MoveBy(x int, y int) { - grid.MoveTo(grid.selCellX + x, grid.selCellY + y) + grid.MoveTo(grid.selCellX+x, grid.selCellY+y) } // Moves the currently selected cell to a specific row. The row must be valid, otherwise the // currently selected cell will not be changed. Returns true if the move was successful func (grid *Grid) MoveTo(newX, newY int) { - maxX, maxY := grid.model.Dimensions() - newX = intMinMax(newX, 0, maxX - 1) - newY = intMinMax(newY, 0, maxY - 1) + maxX, maxY := grid.model.Dimensions() + newX = intMinMax(newX, 0, maxX-1) + newY = intMinMax(newY, 0, maxY-1) - if grid.isCellValid(newX, newY) { - grid.selCellX = newX - grid.selCellY = newY - grid.reposition() - } + if grid.isCellValid(newX, newY) { + grid.selCellX = newX + grid.selCellY = newY + grid.reposition() + } } // Returns the currently selected cell position. func (grid *Grid) CellPosition() (int, int) { - return grid.selCellX, grid.selCellY + return grid.selCellX, grid.selCellY } // Returns true if the user can enter the specific cell func (grid *Grid) isCellValid(x int, y int) bool { - maxX, maxY := grid.model.Dimensions() - return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY) + maxX, maxY := grid.model.Dimensions() + return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY) } - // Determine the topmost cell based on the location of the currently selected cell func (grid *Grid) reposition() { - // If we have no measurement information, forget it. - if (grid.cellsWide == -1) || (grid.cellsHigh == -1) { - return - } + // If we have no measurement information, forget it. + if (grid.cellsWide == -1) || (grid.cellsHigh == -1) { + return + } - if grid.selCellX < grid.viewCellX { - grid.viewCellX = grid.selCellX - } else if grid.selCellX >= (grid.viewCellX + grid.cellsWide - 3) { - grid.viewCellX = grid.selCellX - (grid.cellsWide - 3) - } + if grid.selCellX < grid.viewCellX { + grid.viewCellX = grid.selCellX + } else if grid.selCellX >= (grid.viewCellX + grid.cellsWide - 3) { + grid.viewCellX = grid.selCellX - (grid.cellsWide - 3) + } - if grid.selCellY < grid.viewCellY { - grid.viewCellY = grid.selCellY - } else if grid.selCellY >= (grid.viewCellY + grid.cellsHigh - 3) { - grid.viewCellY = grid.selCellY - (grid.cellsHigh - 3) - } + if grid.selCellY < grid.viewCellY { + grid.viewCellY = grid.selCellY + } else if grid.selCellY >= (grid.viewCellY + grid.cellsHigh - 3) { + grid.viewCellY = grid.selCellY - (grid.cellsHigh - 3) + } } - // Gets the cell value and attributes of a particular cell func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) { - // The fixed cells - modelCellX := cellX - 1 + grid.viewCellX - modelCellY := cellY - 1 + grid.viewCellY - modelMaxX, modelMaxY := grid.model.Dimensions() - - if (cellX == 0) && (cellY == 0) { - return "", AttrBold, AttrBold - } else if (cellX == 0) { - if (modelCellY == grid.selCellY) { - return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrBold | AttrReverse - } else { - return strconv.Itoa(modelCellY), AttrBold, AttrBold - } - } else if (cellY == 0) { - if (modelCellX == grid.selCellX) { - return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrBold | AttrReverse - } else { - return strconv.Itoa(modelCellX), AttrBold, AttrBold - } - } else { - // The data from the model - if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) { - if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) { - return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse - } else { - return grid.model.CellValue(modelCellX, modelCellY), 0, 0 - } - } else { - return "~", ColorBlue | AttrBold, 0 - } - } + // The fixed cells + modelCellX := cellX - 1 + grid.viewCellX + modelCellY := cellY - 1 + grid.viewCellY + modelMaxX, modelMaxY := grid.model.Dimensions() + + if (cellX == 0) && (cellY == 0) { + return "", AttrBold, AttrBold + } else if cellX == 0 { + if modelCellY == grid.selCellY { + return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrReverse + } else { + return strconv.Itoa(modelCellY), AttrBold, 0 + } + } else if cellY == 0 { + if modelCellX == grid.selCellX { + return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrReverse + } else { + return strconv.Itoa(modelCellX), AttrBold, 0 + } + } else { + // The data from the model + if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) { + if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) { + return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse + } else { + return grid.model.CellValue(modelCellX, modelCellY), 0, 0 + } + } else { + return "~", ColorBlue | AttrBold, 0 + } + } } // Gets the cell dimensions func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) { - var cellWidth, cellHeight int - - modelCellX := cellX - 1 + grid.viewCellX - modelCellY := cellY - 1 + grid.viewCellY - modelMaxX, modelMaxY := grid.model.Dimensions() - - // Get the cell width & height from model (if within range) - if (modelCellX >= 0) && (modelCellX < modelMaxX) { - cellWidth = grid.model.ColWidth(modelCellX) - } else { - cellWidth = 8 - } - - if (modelCellY >= 0) && (modelCellY < modelMaxY) { - cellHeight = grid.model.RowHeight(modelCellY) - } else { - cellHeight = 2 - } - - if (cellX == 0) && (cellY == 0) { - return 8, 1 - } else if (cellX == 0) { - return 8, cellHeight - } else if (cellY == 0) { - return cellWidth, 1 - } else { - return cellWidth, cellHeight - } - - // XXX: Workaround for bug in compiler - panic("Unreachable code") - return 0, 0 -} + var cellWidth, cellHeight int + modelCellX := cellX - 1 + grid.viewCellX + modelCellY := cellY - 1 + grid.viewCellY + modelMaxX, modelMaxY := grid.model.Dimensions() + + // Get the cell width & height from model (if within range) + if (modelCellX >= 0) && (modelCellX < modelMaxX) { + cellWidth = grid.model.ColWidth(modelCellX) + } else { + cellWidth = 8 + } + + if (modelCellY >= 0) && (modelCellY < modelMaxY) { + cellHeight = grid.model.RowHeight(modelCellY) + } else { + cellHeight = 2 + } + + if (cellX == 0) && (cellY == 0) { + return 8, 1 + } else if cellX == 0 { + return 8, cellHeight + } else if cellY == 0 { + return cellWidth, 1 + } else { + return cellWidth, cellHeight + } + + // XXX: Workaround for bug in compiler + panic("Unreachable code") + return 0, 0 +} /** * Renders a cell which contains text. The clip rectangle defines the size of the cell, as well as the left offset * of the cell. The sx and sy determine the screen position of the cell top-left. */ func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy int, text string, fg, bg Attribute) { - for x := cellClipRect.x1; x <= cellClipRect.x2; x++ { - for y := cellClipRect.y1; y <= cellClipRect.y2; y++ { - currRune := ' ' - if (y == 0) { - textPos := int(x) - if textPos < len(text) { - currRune = rune(text[textPos]) - } - } - - // TODO: This might be better if this wasn't so low-level - ctx.DrawRuneWithAttrs(int(x - cellClipRect.x1) + sx, int(y - cellClipRect.y1) + sy, currRune, fg, bg) - } - } -} + for x := cellClipRect.x1; x <= cellClipRect.x2; x++ { + for y := cellClipRect.y1; y <= cellClipRect.y2; y++ { + currRune := ' ' + if y == 0 { + textPos := int(x) + if textPos < len(text) { + currRune = rune(text[textPos]) + } + } + // TODO: This might be better if this wasn't so low-level + ctx.DrawRuneWithAttrs(int(x-cellClipRect.x1)+sx, int(y-cellClipRect.y1)+sy, currRune, fg, bg) + } + } +} // Renders a column. The viewport determines the maximum position of the rendered cell. CellX and CellY are the // cell indicies to render, cellOffset are the LOCAL offset of the cell. // This function will return the new X position (gridRect.x1 + colWidth) func (grid *Grid) renderColumn(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (gridPoint, int) { - // The top-left position of the column - screenX := int(screenViewPort.x1) - screenY := int(screenViewPort.y1) - screenWidth := int(screenViewPort.x2 - screenViewPort.x1) - screenHeight := int(screenViewPort.y2 - screenViewPort.y1) + // The top-left position of the column + screenX := int(screenViewPort.x1) + screenY := int(screenViewPort.y1) + screenWidth := int(screenViewPort.x2 - screenViewPort.x1) + screenHeight := int(screenViewPort.y2 - screenViewPort.y1) - // Work out the column width and cap it if it will spill over the edge of the viewport - colWidth, _ := grid.getCellDimensions(cellX, cellY) - colWidth -= cellOffsetX - if colWidth > screenWidth { - colWidth = screenHeight - } + // Work out the column width and cap it if it will spill over the edge of the viewport + colWidth, _ := grid.getCellDimensions(cellX, cellY) + colWidth -= cellOffsetX + if colWidth > screenWidth { + colWidth = screenHeight + } - // The maximum - maxScreenY := screenY + screenHeight - cellsHigh := 0 + // The maximum + maxScreenY := screenY + screenHeight + cellsHigh := 0 - for screenY < maxScreenY { + for screenY < maxScreenY { - // Cap the row height if it will go beyond the edge of the viewport. - _, rowHeight := grid.getCellDimensions(cellX, cellY) - if screenY + rowHeight > maxScreenY { - rowHeight = maxScreenY - screenY - } - - cellText, cellFg, cellBg := grid.getCellData(cellX, cellY) + // Cap the row height if it will go beyond the edge of the viewport. + _, rowHeight := grid.getCellDimensions(cellX, cellY) + if screenY+rowHeight > maxScreenY { + rowHeight = maxScreenY - screenY + } - grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth - cellOffsetX, rowHeight), - screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse + cellText, cellFg, cellBg := grid.getCellData(cellX, cellY) - cellY++ - cellsHigh++ - screenY = screenY + rowHeight - cellOffsetY - cellOffsetY = 0 - } + grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth-cellOffsetX, rowHeight), + screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse - return gridPoint(screenX + colWidth), cellsHigh + cellY++ + cellsHigh++ + screenY = screenY + rowHeight - cellOffsetY + cellOffsetY = 0 + } + + return gridPoint(screenX + colWidth), cellsHigh } - // Renders the grid. Returns the number of cells in the X and Y direction were rendered. // func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (int, int) { - - var cellsHigh = 0 - var cellsWide = 0 - for screenViewPort.x1 < screenViewPort.x2 { - screenViewPort.x1, cellsHigh = grid.renderColumn(ctx, screenViewPort, cellX, cellY, cellOffsetX, cellOffsetY) - cellX = cellX + 1 - cellsWide++ - cellOffsetX = 0 - } + var cellsHigh = 0 + var cellsWide = 0 - return cellsWide, cellsHigh + for screenViewPort.x1 < screenViewPort.x2 { + screenViewPort.x1, cellsHigh = grid.renderColumn(ctx, screenViewPort, cellX, cellY, cellOffsetX, cellOffsetY) + cellX = cellX + 1 + cellsWide++ + cellOffsetX = 0 + } + + return cellsWide, cellsHigh } - /** * Returns the cell of the particular point, along with the top-left position of the cell. */ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, posY int) { - var wid, hei int = grid.model.Dimensions() - posX = 0 - posY = 0 + var wid, hei int = grid.model.Dimensions() + posX = 0 + posY = 0 - cellX = -1 - cellY = -1 + cellX = -1 + cellY = -1 - // Go through columns to locate the particular cellX - for cx := 0; cx < wid; cx++ { - if (x >= posX) && (x < posX + grid.model.ColWidth(cx)) { - // We found the X position - cellX = int(cx) - break - } - } + // Go through columns to locate the particular cellX + for cx := 0; cx < wid; cx++ { + if (x >= posX) && (x < posX+grid.model.ColWidth(cx)) { + // We found the X position + cellX = int(cx) + break + } + } - for cy := 0; cy < hei; cy++ { - if (y >= posY) && (y < posY + grid.model.RowHeight(cy)) { - // And the Y position - cellY = int(cy) - break - } - } + for cy := 0; cy < hei; cy++ { + if (y >= posY) && (y < posY+grid.model.RowHeight(cy)) { + // And the Y position + cellY = int(cy) + break + } + } - return + return } /** * Returns the requested dimensions of a grid (as required by UiComponent) */ func (grid *Grid) Remeasure(w, h int) (int, int) { - return w, h + return w, h } /** * Redraws the grid. */ func (grid *Grid) Redraw(ctx *DrawContext) { - viewportRect := newGridRect(0, 0, ctx.W, ctx.H) - grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0) + viewportRect := newGridRect(0, 0, ctx.W, ctx.H) + grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0) } // Called when the component has focus and a key has been pressed. // This is the default behaviour of the grid, but it is not used by the main grid. func (grid *Grid) KeyPressed(key rune, mod int) { - // TODO: Not sure if this would be better handled using commands - if (key == 'i') || (key == KeyArrowUp) { - grid.MoveBy(0, -1) - } else if (key == 'k') || (key == KeyArrowDown) { - grid.MoveBy(0, 1) - } else if (key == 'j') || (key == KeyArrowLeft) { - grid.MoveBy(-1, 0) - } else if (key == 'l') || (key == KeyArrowRight) { - grid.MoveBy(1, 0) - } + // TODO: Not sure if this would be better handled using commands + if (key == 'i') || (key == KeyArrowUp) { + grid.MoveBy(0, -1) + } else if (key == 'k') || (key == KeyArrowDown) { + grid.MoveBy(0, 1) + } else if (key == 'j') || (key == KeyArrowLeft) { + grid.MoveBy(-1, 0) + } else if (key == 'l') || (key == KeyArrowRight) { + grid.MoveBy(1, 0) + } } - // -------------------------------------------------------------------------------------------- // Test Model type TestModel struct { - thing int + thing int } /** * Returns the size of the grid model (width x height) */ func (model *TestModel) Dimensions() (int, int) { - return 100, 100 + return 100, 100 } /** * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. */ func (model *TestModel) ColWidth(int) int { - return 16 + return 16 } /** * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. */ func (model *TestModel) RowHeight(int) int { - return 1 + return 1 } /** * Returns the value of the cell a position X, Y */ func (model *TestModel) CellValue(x int, y int) string { - return strconv.Itoa(x) + "," + strconv.Itoa(y) + return strconv.Itoa(x) + "," + strconv.Itoa(y) } diff --git a/ui/manager.go b/ui/manager.go index b9174b8..979b511 100644 --- a/ui/manager.go +++ b/ui/manager.go @@ -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() - } - } -} \ No newline at end of file + // HACK: Find another way to refresh the size of the screen to prevent a full redraw. + ui.driver.Sync() + } + } +} diff --git a/ui/stdcomps.go b/ui/stdcomps.go index 5a7eb78..77d420d 100644 --- a/ui/stdcomps.go +++ b/ui/stdcomps.go @@ -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)) -} \ No newline at end of file + te.cursorOffset = intMinMax(toX, 0, len(te.value)) +} diff --git a/ui/termboxdriver.go b/ui/termboxdriver.go index 088c995..a7a9d26 100644 --- a/ui/termboxdriver.go +++ b/ui/termboxdriver.go @@ -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, }