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,33 +1,27 @@
package main package main
import ( import (
"./ui" "bitbucket.org/lmika/ted-v2/ui"
) )
const ( const (
ModAlt rune = 1 << 31 - 1 ModAlt rune = 1<<31 - 1
) )
// A command // A command
type Command struct { type Command struct {
Name string Name string
Doc string Doc string
Action func(ctx CommandContext) error
// TODO: Add argument mapping which will fetch properties from the environment
Action func(ctx *CommandContext) error
} }
// Execute the command // Execute the command
func (cmd *Command) Do(ctx CommandContext) error { func (cmd *Command) Do(ctx *CommandContext) error {
return cmd.Action(ctx) return cmd.Action(ctx)
} }
// The command context
type CommandContext interface {
// Returns the current frame. If no frame is defined, returns nil.
Frame() *Frame
}
// A command mapping // A command mapping
type CommandMapping struct { type CommandMapping struct {
Commands map[string]*Command Commands map[string]*Command
@ -40,7 +34,7 @@ func NewCommandMapping() *CommandMapping {
} }
// Adds a new command // Adds a new command
func (cm *CommandMapping) Define(name string, doc string, opts string, fn func(ctx CommandContext) error) { func (cm *CommandMapping) Define(name string, doc string, opts string, fn func(ctx *CommandContext) error) {
cm.Commands[name] = &Command{name, doc, fn} cm.Commands[name] = &Command{name, doc, fn}
} }
@ -79,7 +73,7 @@ func (cm *CommandMapping) RegisterViewCommands() {
cm.Define("row-bottom", "Moves the cursor to the bottom of the row", "", gridNavOperation(func(grid *ui.Grid) { cm.Define("row-bottom", "Moves the cursor to the bottom of the row", "", gridNavOperation(func(grid *ui.Grid) {
cellX, _ := grid.CellPosition() cellX, _ := grid.CellPosition()
_, dimY := grid.Model().Dimensions() _, dimY := grid.Model().Dimensions()
grid.MoveTo(cellX, dimY - 1) grid.MoveTo(cellX, dimY-1)
})) }))
cm.Define("col-left", "Moves the cursor to the left-most column", "", gridNavOperation(func(grid *ui.Grid) { cm.Define("col-left", "Moves the cursor to the left-most column", "", gridNavOperation(func(grid *ui.Grid) {
_, cellY := grid.CellPosition() _, cellY := grid.CellPosition()
@ -89,8 +83,31 @@ func (cm *CommandMapping) RegisterViewCommands() {
cm.Define("col-right", "Moves the cursor to the right-most column", "", gridNavOperation(func(grid *ui.Grid) { cm.Define("col-right", "Moves the cursor to the right-most column", "", gridNavOperation(func(grid *ui.Grid) {
_, cellY := grid.CellPosition() _, cellY := grid.CellPosition()
dimX, _ := grid.Model().Dimensions() dimX, _ := grid.Model().Dimensions()
grid.MoveTo(dimX - 1, cellY) 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 // Registers the standard view key bindings. These commands require the frame
@ -113,14 +130,17 @@ func (cm *CommandMapping) RegisterViewKeyBindings() {
cm.MapKey(ui.KeyArrowLeft, cm.Command("move-left")) cm.MapKey(ui.KeyArrowLeft, cm.Command("move-left"))
cm.MapKey(ui.KeyArrowRight, cm.Command("move-right")) 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 // 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. // will display the cell value in the message box.
func gridNavOperation(op func(grid *ui.Grid)) func(ctx CommandContext) error { func gridNavOperation(op func(grid *ui.Grid)) func(ctx *CommandContext) error {
return func(ctx CommandContext) error { return func(ctx *CommandContext) error {
op(ctx.Frame().Grid()) op(ctx.Frame().Grid())
ctx.Frame().ShowCellValue() ctx.Frame().ShowCellValue()
return nil return nil

View file

@ -1,20 +1,27 @@
package main package main
import ( import (
"./ui" "bitbucket.org/lmika/ted-v2/ui"
) )
type Mode int type Mode int
const ( const (
NilMode Mode = iota
// The grid is selectable // The grid is selectable
GridMode Mode = iota GridMode
// EntryMode is when the text entry is selected
EntryMode
) )
// A frame is a UI instance. // A frame is a UI instance.
type Frame struct { type Frame struct {
Session *Session Session *Session
mode Mode
uiManager *ui.Ui uiManager *ui.Ui
clientArea *ui.RelativeLayout clientArea *ui.RelativeLayout
grid *ui.Grid grid *ui.Grid
@ -41,7 +48,7 @@ func NewFrame(uiManager *ui.Ui) *Frame {
statusLayout.Append(frame.statusBar) statusLayout.Append(frame.statusBar)
statusLayout.Append(frame.textEntrySwitch) statusLayout.Append(frame.textEntrySwitch)
frame.clientArea = &ui.RelativeLayout{ Client: frame.grid, South: statusLayout } frame.clientArea = &ui.RelativeLayout{Client: frame.grid, South: statusLayout}
return frame return frame
} }
@ -60,14 +67,51 @@ func (frame *Frame) Grid() *ui.Grid {
return frame.grid return frame.grid
} }
// Sets the specific mode. // Enter the specific mode.
func (frame *Frame) EnterMode(mode Mode) { func (frame *Frame) enterMode(mode Mode) {
switch mode { switch mode {
case GridMode: case GridMode:
frame.uiManager.SetFocusedComponent(frame) 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 // Show a message. This will switch the bottom to the messageView and select the frame
func (frame *Frame) ShowMessage(msg string) { func (frame *Frame) ShowMessage(msg string) {
frame.messageView.Text = msg frame.messageView.Text = msg

View file

@ -1,7 +1,7 @@
package main package main
import ( import (
"./ui" "bitbucket.org/lmika/ted-v2/ui"
) )
func main() { func main() {
@ -12,12 +12,13 @@ func main() {
defer uiManager.Close() defer uiManager.Close()
model := &StdModel{} model := &StdModel{}
model.Resize(5, 5)
frame := NewFrame(uiManager) frame := NewFrame(uiManager)
NewSession(frame, model) NewSession(uiManager, frame, model)
uiManager.SetRootComponent(frame.RootComponent()) uiManager.SetRootComponent(frame.RootComponent())
frame.EnterMode(GridMode) frame.enterMode(GridMode)
uiManager.Loop() uiManager.Loop()
} }

View file

@ -1,6 +1,6 @@
package main 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 session is responsible for managing the UI and the model and handling
// the interaction between the two and the user. // the interaction between the two and the user.
@ -8,13 +8,15 @@ type Session struct {
Model Model Model Model
Frame *Frame Frame *Frame
Commands *CommandMapping Commands *CommandMapping
UIManager *ui.Ui
} }
func NewSession(frame *Frame, model Model) *Session { func NewSession(uiManager *ui.Ui, frame *Frame, model Model) *Session {
session := &Session{ session := &Session{
Model: model, Model: model,
Frame: frame, Frame: frame,
Commands: NewCommandMapping(), Commands: NewCommandMapping(),
UIManager: uiManager,
} }
frame.SetModel(&SessionGridModel{session}) frame.SetModel(&SessionGridModel{session})
@ -31,30 +33,31 @@ func NewSession(frame *Frame, model Model) *Session {
// Input from the frame // Input from the frame
func (session *Session) KeyPressed(key rune, mod int) { func (session *Session) KeyPressed(key rune, mod int) {
// Add the mod key modifier // Add the mod key modifier
if (mod & ui.ModKeyAlt != 0) { if mod&ui.ModKeyAlt != 0 {
key |= ModAlt key |= ModAlt
} }
cmd := session.Commands.KeyMapping(key) cmd := session.Commands.KeyMapping(key)
if cmd != nil { if cmd != nil {
err := cmd.Do(SessionCommandContext{session}) err := cmd.Do(&CommandContext{session})
if err != nil { if err != nil {
session.Frame.ShowMessage(err.Error()) session.Frame.ShowMessage(err.Error())
} }
} }
} }
// The command context used by the session // The command context used by the session
type SessionCommandContext struct { type CommandContext struct {
Session *Session session *Session
} }
func (scc SessionCommandContext) Frame() *Frame { func (scc *CommandContext) Session() *Session {
return scc.Session.Frame return scc.session
} }
func (scc *CommandContext) Frame() *Frame {
return scc.session.Frame
}
// Session grid model // Session grid model
type SessionGridModel struct { type SessionGridModel struct {

View file

@ -19,12 +19,11 @@ type DrawContext struct {
fa, ba Attribute fa, ba Attribute
} }
// Returns a new subcontext. The sub-context must be an area within the current context. // 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 { func (dc *DrawContext) NewSubContext(offsetX, offsetY, width, height int) *DrawContext {
return &DrawContext{ return &DrawContext{
X: intMax(dc.X + offsetX, dc.X), X: intMax(dc.X+offsetX, dc.X),
Y: intMax(dc.Y + offsetY, dc.Y), Y: intMax(dc.Y+offsetY, dc.Y),
W: intMax(width, 0), W: intMax(width, 0),
H: intMax(height, 0), H: intMax(height, 0),
@ -42,7 +41,6 @@ func (dc *DrawContext) SetBgAttr(attr Attribute) {
dc.ba = attr dc.ba = attr
} }
// Draws a horizontal rule with a specific rune. // Draws a horizontal rule with a specific rune.
func (dc *DrawContext) HorizRule(y int, ch rune) { func (dc *DrawContext) HorizRule(y int, ch rune) {
for x := 0; x < dc.W; x++ { for x := 0; x < dc.W; x++ {
@ -61,7 +59,7 @@ func (dc *DrawContext) Print(x, y int, str string) {
// Prints a right-justified string at a specific offset. This will be bounded by the size of the drawing context. // 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) { func (dc *DrawContext) PrintRight(x, y int, str string) {
l := len(str) l := len(str)
dc.Print(x - l, y, str) dc.Print(x-l, y, str)
} }
// Draws a rune at a local point X, Y with the current foreground and background attributes // Draws a rune at a local point X, Y with the current foreground and background attributes
@ -85,8 +83,13 @@ func (dc *DrawContext) SetCursorPosition(x, y int) {
} }
} }
// 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 // 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
return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X + dc.W) && (ry < dc.Y + dc.H) return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X+dc.W) && (ry < dc.Y+dc.H)
} }

View file

@ -25,7 +25,6 @@ const (
AttrReverse AttrReverse
) )
// Special keys // Special keys
const ( const (
KeyCtrlSpace rune = 0x8000 + iota KeyCtrlSpace rune = 0x8000 + iota
@ -115,7 +114,6 @@ type Event struct {
Ch rune Ch rune
} }
// The terminal driver interface. // The terminal driver interface.
type Driver interface { type Driver interface {
@ -139,4 +137,7 @@ type Driver interface {
// Move the position of the cursor // Move the position of the cursor
SetCursor(x, y int) SetCursor(x, y int)
// Hide the cursor
HideCursor()
} }

View file

@ -5,7 +5,6 @@ package ui
import "strconv" import "strconv"
/** /**
* An abstract display model. * An abstract display model.
*/ */
@ -31,7 +30,6 @@ type GridModel interface {
CellValue(int, int) string CellValue(int, int) string
} }
type gridPoint int type gridPoint int
/** /**
@ -65,7 +63,6 @@ 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. * Creates a new grid.
*/ */
@ -91,7 +88,6 @@ func (grid *Grid) ShiftBy(x int, y int) {
grid.viewCellY += y grid.viewCellY += y
} }
// Returns the display value of the currently selected cell. // Returns the display value of the currently selected cell.
func (grid *Grid) CurrentCellDisplayValue() string { func (grid *Grid) CurrentCellDisplayValue() string {
if grid.isCellValid(grid.selCellX, grid.selCellY) { if grid.isCellValid(grid.selCellX, grid.selCellY) {
@ -104,15 +100,15 @@ func (grid *Grid) CurrentCellDisplayValue() string {
// 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. // moveTo calls to handle invalid cells.
func (grid *Grid) MoveBy(x int, y int) { 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 // 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 // currently selected cell will not be changed. Returns true if the move was successful
func (grid *Grid) MoveTo(newX, newY int) { func (grid *Grid) MoveTo(newX, newY int) {
maxX, maxY := grid.model.Dimensions() maxX, maxY := grid.model.Dimensions()
newX = intMinMax(newX, 0, maxX - 1) newX = intMinMax(newX, 0, maxX-1)
newY = intMinMax(newY, 0, maxY - 1) newY = intMinMax(newY, 0, maxY-1)
if grid.isCellValid(newX, newY) { if grid.isCellValid(newX, newY) {
grid.selCellX = newX grid.selCellX = newX
@ -132,7 +128,6 @@ func (grid *Grid) isCellValid(x int, y int) bool {
return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY) return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY)
} }
// Determine the topmost cell based on the location of the currently selected cell // Determine the topmost cell based on the location of the currently selected cell
func (grid *Grid) reposition() { func (grid *Grid) reposition() {
@ -154,7 +149,6 @@ func (grid *Grid) reposition() {
} }
} }
// Gets the cell value and attributes of a particular cell // Gets the cell value and attributes of a particular cell
func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) { func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) {
// The fixed cells // The fixed cells
@ -164,17 +158,17 @@ func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute)
if (cellX == 0) && (cellY == 0) { if (cellX == 0) && (cellY == 0) {
return "", AttrBold, AttrBold return "", AttrBold, AttrBold
} else if (cellX == 0) { } else if cellX == 0 {
if (modelCellY == grid.selCellY) { if modelCellY == grid.selCellY {
return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrBold | AttrReverse return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrReverse
} else { } else {
return strconv.Itoa(modelCellY), AttrBold, AttrBold return strconv.Itoa(modelCellY), AttrBold, 0
} }
} else if (cellY == 0) { } else if cellY == 0 {
if (modelCellX == grid.selCellX) { if modelCellX == grid.selCellX {
return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrBold | AttrReverse return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrReverse
} else { } else {
return strconv.Itoa(modelCellX), AttrBold, AttrBold return strconv.Itoa(modelCellX), AttrBold, 0
} }
} else { } else {
// The data from the model // The data from the model
@ -214,9 +208,9 @@ func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) {
if (cellX == 0) && (cellY == 0) { if (cellX == 0) && (cellY == 0) {
return 8, 1 return 8, 1
} else if (cellX == 0) { } else if cellX == 0 {
return 8, cellHeight return 8, cellHeight
} else if (cellY == 0) { } else if cellY == 0 {
return cellWidth, 1 return cellWidth, 1
} else { } else {
return cellWidth, cellHeight return cellWidth, cellHeight
@ -227,7 +221,6 @@ func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) {
return 0, 0 return 0, 0
} }
/** /**
* Renders a cell which contains text. The clip rectangle defines the size of the cell, as well as the left offset * 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. * of the cell. The sx and sy determine the screen position of the cell top-left.
@ -236,7 +229,7 @@ func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy
for x := cellClipRect.x1; x <= cellClipRect.x2; x++ { for x := cellClipRect.x1; x <= cellClipRect.x2; x++ {
for y := cellClipRect.y1; y <= cellClipRect.y2; y++ { for y := cellClipRect.y1; y <= cellClipRect.y2; y++ {
currRune := ' ' currRune := ' '
if (y == 0) { if y == 0 {
textPos := int(x) textPos := int(x)
if textPos < len(text) { if textPos < len(text) {
currRune = rune(text[textPos]) currRune = rune(text[textPos])
@ -244,12 +237,11 @@ func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy
} }
// TODO: This might be better if this wasn't so low-level // TODO: This might be better if this wasn't so low-level
ctx.DrawRuneWithAttrs(int(x - cellClipRect.x1) + sx, int(y - cellClipRect.y1) + sy, currRune, fg, bg) ctx.DrawRuneWithAttrs(int(x-cellClipRect.x1)+sx, int(y-cellClipRect.y1)+sy, currRune, fg, bg)
} }
} }
} }
// Renders a column. The viewport determines the maximum position of the rendered cell. CellX and CellY are the // 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. // cell indicies to render, cellOffset are the LOCAL offset of the cell.
// This function will return the new X position (gridRect.x1 + colWidth) // This function will return the new X position (gridRect.x1 + colWidth)
@ -276,13 +268,13 @@ func (grid *Grid) renderColumn(ctx *DrawContext, screenViewPort gridRect, cellX
// Cap the row height if it will go beyond the edge of the viewport. // Cap the row height if it will go beyond the edge of the viewport.
_, rowHeight := grid.getCellDimensions(cellX, cellY) _, rowHeight := grid.getCellDimensions(cellX, cellY)
if screenY + rowHeight > maxScreenY { if screenY+rowHeight > maxScreenY {
rowHeight = maxScreenY - screenY 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), grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth-cellOffsetX, rowHeight),
screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse
cellY++ cellY++
@ -294,7 +286,6 @@ func (grid *Grid) renderColumn(ctx *DrawContext, screenViewPort gridRect, cellX
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. // 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) { func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (int, int) {
@ -312,7 +303,6 @@ func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX in
return cellsWide, cellsHigh return cellsWide, cellsHigh
} }
/** /**
* Returns the cell of the particular point, along with the top-left position of the cell. * Returns the cell of the particular point, along with the top-left position of the cell.
*/ */
@ -326,7 +316,7 @@ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, pos
// Go through columns to locate the particular cellX // Go through columns to locate the particular cellX
for cx := 0; cx < wid; cx++ { for cx := 0; cx < wid; cx++ {
if (x >= posX) && (x < posX + grid.model.ColWidth(cx)) { if (x >= posX) && (x < posX+grid.model.ColWidth(cx)) {
// We found the X position // We found the X position
cellX = int(cx) cellX = int(cx)
break break
@ -334,7 +324,7 @@ func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, pos
} }
for cy := 0; cy < hei; cy++ { for cy := 0; cy < hei; cy++ {
if (y >= posY) && (y < posY + grid.model.RowHeight(cy)) { if (y >= posY) && (y < posY+grid.model.RowHeight(cy)) {
// And the Y position // And the Y position
cellY = int(cy) cellY = int(cy)
break break
@ -374,7 +364,6 @@ func (grid *Grid) KeyPressed(key rune, mod int) {
} }
} }
// -------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------
// Test Model // Test Model

View file

@ -3,7 +3,6 @@
package ui package ui
// The UI manager // The UI manager
type Ui struct { type Ui struct {
// The root component // The root component
@ -12,9 +11,9 @@ type Ui struct {
drawContext *DrawContext drawContext *DrawContext
driver Driver driver Driver
shutdown bool
} }
// Creates a new UI context. This also initializes the UI state. // Creates a new UI context. This also initializes the UI state.
// Returns the context and an error. // Returns the context and an error.
func NewUI() (*Ui, error) { func NewUI() (*Ui, error) {
@ -25,13 +24,12 @@ func NewUI() (*Ui, error) {
return nil, err return nil, err
} }
drawContext := &DrawContext{ driver: driver } drawContext := &DrawContext{driver: driver}
ui := &Ui{ drawContext: drawContext, driver: driver } ui := &Ui{drawContext: drawContext, driver: driver}
return ui, nil return ui, nil
} }
// Closes the UI context. // Closes the UI context.
func (ui *Ui) Close() { func (ui *Ui) Close() {
ui.driver.Close() ui.driver.Close()
@ -65,10 +63,14 @@ func (ui *Ui) Redraw() {
ui.driver.Sync() ui.driver.Sync()
} }
// Quit indicates to the UI that it should shutdown
func (ui *Ui) Shutdown() {
ui.shutdown = true
}
// Enter the UI loop // Enter the UI loop
func (ui *Ui) Loop() { func (ui *Ui) Loop() {
for { for !ui.shutdown {
ui.Redraw() ui.Redraw()
event := ui.driver.WaitForEvent() event := ui.driver.WaitForEvent()

View file

@ -4,7 +4,6 @@ package ui
import "unicode" import "unicode"
// A text component. This simply renders a text string. // A text component. This simply renders a text string.
type TextView struct { type TextView struct {
@ -24,10 +23,9 @@ func (tv *TextView) Redraw(context *DrawContext) {
context.HorizRule(0, ' ') context.HorizRule(0, ' ')
context.Print(0, 0, tv.Text) context.Print(0, 0, tv.Text)
context.HideCursor()
} }
// Status bar component. This component displays text on the left and right of it's // Status bar component. This component displays text on the left and right of it's
// allocated space. // allocated space.
type StatusBar struct { type StatusBar struct {
@ -50,7 +48,6 @@ func (sbar *StatusBar) Redraw(context *DrawContext) {
context.PrintRight(context.W, 0, sbar.Right) context.PrintRight(context.W, 0, sbar.Right)
} }
// A single-text entry component. // A single-text entry component.
type TextEntry struct { type TextEntry struct {
Prompt string Prompt string
@ -58,6 +55,9 @@ type TextEntry struct {
value string value string
cursorOffset int cursorOffset int
displayOffset int displayOffset int
// Called when the user presses Enter
OnEntry func(val string)
} }
func (te *TextEntry) Remeasure(w, h int) (int, int) { func (te *TextEntry) Remeasure(w, h int) (int, int) {
@ -77,8 +77,8 @@ func (te *TextEntry) Redraw(context *DrawContext) {
valueOffsetX = len(te.Prompt) valueOffsetX = len(te.Prompt)
} }
context.Print(valueOffsetX, 0, te.value[displayOffsetX:intMin(displayOffsetX + context.W,len(te.value))]) context.Print(valueOffsetX, 0, te.value[displayOffsetX:intMin(displayOffsetX+context.W, len(te.value))])
context.SetCursorPosition(te.cursorOffset + valueOffsetX - displayOffsetX, 0) 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))
} }
@ -91,37 +91,46 @@ func (te *TextEntry) calculateDisplayOffset(displayWidth int) int {
if virtualCursorOffset >= displayWidth { if virtualCursorOffset >= displayWidth {
te.displayOffset = te.cursorOffset - displayWidth + 10 te.displayOffset = te.cursorOffset - displayWidth + 10
} else if (virtualCursorOffset < 0) { } else if virtualCursorOffset < 0 {
te.displayOffset = intMax(te.cursorOffset - displayWidth + 1, 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) { func (te *TextEntry) KeyPressed(key rune, mod int) {
if (key >= ' ') && (key <= '~') { if (key >= ' ') && (key <= '~') {
te.insertRune(key) te.insertRune(key)
} else if (key == KeyArrowLeft) { } else if key == KeyArrowLeft {
te.moveCursorBy(-1) te.moveCursorBy(-1)
} else if (key == KeyArrowRight) { } else if key == KeyArrowRight {
te.moveCursorBy(1) te.moveCursorBy(1)
} else if (key == KeyHome) { } else if key == KeyHome {
te.moveCursorTo(0) te.moveCursorTo(0)
} else if (key == KeyEnd) { } else if key == KeyEnd {
te.moveCursorTo(len(te.value)) te.moveCursorTo(len(te.value))
} else if (key == KeyBackspace) || (key == KeyBackspace2) { } else if (key == KeyBackspace) || (key == KeyBackspace2) {
if (mod & ModKeyAlt != 0) { if mod&ModKeyAlt != 0 {
te.backspaceWhile(unicode.IsSpace) te.backspaceWhile(unicode.IsSpace)
te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) }) te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) })
} else { } else {
te.backspace() te.backspace()
} }
} else if (key == KeyCtrlK) { } else if key == KeyCtrlK {
te.killLine() te.killLine()
} else if (key == KeyDelete) { } else if key == KeyDelete {
te.removeCharAtPos(te.cursorOffset) te.removeCharAtPos(te.cursorOffset)
} else if (key == KeyEnter) { } else if key == KeyEnter {
panic("Entered text: '" + te.value + "'") //panic("Entered text: '" + te.value + "'")
if te.OnEntry != nil {
te.OnEntry(te.value)
}
} }
} }
@ -133,8 +142,8 @@ func (te *TextEntry) backspace() {
// Backspace while the character underneith the cursor matches the guard // Backspace while the character underneith the cursor matches the guard
func (te *TextEntry) backspaceWhile(guard func(r rune) bool) { func (te *TextEntry) backspaceWhile(guard func(r rune) bool) {
for (te.cursorOffset > 0) { for te.cursorOffset > 0 {
ch := rune(te.value[te.cursorOffset - 1]) ch := rune(te.value[te.cursorOffset-1])
if guard(ch) { if guard(ch) {
te.backspace() te.backspace()
} else { } else {
@ -146,7 +155,7 @@ func (te *TextEntry) backspaceWhile(guard func(r rune) bool) {
// Kill the line. If the cursor is at the end of the line, kill to the start. // Kill the line. If the cursor is at the end of the line, kill to the start.
// Otherwise, trim the line. // Otherwise, trim the line.
func (te *TextEntry) killLine() { func (te *TextEntry) killLine() {
if (te.cursorOffset < len(te.value)) { if te.cursorOffset < len(te.value) {
te.value = te.value[:te.cursorOffset] te.value = te.value[:te.cursorOffset]
} else { } else {
te.value = "" te.value = ""
@ -156,7 +165,7 @@ func (te *TextEntry) killLine() {
// Inserts a rune at the cursor position // Inserts a rune at the cursor position
func (te *TextEntry) insertRune(key rune) { func (te *TextEntry) insertRune(key rune) {
if (te.cursorOffset >= len(te.value)) { if te.cursorOffset >= len(te.value) {
te.value += string(key) te.value += string(key)
} else { } else {
te.value = te.value[:te.cursorOffset] + string(key) + te.value[te.cursorOffset:] te.value = te.value[:te.cursorOffset] + string(key) + te.value[te.cursorOffset:]

View file

@ -6,11 +6,9 @@ import (
"github.com/nsf/termbox-go" "github.com/nsf/termbox-go"
) )
type TermboxDriver struct { 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 {
err := termbox.Init() err := termbox.Init()
@ -37,6 +35,11 @@ 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 // Synchronizes the internal buffer with the real buffer
func (td *TermboxDriver) Sync() { func (td *TermboxDriver) Sync() {
termbox.Flush() termbox.Flush()
@ -51,12 +54,12 @@ func (td *TermboxDriver) WaitForEvent() Event {
return Event{EventResize, 0, 0} return Event{EventResize, 0, 0}
case termbox.EventKey: case termbox.EventKey:
mod := 0 mod := 0
if tev.Mod & termbox.ModAlt != 0 { if tev.Mod&termbox.ModAlt != 0 {
mod = ModKeyAlt mod = ModKeyAlt
} }
if tev.Ch != 0 { if tev.Ch != 0 {
return Event{EventKeyPress, mod, 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, mod, spec} return Event{EventKeyPress, mod, spec}
} else { } else {
return Event{EventNone, mod, 0} return Event{EventNone, mod, 0}
@ -71,10 +74,8 @@ func (td *TermboxDriver) SetCursor(x, y int) {
termbox.SetCursor(x, y) 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.KeySpace: ' ',
termbox.KeyF1: KeyF1, termbox.KeyF1: KeyF1,