Have added prompting.
This commit is contained in:
parent
665bc8cdae
commit
33847a78c1
168
commandmap.go
168
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
|
||||
}
|
||||
func gridNavOperation(op func(grid *ui.Grid)) func(ctx *CommandContext) error {
|
||||
return func(ctx *CommandContext) error {
|
||||
op(ctx.Frame().Grid())
|
||||
ctx.Frame().ShowCellValue()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
130
frame.go
130
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)
|
||||
}
|
||||
if frame.Session != nil {
|
||||
frame.Session.KeyPressed(key, mod)
|
||||
}
|
||||
}
|
||||
25
main.go
25
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()
|
||||
}
|
||||
|
|
|
|||
81
session.go
81
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,87 +6,90 @@ package ui
|
|||
// drawing within it's context.
|
||||
type DrawContext struct {
|
||||
|
||||
// The left and top position of the context.
|
||||
X, Y int
|
||||
// The left and top position of the context.
|
||||
X, Y int
|
||||
|
||||
// The width and height position of the context.
|
||||
W, H int
|
||||
// The width and height position of the context.
|
||||
W, H int
|
||||
|
||||
// The current driver
|
||||
driver Driver
|
||||
// The current driver
|
||||
driver Driver
|
||||
|
||||
// The current foregound and background attributes
|
||||
fa, ba Attribute
|
||||
// The current foregound and background attributes
|
||||
fa, ba Attribute
|
||||
}
|
||||
|
||||
|
||||
// Returns a new subcontext. The sub-context must be an area within the current context.
|
||||
func (dc *DrawContext) NewSubContext(offsetX, offsetY, width, height int) *DrawContext {
|
||||
return &DrawContext{
|
||||
X: intMax(dc.X + offsetX, dc.X),
|
||||
Y: intMax(dc.Y + offsetY, dc.Y),
|
||||
W: intMax(width, 0),
|
||||
H: intMax(height, 0),
|
||||
return &DrawContext{
|
||||
X: intMax(dc.X+offsetX, dc.X),
|
||||
Y: intMax(dc.Y+offsetY, dc.Y),
|
||||
W: intMax(width, 0),
|
||||
H: intMax(height, 0),
|
||||
|
||||
driver: dc.driver,
|
||||
}
|
||||
driver: dc.driver,
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the foreground attribute
|
||||
func (dc *DrawContext) SetFgAttr(attr Attribute) {
|
||||
dc.fa = attr
|
||||
dc.fa = attr
|
||||
}
|
||||
|
||||
// Sets the background attribute
|
||||
func (dc *DrawContext) SetBgAttr(attr Attribute) {
|
||||
dc.ba = attr
|
||||
dc.ba = attr
|
||||
}
|
||||
|
||||
|
||||
// Draws a horizontal rule with a specific rune.
|
||||
func (dc *DrawContext) HorizRule(y int, ch rune) {
|
||||
for x := 0; x < dc.W; x++ {
|
||||
dc.DrawRune(x, y, ch)
|
||||
}
|
||||
for x := 0; x < dc.W; x++ {
|
||||
dc.DrawRune(x, y, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// Prints a string at a specific offset. This will be bounded by the size of the drawing context.
|
||||
func (dc *DrawContext) Print(x, y int, str string) {
|
||||
for _, ch := range str {
|
||||
dc.DrawRune(x, y, ch)
|
||||
x++
|
||||
}
|
||||
for _, ch := range str {
|
||||
dc.DrawRune(x, y, ch)
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// Prints a right-justified string at a specific offset. This will be bounded by the size of the drawing context.
|
||||
func (dc *DrawContext) PrintRight(x, y int, str string) {
|
||||
l := len(str)
|
||||
dc.Print(x - l, y, str)
|
||||
l := len(str)
|
||||
dc.Print(x-l, y, str)
|
||||
}
|
||||
|
||||
// Draws a rune at a local point X, Y with the current foreground and background attributes
|
||||
func (dc *DrawContext) DrawRune(x, y int, ch rune) {
|
||||
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
|
||||
dc.driver.SetCell(rx, ry, ch, dc.fa, dc.ba)
|
||||
}
|
||||
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
|
||||
dc.driver.SetCell(rx, ry, ch, dc.fa, dc.ba)
|
||||
}
|
||||
}
|
||||
|
||||
// Draws a rune at a local point X, Y with specific foreground and background attributes
|
||||
func (dc *DrawContext) DrawRuneWithAttrs(x, y int, ch rune, fa, ba Attribute) {
|
||||
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
|
||||
dc.driver.SetCell(rx, ry, ch, fa, ba)
|
||||
}
|
||||
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
|
||||
dc.driver.SetCell(rx, ry, ch, fa, ba)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the position of the cursor
|
||||
func (dc *DrawContext) SetCursorPosition(x, y int) {
|
||||
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
|
||||
dc.driver.SetCursor(rx, ry)
|
||||
}
|
||||
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
|
||||
dc.driver.SetCursor(rx, ry)
|
||||
}
|
||||
}
|
||||
|
||||
// HideCursor hides the cursor
|
||||
func (dc *DrawContext) HideCursor() {
|
||||
dc.driver.HideCursor()
|
||||
}
|
||||
|
||||
// Converts a local point to a real point. If the point is within the context, also returns true
|
||||
func (dc *DrawContext) localPointToRealPoint(x, y int) (int, int, bool) {
|
||||
rx, ry := x + dc.X, y + dc.Y
|
||||
return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X + dc.W) && (ry < dc.Y + dc.H)
|
||||
rx, ry := x+dc.X, y+dc.Y
|
||||
return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X+dc.W) && (ry < dc.Y+dc.H)
|
||||
}
|
||||
199
ui/driver.go
199
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 when the window is resized
|
||||
EventResize
|
||||
|
||||
// Event indicating a key press. The key is set in Ch and modifications
|
||||
// are set in Or
|
||||
EventKeyPress
|
||||
// Event indicating a key press. The key is set in Ch and modifications
|
||||
// are set in Or
|
||||
EventKeyPress
|
||||
)
|
||||
|
||||
const (
|
||||
ModKeyAlt int = (1 << iota)
|
||||
ModKeyAlt int = (1 << iota)
|
||||
)
|
||||
|
||||
// Data from an event callback.
|
||||
type Event struct {
|
||||
Type EventType
|
||||
Par int
|
||||
Ch rune
|
||||
Type EventType
|
||||
Par int
|
||||
Ch rune
|
||||
}
|
||||
|
||||
|
||||
// The terminal driver interface.
|
||||
type Driver interface {
|
||||
|
||||
// Initializes the driver. Returns an error if there was an error
|
||||
Init() error
|
||||
// Initializes the driver. Returns an error if there was an error
|
||||
Init() error
|
||||
|
||||
// Closes the driver
|
||||
Close()
|
||||
// Closes the driver
|
||||
Close()
|
||||
|
||||
// Returns the size of the window.
|
||||
Size() (int, int)
|
||||
// Returns the size of the window.
|
||||
Size() (int, int)
|
||||
|
||||
// Sets the value of a specific cell
|
||||
SetCell(x, y int, ch rune, fg, bg Attribute)
|
||||
// Sets the value of a specific cell
|
||||
SetCell(x, y int, ch rune, fg, bg Attribute)
|
||||
|
||||
// Synchronizes the internal buffer with the real buffer
|
||||
Sync()
|
||||
// Synchronizes the internal buffer with the real buffer
|
||||
Sync()
|
||||
|
||||
// Wait for an event
|
||||
WaitForEvent() Event
|
||||
// Wait for an event
|
||||
WaitForEvent() Event
|
||||
|
||||
// Move the position of the cursor
|
||||
SetCursor(x, y int)
|
||||
// Move the position of the cursor
|
||||
SetCursor(x, y int)
|
||||
|
||||
// Hide the cursor
|
||||
HideCursor()
|
||||
}
|
||||
|
|
|
|||
435
ui/grid.go
435
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
|
||||
// moveTo calls to handle invalid cells.
|
||||
func (grid *Grid) MoveBy(x int, y int) {
|
||||
grid.MoveTo(grid.selCellX + x, grid.selCellY + y)
|
||||
grid.MoveTo(grid.selCellX+x, grid.selCellY+y)
|
||||
}
|
||||
|
||||
// Moves the currently selected cell to a specific row. The row must be valid, otherwise the
|
||||
// currently selected cell will not be changed. Returns true if the move was successful
|
||||
func (grid *Grid) MoveTo(newX, newY int) {
|
||||
maxX, maxY := grid.model.Dimensions()
|
||||
newX = intMinMax(newX, 0, maxX - 1)
|
||||
newY = intMinMax(newY, 0, maxY - 1)
|
||||
maxX, maxY := grid.model.Dimensions()
|
||||
newX = intMinMax(newX, 0, maxX-1)
|
||||
newY = intMinMax(newY, 0, maxY-1)
|
||||
|
||||
if grid.isCellValid(newX, newY) {
|
||||
grid.selCellX = newX
|
||||
grid.selCellY = newY
|
||||
grid.reposition()
|
||||
}
|
||||
if grid.isCellValid(newX, newY) {
|
||||
grid.selCellX = newX
|
||||
grid.selCellY = newY
|
||||
grid.reposition()
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the currently selected cell position.
|
||||
func (grid *Grid) CellPosition() (int, int) {
|
||||
return grid.selCellX, grid.selCellY
|
||||
return grid.selCellX, grid.selCellY
|
||||
}
|
||||
|
||||
// Returns true if the user can enter the specific cell
|
||||
func (grid *Grid) isCellValid(x int, y int) bool {
|
||||
maxX, maxY := grid.model.Dimensions()
|
||||
return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY)
|
||||
maxX, maxY := grid.model.Dimensions()
|
||||
return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY)
|
||||
}
|
||||
|
||||
|
||||
// Determine the topmost cell based on the location of the currently selected cell
|
||||
func (grid *Grid) reposition() {
|
||||
|
||||
// If we have no measurement information, forget it.
|
||||
if (grid.cellsWide == -1) || (grid.cellsHigh == -1) {
|
||||
return
|
||||
}
|
||||
// If we have no measurement information, forget it.
|
||||
if (grid.cellsWide == -1) || (grid.cellsHigh == -1) {
|
||||
return
|
||||
}
|
||||
|
||||
if grid.selCellX < grid.viewCellX {
|
||||
grid.viewCellX = grid.selCellX
|
||||
} else if grid.selCellX >= (grid.viewCellX + grid.cellsWide - 3) {
|
||||
grid.viewCellX = grid.selCellX - (grid.cellsWide - 3)
|
||||
}
|
||||
if grid.selCellX < grid.viewCellX {
|
||||
grid.viewCellX = grid.selCellX
|
||||
} else if grid.selCellX >= (grid.viewCellX + grid.cellsWide - 3) {
|
||||
grid.viewCellX = grid.selCellX - (grid.cellsWide - 3)
|
||||
}
|
||||
|
||||
if grid.selCellY < grid.viewCellY {
|
||||
grid.viewCellY = grid.selCellY
|
||||
} else if grid.selCellY >= (grid.viewCellY + grid.cellsHigh - 3) {
|
||||
grid.viewCellY = grid.selCellY - (grid.cellsHigh - 3)
|
||||
}
|
||||
if grid.selCellY < grid.viewCellY {
|
||||
grid.viewCellY = grid.selCellY
|
||||
} else if grid.selCellY >= (grid.viewCellY + grid.cellsHigh - 3) {
|
||||
grid.viewCellY = grid.selCellY - (grid.cellsHigh - 3)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Gets the cell value and attributes of a particular cell
|
||||
func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) {
|
||||
// The fixed cells
|
||||
modelCellX := cellX - 1 + grid.viewCellX
|
||||
modelCellY := cellY - 1 + grid.viewCellY
|
||||
modelMaxX, modelMaxY := grid.model.Dimensions()
|
||||
// The fixed cells
|
||||
modelCellX := cellX - 1 + grid.viewCellX
|
||||
modelCellY := cellY - 1 + grid.viewCellY
|
||||
modelMaxX, modelMaxY := grid.model.Dimensions()
|
||||
|
||||
if (cellX == 0) && (cellY == 0) {
|
||||
return "", AttrBold, AttrBold
|
||||
} else if (cellX == 0) {
|
||||
if (modelCellY == grid.selCellY) {
|
||||
return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrBold | AttrReverse
|
||||
} else {
|
||||
return strconv.Itoa(modelCellY), AttrBold, AttrBold
|
||||
}
|
||||
} else if (cellY == 0) {
|
||||
if (modelCellX == grid.selCellX) {
|
||||
return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrBold | AttrReverse
|
||||
} else {
|
||||
return strconv.Itoa(modelCellX), AttrBold, AttrBold
|
||||
}
|
||||
} else {
|
||||
// The data from the model
|
||||
if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) {
|
||||
if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) {
|
||||
return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse
|
||||
} else {
|
||||
return grid.model.CellValue(modelCellX, modelCellY), 0, 0
|
||||
}
|
||||
} else {
|
||||
return "~", ColorBlue | AttrBold, 0
|
||||
}
|
||||
}
|
||||
if (cellX == 0) && (cellY == 0) {
|
||||
return "", AttrBold, AttrBold
|
||||
} else if cellX == 0 {
|
||||
if modelCellY == grid.selCellY {
|
||||
return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrReverse
|
||||
} else {
|
||||
return strconv.Itoa(modelCellY), AttrBold, 0
|
||||
}
|
||||
} else if cellY == 0 {
|
||||
if modelCellX == grid.selCellX {
|
||||
return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrReverse
|
||||
} else {
|
||||
return strconv.Itoa(modelCellX), AttrBold, 0
|
||||
}
|
||||
} else {
|
||||
// The data from the model
|
||||
if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) {
|
||||
if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) {
|
||||
return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse
|
||||
} else {
|
||||
return grid.model.CellValue(modelCellX, modelCellY), 0, 0
|
||||
}
|
||||
} else {
|
||||
return "~", ColorBlue | AttrBold, 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the cell dimensions
|
||||
func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) {
|
||||
|
||||
var cellWidth, cellHeight int
|
||||
var cellWidth, cellHeight int
|
||||
|
||||
modelCellX := cellX - 1 + grid.viewCellX
|
||||
modelCellY := cellY - 1 + grid.viewCellY
|
||||
modelMaxX, modelMaxY := grid.model.Dimensions()
|
||||
modelCellX := cellX - 1 + grid.viewCellX
|
||||
modelCellY := cellY - 1 + grid.viewCellY
|
||||
modelMaxX, modelMaxY := grid.model.Dimensions()
|
||||
|
||||
// Get the cell width & height from model (if within range)
|
||||
if (modelCellX >= 0) && (modelCellX < modelMaxX) {
|
||||
cellWidth = grid.model.ColWidth(modelCellX)
|
||||
} else {
|
||||
cellWidth = 8
|
||||
}
|
||||
// Get the cell width & height from model (if within range)
|
||||
if (modelCellX >= 0) && (modelCellX < modelMaxX) {
|
||||
cellWidth = grid.model.ColWidth(modelCellX)
|
||||
} else {
|
||||
cellWidth = 8
|
||||
}
|
||||
|
||||
if (modelCellY >= 0) && (modelCellY < modelMaxY) {
|
||||
cellHeight = grid.model.RowHeight(modelCellY)
|
||||
} else {
|
||||
cellHeight = 2
|
||||
}
|
||||
if (modelCellY >= 0) && (modelCellY < modelMaxY) {
|
||||
cellHeight = grid.model.RowHeight(modelCellY)
|
||||
} else {
|
||||
cellHeight = 2
|
||||
}
|
||||
|
||||
if (cellX == 0) && (cellY == 0) {
|
||||
return 8, 1
|
||||
} else if (cellX == 0) {
|
||||
return 8, cellHeight
|
||||
} else if (cellY == 0) {
|
||||
return cellWidth, 1
|
||||
} else {
|
||||
return cellWidth, cellHeight
|
||||
}
|
||||
if (cellX == 0) && (cellY == 0) {
|
||||
return 8, 1
|
||||
} else if cellX == 0 {
|
||||
return 8, cellHeight
|
||||
} else if cellY == 0 {
|
||||
return cellWidth, 1
|
||||
} else {
|
||||
return cellWidth, cellHeight
|
||||
}
|
||||
|
||||
// XXX: Workaround for bug in compiler
|
||||
panic("Unreachable code")
|
||||
return 0, 0
|
||||
// XXX: Workaround for bug in compiler
|
||||
panic("Unreachable code")
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders a cell which contains text. The clip rectangle defines the size of the cell, as well as the left offset
|
||||
* of the cell. The sx and sy determine the screen position of the cell top-left.
|
||||
*/
|
||||
func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy int, text string, fg, bg Attribute) {
|
||||
for x := cellClipRect.x1; x <= cellClipRect.x2; x++ {
|
||||
for y := cellClipRect.y1; y <= cellClipRect.y2; y++ {
|
||||
currRune := ' '
|
||||
if (y == 0) {
|
||||
textPos := int(x)
|
||||
if textPos < len(text) {
|
||||
currRune = rune(text[textPos])
|
||||
}
|
||||
}
|
||||
for x := cellClipRect.x1; x <= cellClipRect.x2; x++ {
|
||||
for y := cellClipRect.y1; y <= cellClipRect.y2; y++ {
|
||||
currRune := ' '
|
||||
if y == 0 {
|
||||
textPos := int(x)
|
||||
if textPos < len(text) {
|
||||
currRune = rune(text[textPos])
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This might be better if this wasn't so low-level
|
||||
ctx.DrawRuneWithAttrs(int(x - cellClipRect.x1) + sx, int(y - cellClipRect.y1) + sy, currRune, fg, bg)
|
||||
}
|
||||
}
|
||||
// TODO: This might be better if this wasn't so low-level
|
||||
ctx.DrawRuneWithAttrs(int(x-cellClipRect.x1)+sx, int(y-cellClipRect.y1)+sy, currRune, fg, bg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Renders a column. The viewport determines the maximum position of the rendered cell. CellX and CellY are the
|
||||
// cell indicies to render, cellOffset are the LOCAL offset of the cell.
|
||||
// This function will return the new X position (gridRect.x1 + colWidth)
|
||||
func (grid *Grid) renderColumn(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (gridPoint, int) {
|
||||
|
||||
// The top-left position of the column
|
||||
screenX := int(screenViewPort.x1)
|
||||
screenY := int(screenViewPort.y1)
|
||||
screenWidth := int(screenViewPort.x2 - screenViewPort.x1)
|
||||
screenHeight := int(screenViewPort.y2 - screenViewPort.y1)
|
||||
// The top-left position of the column
|
||||
screenX := int(screenViewPort.x1)
|
||||
screenY := int(screenViewPort.y1)
|
||||
screenWidth := int(screenViewPort.x2 - screenViewPort.x1)
|
||||
screenHeight := int(screenViewPort.y2 - screenViewPort.y1)
|
||||
|
||||
// Work out the column width and cap it if it will spill over the edge of the viewport
|
||||
colWidth, _ := grid.getCellDimensions(cellX, cellY)
|
||||
colWidth -= cellOffsetX
|
||||
if colWidth > screenWidth {
|
||||
colWidth = screenHeight
|
||||
}
|
||||
// Work out the column width and cap it if it will spill over the edge of the viewport
|
||||
colWidth, _ := grid.getCellDimensions(cellX, cellY)
|
||||
colWidth -= cellOffsetX
|
||||
if colWidth > screenWidth {
|
||||
colWidth = screenHeight
|
||||
}
|
||||
|
||||
// The maximum
|
||||
maxScreenY := screenY + screenHeight
|
||||
cellsHigh := 0
|
||||
// The maximum
|
||||
maxScreenY := screenY + screenHeight
|
||||
cellsHigh := 0
|
||||
|
||||
for screenY < maxScreenY {
|
||||
for screenY < maxScreenY {
|
||||
|
||||
// Cap the row height if it will go beyond the edge of the viewport.
|
||||
_, rowHeight := grid.getCellDimensions(cellX, cellY)
|
||||
if screenY + rowHeight > maxScreenY {
|
||||
rowHeight = maxScreenY - screenY
|
||||
}
|
||||
// Cap the row height if it will go beyond the edge of the viewport.
|
||||
_, rowHeight := grid.getCellDimensions(cellX, cellY)
|
||||
if screenY+rowHeight > maxScreenY {
|
||||
rowHeight = maxScreenY - screenY
|
||||
}
|
||||
|
||||
cellText, cellFg, cellBg := grid.getCellData(cellX, cellY)
|
||||
cellText, cellFg, cellBg := grid.getCellData(cellX, cellY)
|
||||
|
||||
grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth - cellOffsetX, rowHeight),
|
||||
screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse
|
||||
grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth-cellOffsetX, rowHeight),
|
||||
screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse
|
||||
|
||||
cellY++
|
||||
cellsHigh++
|
||||
screenY = screenY + rowHeight - cellOffsetY
|
||||
cellOffsetY = 0
|
||||
}
|
||||
cellY++
|
||||
cellsHigh++
|
||||
screenY = screenY + rowHeight - cellOffsetY
|
||||
cellOffsetY = 0
|
||||
}
|
||||
|
||||
return gridPoint(screenX + colWidth), cellsHigh
|
||||
return gridPoint(screenX + colWidth), cellsHigh
|
||||
}
|
||||
|
||||
|
||||
// Renders the grid. Returns the number of cells in the X and Y direction were rendered.
|
||||
//
|
||||
func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (int, int) {
|
||||
|
||||
var cellsHigh = 0
|
||||
var cellsWide = 0
|
||||
var cellsHigh = 0
|
||||
var cellsWide = 0
|
||||
|
||||
for screenViewPort.x1 < screenViewPort.x2 {
|
||||
screenViewPort.x1, cellsHigh = grid.renderColumn(ctx, screenViewPort, cellX, cellY, cellOffsetX, cellOffsetY)
|
||||
cellX = cellX + 1
|
||||
cellsWide++
|
||||
cellOffsetX = 0
|
||||
}
|
||||
for screenViewPort.x1 < screenViewPort.x2 {
|
||||
screenViewPort.x1, cellsHigh = grid.renderColumn(ctx, screenViewPort, cellX, cellY, cellOffsetX, cellOffsetY)
|
||||
cellX = cellX + 1
|
||||
cellsWide++
|
||||
cellOffsetX = 0
|
||||
}
|
||||
|
||||
return cellsWide, cellsHigh
|
||||
return cellsWide, cellsHigh
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the cell of the particular point, along with the top-left position of the cell.
|
||||
*/
|
||||
func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, posY int) {
|
||||
var wid, hei int = grid.model.Dimensions()
|
||||
posX = 0
|
||||
posY = 0
|
||||
var wid, hei int = grid.model.Dimensions()
|
||||
posX = 0
|
||||
posY = 0
|
||||
|
||||
cellX = -1
|
||||
cellY = -1
|
||||
cellX = -1
|
||||
cellY = -1
|
||||
|
||||
// Go through columns to locate the particular cellX
|
||||
for cx := 0; cx < wid; cx++ {
|
||||
if (x >= posX) && (x < posX + grid.model.ColWidth(cx)) {
|
||||
// We found the X position
|
||||
cellX = int(cx)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Go through columns to locate the particular cellX
|
||||
for cx := 0; cx < wid; cx++ {
|
||||
if (x >= posX) && (x < posX+grid.model.ColWidth(cx)) {
|
||||
// We found the X position
|
||||
cellX = int(cx)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for cy := 0; cy < hei; cy++ {
|
||||
if (y >= posY) && (y < posY + grid.model.RowHeight(cy)) {
|
||||
// And the Y position
|
||||
cellY = int(cy)
|
||||
break
|
||||
}
|
||||
}
|
||||
for cy := 0; cy < hei; cy++ {
|
||||
if (y >= posY) && (y < posY+grid.model.RowHeight(cy)) {
|
||||
// And the Y position
|
||||
cellY = int(cy)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested dimensions of a grid (as required by UiComponent)
|
||||
*/
|
||||
func (grid *Grid) Remeasure(w, h int) (int, int) {
|
||||
return w, h
|
||||
return w, h
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws the grid.
|
||||
*/
|
||||
func (grid *Grid) Redraw(ctx *DrawContext) {
|
||||
viewportRect := newGridRect(0, 0, ctx.W, ctx.H)
|
||||
grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0)
|
||||
viewportRect := newGridRect(0, 0, ctx.W, ctx.H)
|
||||
grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
// Called when the component has focus and a key has been pressed.
|
||||
// This is the default behaviour of the grid, but it is not used by the main grid.
|
||||
func (grid *Grid) KeyPressed(key rune, mod int) {
|
||||
// TODO: Not sure if this would be better handled using commands
|
||||
if (key == 'i') || (key == KeyArrowUp) {
|
||||
grid.MoveBy(0, -1)
|
||||
} else if (key == 'k') || (key == KeyArrowDown) {
|
||||
grid.MoveBy(0, 1)
|
||||
} else if (key == 'j') || (key == KeyArrowLeft) {
|
||||
grid.MoveBy(-1, 0)
|
||||
} else if (key == 'l') || (key == KeyArrowRight) {
|
||||
grid.MoveBy(1, 0)
|
||||
}
|
||||
// TODO: Not sure if this would be better handled using commands
|
||||
if (key == 'i') || (key == KeyArrowUp) {
|
||||
grid.MoveBy(0, -1)
|
||||
} else if (key == 'k') || (key == KeyArrowDown) {
|
||||
grid.MoveBy(0, 1)
|
||||
} else if (key == 'j') || (key == KeyArrowLeft) {
|
||||
grid.MoveBy(-1, 0)
|
||||
} else if (key == 'l') || (key == KeyArrowRight) {
|
||||
grid.MoveBy(1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Test Model
|
||||
|
||||
type TestModel struct {
|
||||
thing int
|
||||
thing int
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the grid model (width x height)
|
||||
*/
|
||||
func (model *TestModel) Dimensions() (int, int) {
|
||||
return 100, 100
|
||||
return 100, 100
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
|
||||
*/
|
||||
func (model *TestModel) ColWidth(int) int {
|
||||
return 16
|
||||
return 16
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
|
||||
*/
|
||||
func (model *TestModel) RowHeight(int) int {
|
||||
return 1
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the cell a position X, Y
|
||||
*/
|
||||
func (model *TestModel) CellValue(x int, y int) string {
|
||||
return strconv.Itoa(x) + "," + strconv.Itoa(y)
|
||||
return strconv.Itoa(x) + "," + strconv.Itoa(y)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,84 +3,86 @@
|
|||
|
||||
package ui
|
||||
|
||||
|
||||
// The UI manager
|
||||
type Ui struct {
|
||||
// The root component
|
||||
rootComponent UiComponent
|
||||
focusedComponent FocusableComponent
|
||||
// The root component
|
||||
rootComponent UiComponent
|
||||
focusedComponent FocusableComponent
|
||||
|
||||
drawContext *DrawContext
|
||||
driver Driver
|
||||
drawContext *DrawContext
|
||||
driver Driver
|
||||
shutdown bool
|
||||
}
|
||||
|
||||
|
||||
// Creates a new UI context. This also initializes the UI state.
|
||||
// Returns the context and an error.
|
||||
func NewUI() (*Ui, error) {
|
||||
driver := &TermboxDriver{}
|
||||
err := driver.Init()
|
||||
driver := &TermboxDriver{}
|
||||
err := driver.Init()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
drawContext := &DrawContext{ driver: driver }
|
||||
ui := &Ui{ drawContext: drawContext, driver: driver }
|
||||
drawContext := &DrawContext{driver: driver}
|
||||
ui := &Ui{drawContext: drawContext, driver: driver}
|
||||
|
||||
return ui, nil
|
||||
return ui, nil
|
||||
}
|
||||
|
||||
|
||||
// Closes the UI context.
|
||||
func (ui *Ui) Close() {
|
||||
ui.driver.Close()
|
||||
ui.driver.Close()
|
||||
}
|
||||
|
||||
// Sets the root component
|
||||
func (ui *Ui) SetRootComponent(comp UiComponent) {
|
||||
ui.rootComponent = comp
|
||||
ui.Remeasure()
|
||||
ui.rootComponent = comp
|
||||
ui.Remeasure()
|
||||
}
|
||||
|
||||
// Sets the focused component
|
||||
func (ui *Ui) SetFocusedComponent(newFocused FocusableComponent) {
|
||||
ui.focusedComponent = newFocused
|
||||
ui.focusedComponent = newFocused
|
||||
}
|
||||
|
||||
// Remeasures the UI
|
||||
func (ui *Ui) Remeasure() {
|
||||
ui.drawContext.X = 0
|
||||
ui.drawContext.Y = 0
|
||||
ui.drawContext.W, ui.drawContext.H = ui.driver.Size()
|
||||
ui.drawContext.X = 0
|
||||
ui.drawContext.Y = 0
|
||||
ui.drawContext.W, ui.drawContext.H = ui.driver.Size()
|
||||
|
||||
ui.rootComponent.Remeasure(ui.drawContext.W, ui.drawContext.H)
|
||||
ui.rootComponent.Remeasure(ui.drawContext.W, ui.drawContext.H)
|
||||
}
|
||||
|
||||
// Redraws the UI.
|
||||
func (ui *Ui) Redraw() {
|
||||
ui.Remeasure()
|
||||
ui.Remeasure()
|
||||
|
||||
ui.rootComponent.Redraw(ui.drawContext)
|
||||
ui.driver.Sync()
|
||||
ui.rootComponent.Redraw(ui.drawContext)
|
||||
ui.driver.Sync()
|
||||
}
|
||||
|
||||
// Quit indicates to the UI that it should shutdown
|
||||
func (ui *Ui) Shutdown() {
|
||||
ui.shutdown = true
|
||||
}
|
||||
|
||||
// Enter the UI loop
|
||||
func (ui *Ui) Loop() {
|
||||
for {
|
||||
ui.Redraw()
|
||||
event := ui.driver.WaitForEvent()
|
||||
for !ui.shutdown {
|
||||
ui.Redraw()
|
||||
event := ui.driver.WaitForEvent()
|
||||
|
||||
// TODO: If the event is a key-press, do something.
|
||||
if event.Type == EventKeyPress {
|
||||
if ui.focusedComponent != nil {
|
||||
ui.focusedComponent.KeyPressed(event.Ch, event.Par)
|
||||
}
|
||||
} else if event.Type == EventResize {
|
||||
// TODO: If the event is a key-press, do something.
|
||||
if event.Type == EventKeyPress {
|
||||
if ui.focusedComponent != nil {
|
||||
ui.focusedComponent.KeyPressed(event.Ch, event.Par)
|
||||
}
|
||||
} else if event.Type == EventResize {
|
||||
|
||||
// HACK: Find another way to refresh the size of the screen to prevent a full redraw.
|
||||
ui.driver.Sync()
|
||||
}
|
||||
}
|
||||
// HACK: Find another way to refresh the size of the screen to prevent a full redraw.
|
||||
ui.driver.Sync()
|
||||
}
|
||||
}
|
||||
}
|
||||
203
ui/stdcomps.go
203
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))
|
||||
te.cursorOffset = intMinMax(toX, 0, len(te.value))
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue