Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions button.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ type Button struct {
ClickHandler ButtonClickEvent
}

func (b *Button) GetBounds() *Rect {
return &b.Bounds
func (b *Button) GetBounds() Rect {
return b.Bounds
}

func (b *Button) Draw(target *DrawTarget) {
func (b *Button) Draw(target DrawTarget) {
target.Print(2, 1, termbox.ColorWhite, termbox.ColorBlack, b.Text)

if b.focus {
Expand Down
6 changes: 3 additions & 3 deletions checkbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ type CheckBox struct {
focus bool
}

func (c *CheckBox) GetBounds() *Rect {
return &c.Bounds
func (c *CheckBox) GetBounds() Rect {
return c.Bounds
}

func (c *CheckBox) Draw(target *DrawTarget) {
func (c *CheckBox) Draw(target DrawTarget) {
checkContent := " "

if c.Checked {
Expand Down
4 changes: 2 additions & 2 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ func (c *Container) FocusPrevious() {
}
}

func (c *Container) Draw(target *DrawTarget) {
func (c *Container) Draw(target DrawTarget) {
for _, child := range c.Controls {
childBounds := child.GetBounds()
childContext, err := target.Slice(childBounds)
childContext, err := Scope(target, childBounds)

if err != nil {
panic(err)
Expand Down
6 changes: 3 additions & 3 deletions detailview.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ type DetailView struct {
SelectedBg termbox.Attribute
}

func (d *DetailView) GetBounds() *Rect {
return &d.Bounds
func (d *DetailView) GetBounds() Rect {
return d.Bounds
}

func (d *DetailView) Reset() {
Expand Down Expand Up @@ -137,7 +137,7 @@ func (d *DetailView) totalWidth() int {
return ret
}

func (d *DetailView) Draw(target *DrawTarget) {
func (d *DetailView) Draw(target DrawTarget) {
top := 0
left := 0

Expand Down
129 changes: 9 additions & 120 deletions draw_target.go
Original file line number Diff line number Diff line change
@@ -1,129 +1,18 @@
package tui

import (
"errors"
"fmt"
"github.com/nsf/termbox-go"
"golang.org/x/text/unicode/norm"
"unicode/utf8"
)

// A DrawTarget represents a drawable portion of the terminal window. Drawing
// with methods like SetCell and Print will automatically translate from local
// coordinates to screen coordinates and clip drawing to the drawable region.
// It can be further subdivided by calling ChildDrawTarget().
type DrawTarget struct {
Width int
Height int

offsetLeft int
offsetTop int
}

// The location and size of the drawable area in local coordinates.
func (target *DrawTarget) Bounds() *Rect {
return &Rect{
Left: 0,
Top: 0,
Width: target.Width,
Height: target.Height,
}
}

// Set one terminal cell. If (x, y) is out of bounds, an error will be returned
// and the terminal will be unchanged.
func (target *DrawTarget) SetCell(x, y int,
foreground, background termbox.Attribute, char rune) error {
if !target.Bounds().ContainsPoint(x, y) {
return errors.New(
"Coordinates are out of bounds for the DrawTarget")
}

globalX, globalY := target.localToScreenCoords(x, y)

termbox.SetCell(globalX, globalY, char, foreground, background)

return nil
}

// Write formatted text to the terminal using the "fmt" package formatting
// style. The text will be automatically clipped to the DrawTarget's drawable
// region.
func (target *DrawTarget) Print(x, y int,
foreground, background termbox.Attribute, text string,
args ...interface{}) {
formatted := fmt.Sprintf(text, args...)
normalized := normalizeString(formatted)
for i, r := range normalized {
target.SetCell(x+i, y, foreground, background, r)
}
}

// Create a DrawTarget which allows drawing to a portion of the parent's
// DrawTarget area. childBounds should be specified in the parent's local
// coordinates.
//
// Note: this is mostly needed if you're writing a control that contains other
// controls.
func (parent *DrawTarget) Slice(childBounds *Rect) (*DrawTarget, error) {
if !parent.Bounds().ContainsRect(childBounds) {
return nil, errors.New("Provided child bounds would exceed " +
"the parent's dimensions.")
}

return &DrawTarget{
offsetLeft: parent.offsetLeft + childBounds.Left,
offsetTop: parent.offsetTop + childBounds.Top,
Width: childBounds.Width,
Height: childBounds.Height,
}, nil
}

func (target *DrawTarget) localToScreenCoords(x, y int) (int, int) {
return target.offsetLeft + x, target.offsetTop + y
}

// The termbox API allows a single rune per terminal cell (x, y). Strings can
// contain grapheme clusters which are made up of multiple runes but occupy
// only the width of a single character when displayed. I don't know a way to
// map grapheme clusters to termbox's API without data loss or display issues.
//
// This function detects multi-rune grapheme clusters and replaces them with
// Unicode replacement characters in order to be explicit that the decode was
// not entirely successful.
//
// TODO: Is there a way to support grapheme clusters on the terminal? Can it
// be done with termbox or would it require switching libraries?
func normalizeString(input string) []rune {
output := make([]rune, 0)

var ia norm.Iter
ia.InitString(norm.NFKD, input)

for !ia.Done() {
glyph := ia.Next()
firstRune, decodedSize := utf8.DecodeRune(glyph)
isGraphemeCluster := decodedSize < len(glyph)

if isGraphemeCluster {
firstRune = utf8.RuneError
}

output = append(output, firstRune)

}

return output
type DrawTarget interface {
Width() int
Height() int
SetCell(x, y int, foreground, background termbox.Attribute,
char rune) error
Print(x, y int, foreground, background termbox.Attribute, text string,
args ...interface{})
}

// Create a DrawTarget that allows drawing to the entire terminal window.
func fullTerminalDrawTarget() *DrawTarget {
terminalWidth, terminalHeight := termbox.Size()

return &DrawTarget{
Width: terminalWidth,
Height: terminalHeight,
offsetLeft: 0,
offsetTop: 0,
}
func Bounds(target DrawTarget) Rect {
return Rect{0, 0, target.Width(), target.Height()}
}
6 changes: 3 additions & 3 deletions editbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ type EditBox struct {
clipBoard [][]Char
}

func (e *EditBox) GetBounds() *Rect {
return &e.Bounds
func (e *EditBox) GetBounds() Rect {
return e.Bounds
}

var whitespace []rune = []rune{' ', '\t'}
Expand Down Expand Up @@ -209,7 +209,7 @@ func (e *EditBox) SetText(raw string) {
e.fireTextChanged()
}

func (e *EditBox) Draw(target *DrawTarget) {
func (e *EditBox) Draw(target DrawTarget) {
textWidth := e.Bounds.Width
textHeight := e.Bounds.Height - 1 // Bottom line free for modes/notices

Expand Down
6 changes: 3 additions & 3 deletions label.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ type Label struct {
Text string
}

func (l *Label) GetBounds() *Rect {
return &l.Bounds
func (l *Label) GetBounds() Rect {
return l.Bounds
}

func (l *Label) Draw(target *DrawTarget) {
func (l *Label) Draw(target DrawTarget) {
target.Print(0, 0, termbox.ColorWhite, termbox.ColorBlack, l.Text)
}
8 changes: 4 additions & 4 deletions rect.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ type Rect struct {
Left, Top, Width, Height int
}

func (r *Rect) Right() int {
func (r Rect) Right() int {
return r.Left + r.Width - 1
}

func (r *Rect) Bottom() int {
func (r Rect) Bottom() int {
return r.Top + r.Height - 1
}

func (r *Rect) ContainsRect(other *Rect) bool {
func (r Rect) ContainsRect(other Rect) bool {
return other.Left >= r.Left && other.Right() <= r.Right() &&
other.Top >= r.Top && other.Bottom() <= r.Bottom()
}

func (r *Rect) ContainsPoint(x, y int) bool {
func (r Rect) ContainsPoint(x, y int) bool {
return x >= 0 && x < r.Width &&
y >= 0 && y < r.Height
}
78 changes: 78 additions & 0 deletions scoped_draw_target.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package tui

import (
"errors"
"github.com/nsf/termbox-go"
)

// A DrawTarget represents a drawable portion of the terminal window. Drawing
// with methods like SetCell and Print will automatically translate from local
// coordinates to screen coordinates and clip drawing to the drawable region.
// It can be further subdivided by calling ChildDrawTarget().
type ScopedDrawTarget struct {
width int
height int

offsetLeft int
offsetTop int

parent DrawTarget
}

func (target ScopedDrawTarget) Width() int {
return target.width
}

func (target ScopedDrawTarget) Height() int {
return target.height
}

// Set one terminal cell. If (x, y) is out of bounds, an error will be returned
// and the terminal will be unchanged.
func (target ScopedDrawTarget) SetCell(x, y int,
foreground, background termbox.Attribute, char rune) error {
if !Bounds(target).ContainsPoint(x, y) {
return errors.New(
"Coordinates are out of bounds for the ScopedDrawTarget")
}

parentX, parentY := target.localToParentCoords(x, y)
target.parent.SetCell(parentX, parentY, foreground, background, char)
return nil
}

// Write formatted text to the terminal using the "fmt" package formatting
// style. The text will be automatically clipped to the ScopedDrawTarget's drawable
// region.
func (target ScopedDrawTarget) Print(x, y int,
foreground, background termbox.Attribute, text string,
args ...interface{}) {
parentX, parentY := target.localToParentCoords(x, y)
target.parent.Print(parentX, parentY, foreground, background, text,
args...)
}

// Create a ScopedDrawTarget which allows drawing to a portion of the parent's
// ScopedDrawTarget area. childBounds should be specified in the parent's local
// coordinates.
//
// Note: this is mostly needed if you're writing a control that contains other
// controls.
func Scope(parent DrawTarget, childBounds Rect) (*ScopedDrawTarget, error) {
if !Bounds(parent).ContainsRect(childBounds) {
return nil, errors.New("Provided child bounds would exceed " +
"the parent's dimensions.")
}

return &ScopedDrawTarget{
parent: parent,
offsetLeft: childBounds.Left,
offsetTop: childBounds.Top,
width: childBounds.Width,
height: childBounds.Height,
}, nil
}

func (target ScopedDrawTarget) localToParentCoords(x, y int) (int, int) {
return target.offsetLeft + x, target.offsetTop + y
}
Loading