diff --git a/main.go b/main.go index 2820a70..26a1ae1 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,28 @@ package main import ( - "fmt" + "./ui" ) func main() { + uiManager, err := ui.NewUI() + if err != nil { + panic(err) + } + defer uiManager.Close() + + + statusLayout := &ui.VertLinearLayout{} + statusLayout.Append(&ui.StatusBar{"Test", "Component"}) + statusLayout.Append(&ui.StatusBar{"Another", "Component"}) + statusLayout.Append(&ui.StatusBar{"Third", "Test"}) + + clientArea := &ui.RelativeLayout{ South: statusLayout } + + uiManager.SetRootComponent(clientArea) + + uiManager.Loop() + /* uiCtx, _ := NewUI() uiCtx.Redraw() @@ -12,4 +30,5 @@ func main() { uiCtx.Close() fmt.Printf("OK!") + */ } diff --git a/ui.go b/ui.go deleted file mode 100644 index 4c9d371..0000000 --- a/ui.go +++ /dev/null @@ -1,174 +0,0 @@ -/** - * UI package. - */ -package main - -import "github.com/nsf/termbox-go" - - -// ========================================================================== -// UI event. - -/** - * The types of events. - */ -type EventType int -const ( - EventKeyPress EventType = iota -) - -/** - * An event callback - */ -type UiEvent struct { - eventType EventType - eventPar int -} - -// ========================================================================== -// UI component. - -type UiComponent interface { - /** - * Request to redraw this component. - */ - Redraw(x int, y int, w int, h int) - - /** - * Request the minimum dimensions of the component (width, height). If - * either dimension is -1, no minimum is imposed. - */ - RequestDims() (int, int) -} - - -// ========================================================================== -// UI context. - -/** - * Ui context type. - */ -type Ui struct { - grid *Grid - statusBar *UiStatusBar -} - - -/** - * Creates a new UI context. This also initializes the UI state. - * Returns the context and an error. - */ -func NewUI() (*Ui, error) { - termboxError := termbox.Init() - - if termboxError != nil { - return nil, termboxError - } else { - uiCtx := new(Ui) // &Ui{&UiStatusBar{"Hello", "World"}} - uiCtx.grid = NewGrid(&TestModel{}) - uiCtx.statusBar = &UiStatusBar{"Hello", "World"} - return uiCtx, nil - } - - // XXX: Workaround for bug in compiler - panic("Unreachable code") - return nil, nil -} - - -/** - * Closes the UI context. - */ -func (ui *Ui) Close() { - termbox.Close() -} - - -/** - * Redraws the UI. - */ -func (ui *Ui) Redraw() { - var width, height int = termbox.Size() - ui.redrawInternal(width, height) -} - -/** - * Internal redraw function which does not query the terminal size. - */ -func (ui *Ui) redrawInternal(width, height int) { - termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) - - // TODO: This will eventually offload to UI "components" - ui.grid.Redraw(0, 0, width, height - 2) - - // Draws the status bar - ui.statusBar.Redraw(0, height - 2, width, 2) - - termbox.Flush() -} - - -/** - * Waits for a UI event. Returns the event (if it's relevant to the user). - */ -func (ui *Ui) NextEvent() UiEvent { - for { - event := termbox.PollEvent() - if event.Type == termbox.EventResize { - ui.redrawInternal(event.Width, event.Height) - } else { - - // !!TEMP!! - if (event.Ch == 'i') { - ui.grid.MoveBy(0, -1) - } else if (event.Ch == 'k') { - ui.grid.MoveBy(0, 1) - } else if (event.Ch == 'j') { - ui.grid.MoveBy(-1, 0) - } else if (event.Ch == 'l') { - ui.grid.MoveBy(1, 0) - } else { - return UiEvent{EventKeyPress, 0} - } - // !!END TEMP!! - - ui.Redraw() - //return UiEvent{EventKeyPress, 0} - } - } - - // XXX: Workaround for bug in compiler - panic("Unreachable code") - return UiEvent{EventKeyPress, 0} -} - - -// ========================================================================== -// Status bar component - -type UiStatusBar struct { - left string // Left aligned string - right string // Right aligned string -} - -// Minimum dimensions -func (sbar *UiStatusBar) RequestDims() (int, int) { - return -1, 2 -} - -// Status bar redraw -func (sbar *UiStatusBar) Redraw(x int, y int, w int, h int) { - leftLen := len(sbar.left) - rightLen := len(sbar.right) - rightPos := w - rightLen - - for x1 := 0; x1 < w; x1++ { - var runeToPrint rune = ' ' - if x1 < leftLen { - runeToPrint = rune(sbar.left[x1]) - } else if x1 >= rightPos { - runeToPrint = rune(sbar.right[x1 - rightPos]) - } - termbox.SetCell(x1, y, runeToPrint, termbox.AttrReverse, termbox.AttrReverse) - } -} diff --git a/ui/component.go b/ui/component.go new file mode 100644 index 0000000..a6a66d9 --- /dev/null +++ b/ui/component.go @@ -0,0 +1,60 @@ +// Components of the UI and various event interfaces. + +package ui + + +// The set of event types supported by the UI package. + + + + +// An interface of a UI component. +type UiComponent interface { + + // Request from the manager for the component to draw itself. This is given a drawable context. + Redraw(context *DrawContext) + + // Called to remeasure the size of the component. Provided with the maximum dimensions of the component + // and expected to provide the minimum component size. When called to redraw, the component will be + // provided with AT LEAST the minimum dimensions returned by this method. + Remeasure(w, h int) (int, int) +} + + +// ========================================================================== +// UI context. + + + + +// ========================================================================== +// Status bar component + +/* +type UiStatusBar struct { + left string // Left aligned string + right string // Right aligned string +} + +// Minimum dimensions +func (sbar *UiStatusBar) RequestDims() (int, int) { + return -1, 2 +} + +// Status bar redraw +func (sbar *UiStatusBar) Redraw(x int, y int, w int, h int) { + leftLen := len(sbar.left) + rightLen := len(sbar.right) + rightPos := w - rightLen + + for x1 := 0; x1 < w; x1++ { + var runeToPrint rune = ' ' + if x1 < leftLen { + runeToPrint = rune(sbar.left[x1]) + } else if x1 >= rightPos { + runeToPrint = rune(sbar.right[x1 - rightPos]) + } + termbox.SetCell(x1, y, runeToPrint, termbox.AttrReverse, termbox.AttrReverse) + } +} +*/ \ No newline at end of file diff --git a/ui/drawable.go b/ui/drawable.go new file mode 100644 index 0000000..d7e2733 --- /dev/null +++ b/ui/drawable.go @@ -0,0 +1,78 @@ +// Provides access to the drawable primitives. + +package ui + +// A drawable context. Each context is allocated part of the screen and provides methods for +// drawing within it's context. +type DrawContext struct { + + // The left and top position of the context. + X, Y int + + // The width and height position of the context. + W, H int + + // The current driver + driver Driver + + // 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), + + driver: dc.driver, + } +} + +// Sets the foreground attribute +func (dc *DrawContext) SetFgAttr(attr Attribute) { + dc.fa = attr +} + +// Sets the background attribute +func (dc *DrawContext) SetBgAttr(attr Attribute) { + 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) + } +} + +// 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++ + } +} + +// 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) +} + +// 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) + } +} + +// Converts a local point to a real point. If the point is within the context, also returns true +func (dc *DrawContext) localPointToRealPoint(x, y int) (int, int, bool) { + rx, ry := x + dc.X, y + dc.Y + return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X + dc.W) && (ry < dc.Y + dc.H) +} \ No newline at end of file diff --git a/ui/driver.go b/ui/driver.go new file mode 100644 index 0000000..1adc4b0 --- /dev/null +++ b/ui/driver.go @@ -0,0 +1,65 @@ +// The UI driver. This is used to interact with the terminal drawing routines. + +package ui + +// The set of attributes a specific cell can have +type Attribute uint16 + +const ( + // 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 +) + +// The type of events supported by the driver +type EventType int + +const ( + // Event indicating a key press. The event parameter is the key scancode? + EventKeyPress EventType = iota +) + +// Data from an event callback. +type Event struct { + Type EventType + Par int +} + + +// The terminal driver interface. +type Driver interface { + + // Initializes the driver. Returns an error if there was an error + Init() error + + // Closes the driver + Close() + + // 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) + + // Synchronizes the internal buffer with the real buffer + Sync() + + // Wait for an event + WaitForEvent() Event +} + +// \ No newline at end of file diff --git a/ui/layout.go b/ui/layout.go new file mode 100644 index 0000000..8f31ef6 --- /dev/null +++ b/ui/layout.go @@ -0,0 +1,114 @@ +// Standard layout components. + +package ui + +// Instance of a component. +type componentInstance struct { + component UiComponent + x, y int // X and Y offset of this component + height int // Height allocated to the component + width int // Width allocated to the component +} + +// A layout component. +type LinearLayout struct { + // The set of components that this layout component contains + subcomponents []*componentInstance +} + +// Adds a component to the end of the component list managed by this layout. +func (l *LinearLayout) Append(component UiComponent) { + l.subcomponents = append(l.subcomponents, &componentInstance{component, 0, 0, 0, 0}) +} + + +// A vertical layout component. +type VertLinearLayout struct { + LinearLayout + maxHeight int +} + + +// Request from the manager for the component to draw itself. This is given a drawable context. +func (vl *VertLinearLayout) Redraw(context *DrawContext) { + for _, ci := range vl.LinearLayout.subcomponents { + subContext := context.NewSubContext(ci.x, ci.y, ci.width, ci.height) + ci.component.Redraw(subContext) + } +} + +// Remeasures the components currently managed within this layout. +func (vl *VertLinearLayout) Remeasure(w, h int) (int, int) { + posy := 0 + + // TODO: At the moment, this simply takes the minimum and maximum dimensions requested + // by the components. This needs to be extended to allow for "special" components which + // take up dynamic space. + for _, ci := range vl.LinearLayout.subcomponents { + _, rh := ci.component.Remeasure(w, h - posy) + + // All components within this layer are given the full width of the screen. + ci.x = 0 + ci.width = w + ci.y = posy + ci.height = rh + + // Put the next component directly below the previous one + posy += rh + } + + return w, posy +} + + +// A relative layout component. This has a "client" component, bordered by a north, +// south, east and west component. The N,S,E,W components will be provided with the full dimensions +// whereas the client component will be provided with the remaining size. Each one of the components +// can be null. +type RelativeLayout struct { + North, South, East, West, Client UiComponent + + // Measured client borders + ct, cb, cl, cr, ch, cw int + + // North/south heights and east/west widths + nh, sh int + ew, ww int +} + +func (rl *RelativeLayout) Remeasure(w, h int) (int, int) { + if rl.North != nil { + _, rl.nh = rl.North.Remeasure(w, h) + rl.ct = rl.nh + } else { + rl.ct = 0 + } + + if rl.South != nil { + _, rl.sh = rl.South.Remeasure(w, h - rl.nh) + rl.cb = h - rl.sh + } else { + rl.cb = h + } + + // TODO: East and west + rl.cl = 0 + rl.cr = w + + rl.ch = h - rl.nh - rl.sh + rl.cw = w + + return w, h +} + +func (vl *RelativeLayout) Redraw(context *DrawContext) { + if vl.North != nil { + vl.North.Redraw(context.NewSubContext(0, 0, context.W, vl.nh)) + } + if vl.South != nil { + vl.South.Redraw(context.NewSubContext(0, vl.cb, context.W, vl.sh)) + } + if vl.Client != nil { + vl.Client.Redraw(context.NewSubContext(vl.cl, vl.ct, vl.cw, vl.ch)) + } +} \ No newline at end of file diff --git a/ui/manager.go b/ui/manager.go new file mode 100644 index 0000000..6a37753 --- /dev/null +++ b/ui/manager.go @@ -0,0 +1,131 @@ +// The UI manager. This manages the components that make up the UI and dispatches +// events. + +package ui + + +// The UI manager +type Ui struct { + //grid *Grid + //statusBar *UiStatusBar + + // The root component + rootComponent UiComponent + + drawContext *DrawContext + driver Driver +} + + +// 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() + + if err != nil { + return nil, err + } + + drawContext := &DrawContext{ driver: driver } + ui := &Ui{ drawContext: drawContext, driver: driver } + + /* + termboxError := termbox.Init() + + if termboxError != nil { + return nil, termboxError + } else { + uiCtx := new(Ui) // &Ui{&UiStatusBar{"Hello", "World"}} + uiCtx.grid = NewGrid(&TestModel{}) + uiCtx.statusBar = &UiStatusBar{"Hello", "World"} + return uiCtx, nil + } + + // XXX: Workaround for bug in compiler + panic("Unreachable code") + return nil, nil + */ + return ui, nil +} + + +// Closes the UI context. +func (ui *Ui) Close() { + ui.driver.Close() +} + +// Sets the root component +func (ui *Ui) SetRootComponent(comp UiComponent) { + ui.rootComponent = comp + ui.Remeasure() +} + +// 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.rootComponent.Remeasure(ui.drawContext.W, ui.drawContext.H) +} + +// Redraws the UI. +func (ui *Ui) Redraw() { + ui.Remeasure() + + ui.rootComponent.Redraw(ui.drawContext) + ui.driver.Sync() +} + +/** + * Internal redraw function which does not query the terminal size. + */ + /* +func (ui *Ui) redrawInternal(width, height int) { + termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) + + // TODO: This will eventually offload to UI "components" + ui.grid.Redraw(0, 0, width, height - 2) + + // Draws the status bar + ui.statusBar.Redraw(0, height - 2, width, 2) + + termbox.Flush() +} +*/ + + +// Enter the UI loop +func (ui *Ui) Loop() { + //for { + ui.Redraw() + ui.driver.WaitForEvent() + + /* + if event.Type == termbox.EventResize { + ui.redrawInternal(event.Width, event.Height) + } else { + + // !!TEMP!! + if (event.Ch == 'i') { + ui.grid.MoveBy(0, -1) + } else if (event.Ch == 'k') { + ui.grid.MoveBy(0, 1) + } else if (event.Ch == 'j') { + ui.grid.MoveBy(-1, 0) + } else if (event.Ch == 'l') { + ui.grid.MoveBy(1, 0) + } else { + return UiEvent{EventKeyPress, 0} + } + // !!END TEMP!! + + ui.Redraw() + //return UiEvent{EventKeyPress, 0} + } + */ + //} + + // XXX: Workaround for bug in compiler +} \ No newline at end of file diff --git a/ui/stdcomps.go b/ui/stdcomps.go new file mode 100644 index 0000000..0870962 --- /dev/null +++ b/ui/stdcomps.go @@ -0,0 +1,41 @@ +// Standard components + +package ui + + +// 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 +} + +// Minimum dimensions +func (sbar *StatusBar) Remeasure(w, h int) (int, int) { + return w, 1 +} + +// Status bar redraw +func (sbar *StatusBar) Redraw(context *DrawContext) { + context.SetFgAttr(AttrReverse) + context.SetBgAttr(AttrReverse) + + context.HorizRule(0, ' ') + context.Print(0, 0, sbar.Left) + context.PrintRight(context.W, 0, sbar.Right) + /* + leftLen := len(sbar.left) + rightLen := len(sbar.right) + rightPos := w - rightLen + + for x1 := 0; x1 < w; x1++ { + var runeToPrint rune = ' ' + if x1 < leftLen { + runeToPrint = rune(sbar.left[x1]) + } else if x1 >= rightPos { + runeToPrint = rune(sbar.right[x1 - rightPos]) + } + termbox.SetCell(x1, y, runeToPrint, termbox.AttrReverse, termbox.AttrReverse) + } + */ +} \ No newline at end of file diff --git a/ui/termboxdriver.go b/ui/termboxdriver.go new file mode 100644 index 0000000..4d500a6 --- /dev/null +++ b/ui/termboxdriver.go @@ -0,0 +1,43 @@ +// The terminal-box driver + +package ui + +import ( + "github.com/nsf/termbox-go" +) + + +type TermboxDriver struct { +} + + +// Initializes the driver. Returns an error if there was an error +func (td *TermboxDriver) Init() error { + return termbox.Init() +} + +// Closes the driver +func (td *TermboxDriver) Close() { + termbox.Close() +} + +// Returns the size of the window. +func (td *TermboxDriver) Size() (int, int) { + 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)) +} + +// Synchronizes the internal buffer with the real buffer +func (td *TermboxDriver) Sync() { + termbox.Sync() +} + +// Wait for an event +func (td *TermboxDriver) WaitForEvent() Event { + termbox.PollEvent() + return Event{} +} \ No newline at end of file diff --git a/ui/utils.go b/ui/utils.go new file mode 100644 index 0000000..12ff8cc --- /dev/null +++ b/ui/utils.go @@ -0,0 +1,13 @@ +// Various utilities + +package ui + + +// Returns the maximum value of either x or y. +func intMax(x, y int) int { + if (x < y) { + return y + } else { + return x + } +} \ No newline at end of file