From e3651644bc8cab75ffc722ba6f00461925825a34 Mon Sep 17 00:00:00 2001 From: Brian Steffens Date: Tue, 21 Aug 2018 00:52:00 -0700 Subject: [PATCH 1/3] checkpoint --- button.go | 2 +- checkbox.go | 2 +- container.go | 2 +- detailview.go | 2 +- draw_target.go | 29 ++++++++++++++++++++--------- editbox.go | 2 +- label.go | 2 +- shared.go | 2 +- textbox.go | 2 +- 9 files changed, 28 insertions(+), 17 deletions(-) diff --git a/button.go b/button.go index 7e3c8ba..22f4311 100644 --- a/button.go +++ b/button.go @@ -18,7 +18,7 @@ func (b *Button) GetBounds() *Rect { return &b.Bounds } -func (b *Button) Draw(target *DrawTarget) { +func (b *Button) Draw(target IDrawTarget) { target.Print(2, 1, termbox.ColorWhite, termbox.ColorBlack, b.Text) if b.focus { diff --git a/checkbox.go b/checkbox.go index 6f47f8b..88ea13a 100644 --- a/checkbox.go +++ b/checkbox.go @@ -16,7 +16,7 @@ func (c *CheckBox) GetBounds() *Rect { return &c.Bounds } -func (c *CheckBox) Draw(target *DrawTarget) { +func (c *CheckBox) Draw(target IDrawTarget) { checkContent := " " if c.Checked { diff --git a/container.go b/container.go index 533c0d0..de658f8 100644 --- a/container.go +++ b/container.go @@ -95,7 +95,7 @@ func (c *Container) FocusPrevious() { 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) diff --git a/detailview.go b/detailview.go index ee800a7..1f001a8 100644 --- a/detailview.go +++ b/detailview.go @@ -137,7 +137,7 @@ func (d *DetailView) totalWidth() int { return ret } -func (d *DetailView) Draw(target *DrawTarget) { +func (d *DetailView) Draw(target IDrawTarget) { top := 0 left := 0 diff --git a/draw_target.go b/draw_target.go index 2fb4590..f6059f4 100644 --- a/draw_target.go +++ b/draw_target.go @@ -8,6 +8,14 @@ import ( "unicode/utf8" ) +type IDrawTarget interface { + Bounds() *Rect + SetCell(x, y int, foreground, background termbox.Attribute, char rune) + error + Print(x, y int, foreground, background termbox.Attribute, text string, + args ...interface{}) +} + // 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. @@ -21,7 +29,7 @@ type DrawTarget struct { } // The location and size of the drawable area in local coordinates. -func (target *DrawTarget) Bounds() *Rect { +func (target DrawTarget) Bounds() *Rect { return &Rect{ Left: 0, Top: 0, @@ -32,7 +40,7 @@ func (target *DrawTarget) Bounds() *Rect { // 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, +func (target DrawTarget) SetCell(x, y int, foreground, background termbox.Attribute, char rune) error { if !target.Bounds().ContainsPoint(x, y) { return errors.New( @@ -49,7 +57,7 @@ func (target *DrawTarget) SetCell(x, y int, // 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, +func (target DrawTarget) Print(x, y int, foreground, background termbox.Attribute, text string, args ...interface{}) { formatted := fmt.Sprintf(text, args...) @@ -65,21 +73,24 @@ func (target *DrawTarget) Print(x, y int, // // Note: this is mostly needed if you're writing a control that contains other // controls. -func (parent *DrawTarget) Slice(childBounds *Rect) (*DrawTarget, error) { +func Scope(parent *DrawTarget, childBounds *Rect) IDrawTarget { if !parent.Bounds().ContainsRect(childBounds) { - return nil, errors.New("Provided child bounds would exceed " + - "the parent's dimensions.") + //return nil, errors.New("Provided child bounds would exceed " + + //"the parent's dimensions.") + panic("asdf") } - return &DrawTarget{ + target := DrawTarget{ offsetLeft: parent.offsetLeft + childBounds.Left, offsetTop: parent.offsetTop + childBounds.Top, Width: childBounds.Width, Height: childBounds.Height, - }, nil + } + + return target } -func (target *DrawTarget) localToScreenCoords(x, y int) (int, int) { +func (target DrawTarget) localToScreenCoords(x, y int) (int, int) { return target.offsetLeft + x, target.offsetTop + y } diff --git a/editbox.go b/editbox.go index 5466e82..459480e 100644 --- a/editbox.go +++ b/editbox.go @@ -209,7 +209,7 @@ func (e *EditBox) SetText(raw string) { e.fireTextChanged() } -func (e *EditBox) Draw(target *DrawTarget) { +func (e *EditBox) Draw(target IDrawTarget) { textWidth := e.Bounds.Width textHeight := e.Bounds.Height - 1 // Bottom line free for modes/notices diff --git a/label.go b/label.go index 8487694..f1da7e3 100644 --- a/label.go +++ b/label.go @@ -13,6 +13,6 @@ func (l *Label) GetBounds() *Rect { return &l.Bounds } -func (l *Label) Draw(target *DrawTarget) { +func (l *Label) Draw(target IDrawTarget) { target.Print(0, 0, termbox.ColorWhite, termbox.ColorBlack, l.Text) } diff --git a/shared.go b/shared.go index ab9fa7e..7be0db8 100644 --- a/shared.go +++ b/shared.go @@ -59,7 +59,7 @@ func matchBinding(ev escapebox.Event, kb KeyBinding) bool { type Control interface { GetBounds() *Rect - Draw(*DrawTarget) + Draw(IDrawTarget) } type Focusable interface { diff --git a/textbox.go b/textbox.go index a2143e4..ef0e4c9 100644 --- a/textbox.go +++ b/textbox.go @@ -30,7 +30,7 @@ func (t *TextBox) lastVisible() int { return t.scroll + t.visibleChars() - 1 } -func (t *TextBox) Draw(target *DrawTarget) { +func (t *TextBox) Draw(target IDrawTarget) { target.Print(1, 1, termbox.ColorWhite, termbox.ColorBlack, t.Value[t.scroll:t.lastVisible()+1]) From b710aea5bb1f694568588bbfbd0268695c4d4563 Mon Sep 17 00:00:00 2001 From: Brian Steffens Date: Tue, 21 Aug 2018 01:27:03 -0700 Subject: [PATCH 2/3] Add DrawTarget interface --- button.go | 2 +- checkbox.go | 2 +- container.go | 2 +- detailview.go | 2 +- draw_target.go | 154 +++++++++++++++++++++++++++++++++---------------- editbox.go | 2 +- label.go | 2 +- shared.go | 4 +- textbox.go | 2 +- 9 files changed, 113 insertions(+), 59 deletions(-) diff --git a/button.go b/button.go index 22f4311..f6bfa52 100644 --- a/button.go +++ b/button.go @@ -18,7 +18,7 @@ func (b *Button) GetBounds() *Rect { return &b.Bounds } -func (b *Button) Draw(target IDrawTarget) { +func (b *Button) Draw(target DrawTarget) { target.Print(2, 1, termbox.ColorWhite, termbox.ColorBlack, b.Text) if b.focus { diff --git a/checkbox.go b/checkbox.go index 88ea13a..4fbd110 100644 --- a/checkbox.go +++ b/checkbox.go @@ -16,7 +16,7 @@ func (c *CheckBox) GetBounds() *Rect { return &c.Bounds } -func (c *CheckBox) Draw(target IDrawTarget) { +func (c *CheckBox) Draw(target DrawTarget) { checkContent := " " if c.Checked { diff --git a/container.go b/container.go index de658f8..2a82a2a 100644 --- a/container.go +++ b/container.go @@ -92,7 +92,7 @@ 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 := Scope(target, childBounds) diff --git a/detailview.go b/detailview.go index 1f001a8..f28b88e 100644 --- a/detailview.go +++ b/detailview.go @@ -137,7 +137,7 @@ func (d *DetailView) totalWidth() int { return ret } -func (d *DetailView) Draw(target IDrawTarget) { +func (d *DetailView) Draw(target DrawTarget) { top := 0 left := 0 diff --git a/draw_target.go b/draw_target.go index f6059f4..44db838 100644 --- a/draw_target.go +++ b/draw_target.go @@ -8,56 +8,124 @@ import ( "unicode/utf8" ) -type IDrawTarget interface { - Bounds() *Rect - SetCell(x, y int, foreground, background termbox.Attribute, char rune) - error +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{}) } +func Bounds(target DrawTarget) *Rect { + return &Rect{ + Left: 0, + Top: 0, + Width: target.Width(), + Height: target.Height(), + } +} + +type TermboxDrawTarget struct { + width, height int +} + +func (target TermboxDrawTarget) Width() int { + return target.width +} + +func (target TermboxDrawTarget) Height() int { + return target.height +} + +// Create a draw target that allows drawing to the entire terminal window. +func newTermboxDrawTarget() *TermboxDrawTarget { + terminalWidth, terminalHeight := termbox.Size() + + return &TermboxDrawTarget{ + width: terminalWidth, + height: terminalHeight, + } +} + +func (target TermboxDrawTarget) 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 TermboxDrawTarget) 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 " + + "TermboxDrawTarget") + } + + termbox.SetCell(x, y, 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 ScopedDrawTarget's drawable +// region. +func (target TermboxDrawTarget) 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) + } +} + // 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 +type ScopedDrawTarget struct { + width int + height int offsetLeft int offsetTop int + + parent DrawTarget } -// 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, - } +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 DrawTarget) SetCell(x, y int, +func (target ScopedDrawTarget) SetCell(x, y int, foreground, background termbox.Attribute, char rune) error { - if !target.Bounds().ContainsPoint(x, y) { + if !Bounds(target).ContainsPoint(x, y) { return errors.New( - "Coordinates are out of bounds for the DrawTarget") + "Coordinates are out of bounds for the ScopedDrawTarget") } - globalX, globalY := target.localToScreenCoords(x, y) + parentX, parentY := target.localToParentCoords(x, y) - termbox.SetCell(globalX, globalY, char, foreground, background) + 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 DrawTarget's drawable +// style. The text will be automatically clipped to the ScopedDrawTarget's drawable // region. -func (target DrawTarget) Print(x, y int, +func (target ScopedDrawTarget) Print(x, y int, foreground, background termbox.Attribute, text string, args ...interface{}) { formatted := fmt.Sprintf(text, args...) @@ -67,30 +135,28 @@ func (target DrawTarget) Print(x, y int, } } -// Create a DrawTarget which allows drawing to a portion of the parent's -// DrawTarget area. childBounds should be specified in the parent's local +// 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) IDrawTarget { - if !parent.Bounds().ContainsRect(childBounds) { - //return nil, errors.New("Provided child bounds would exceed " + - //"the parent's dimensions.") - panic("asdf") +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.") } - target := DrawTarget{ - offsetLeft: parent.offsetLeft + childBounds.Left, - offsetTop: parent.offsetTop + childBounds.Top, - Width: childBounds.Width, - Height: childBounds.Height, - } - - return target + return &ScopedDrawTarget{ + parent: parent, + offsetLeft: childBounds.Left, + offsetTop: childBounds.Top, + width: childBounds.Width, + height: childBounds.Height, + }, nil } -func (target DrawTarget) localToScreenCoords(x, y int) (int, int) { +func (target ScopedDrawTarget) localToParentCoords(x, y int) (int, int) { return target.offsetLeft + x, target.offsetTop + y } @@ -126,15 +192,3 @@ func normalizeString(input string) []rune { return output } - -// 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, - } -} diff --git a/editbox.go b/editbox.go index 459480e..a530ca0 100644 --- a/editbox.go +++ b/editbox.go @@ -209,7 +209,7 @@ func (e *EditBox) SetText(raw string) { e.fireTextChanged() } -func (e *EditBox) Draw(target IDrawTarget) { +func (e *EditBox) Draw(target DrawTarget) { textWidth := e.Bounds.Width textHeight := e.Bounds.Height - 1 // Bottom line free for modes/notices diff --git a/label.go b/label.go index f1da7e3..b5d520d 100644 --- a/label.go +++ b/label.go @@ -13,6 +13,6 @@ func (l *Label) GetBounds() *Rect { return &l.Bounds } -func (l *Label) Draw(target IDrawTarget) { +func (l *Label) Draw(target DrawTarget) { target.Print(0, 0, termbox.ColorWhite, termbox.ColorBlack, l.Text) } diff --git a/shared.go b/shared.go index 7be0db8..d296ddb 100644 --- a/shared.go +++ b/shared.go @@ -59,7 +59,7 @@ func matchBinding(ev escapebox.Event, kb KeyBinding) bool { type Control interface { GetBounds() *Rect - Draw(IDrawTarget) + Draw(DrawTarget) } type Focusable interface { @@ -106,7 +106,7 @@ func log(message string, args ...interface{}) { } func Refresh(root *Container) { - target := fullTerminalDrawTarget() + target := newTermboxDrawTarget() root.Draw(target) termbox.Flush() } diff --git a/textbox.go b/textbox.go index ef0e4c9..67bcdcc 100644 --- a/textbox.go +++ b/textbox.go @@ -30,7 +30,7 @@ func (t *TextBox) lastVisible() int { return t.scroll + t.visibleChars() - 1 } -func (t *TextBox) Draw(target IDrawTarget) { +func (t *TextBox) Draw(target DrawTarget) { target.Print(1, 1, termbox.ColorWhite, termbox.ColorBlack, t.Value[t.scroll:t.lastVisible()+1]) From 709adf3d1c094a8358e2472929d001e11c1328f2 Mon Sep 17 00:00:00 2001 From: Brian Steffens Date: Tue, 21 Aug 2018 01:48:13 -0700 Subject: [PATCH 3/3] More stuff --- button.go | 4 +- checkbox.go | 4 +- detailview.go | 4 +- draw_target.go | 180 +--------------------------------------- editbox.go | 4 +- label.go | 4 +- rect.go | 8 +- scoped_draw_target.go | 78 +++++++++++++++++ scrolled_draw_target.go | 71 ++++++++++++++++ shared.go | 2 +- termbox_draw_target.go | 91 ++++++++++++++++++++ textbox.go | 4 +- 12 files changed, 259 insertions(+), 195 deletions(-) create mode 100644 scoped_draw_target.go create mode 100644 scrolled_draw_target.go create mode 100644 termbox_draw_target.go diff --git a/button.go b/button.go index f6bfa52..d90b88e 100644 --- a/button.go +++ b/button.go @@ -14,8 +14,8 @@ 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) { diff --git a/checkbox.go b/checkbox.go index 4fbd110..3682f05 100644 --- a/checkbox.go +++ b/checkbox.go @@ -12,8 +12,8 @@ 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) { diff --git a/detailview.go b/detailview.go index f28b88e..0bfc857 100644 --- a/detailview.go +++ b/detailview.go @@ -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() { diff --git a/draw_target.go b/draw_target.go index 44db838..28f5c8a 100644 --- a/draw_target.go +++ b/draw_target.go @@ -1,11 +1,7 @@ package tui import ( - "errors" - "fmt" "github.com/nsf/termbox-go" - "golang.org/x/text/unicode/norm" - "unicode/utf8" ) type DrawTarget interface { @@ -17,178 +13,6 @@ type DrawTarget interface { args ...interface{}) } -func Bounds(target DrawTarget) *Rect { - return &Rect{ - Left: 0, - Top: 0, - Width: target.Width(), - Height: target.Height(), - } -} - -type TermboxDrawTarget struct { - width, height int -} - -func (target TermboxDrawTarget) Width() int { - return target.width -} - -func (target TermboxDrawTarget) Height() int { - return target.height -} - -// Create a draw target that allows drawing to the entire terminal window. -func newTermboxDrawTarget() *TermboxDrawTarget { - terminalWidth, terminalHeight := termbox.Size() - - return &TermboxDrawTarget{ - width: terminalWidth, - height: terminalHeight, - } -} - -func (target TermboxDrawTarget) 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 TermboxDrawTarget) 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 " + - "TermboxDrawTarget") - } - - termbox.SetCell(x, y, 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 ScopedDrawTarget's drawable -// region. -func (target TermboxDrawTarget) 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) - } -} - -// 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{}) { - formatted := fmt.Sprintf(text, args...) - normalized := normalizeString(formatted) - for i, r := range normalized { - target.SetCell(x+i, y, foreground, background, r) - } -} - -// 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 -} - -// 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 +func Bounds(target DrawTarget) Rect { + return Rect{0, 0, target.Width(), target.Height()} } diff --git a/editbox.go b/editbox.go index a530ca0..81a9db5 100644 --- a/editbox.go +++ b/editbox.go @@ -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'} diff --git a/label.go b/label.go index b5d520d..4e6866b 100644 --- a/label.go +++ b/label.go @@ -9,8 +9,8 @@ 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) { diff --git a/rect.go b/rect.go index ad2cd51..0fe48b7 100644 --- a/rect.go +++ b/rect.go @@ -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 } diff --git a/scoped_draw_target.go b/scoped_draw_target.go new file mode 100644 index 0000000..7fcdec5 --- /dev/null +++ b/scoped_draw_target.go @@ -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 +} diff --git a/scrolled_draw_target.go b/scrolled_draw_target.go new file mode 100644 index 0000000..beb276e --- /dev/null +++ b/scrolled_draw_target.go @@ -0,0 +1,71 @@ +package tui + +import ( + "errors" + "github.com/nsf/termbox-go" +) + +type ScrolledDrawTarget struct { + width int + height int + + scrollLeft int + scrollTop int + + parent DrawTarget +} + +func (target ScrolledDrawTarget) Width() int { + return target.width +} + +func (target ScrolledDrawTarget) Height() int { + return target.height +} + +func (target ScrolledDrawTarget) ScrollLeft() int { + return target.scrollLeft +} + +func (target ScrolledDrawTarget) ScrollTop() int { + return target.scrollTop +} + +func (target ScrolledDrawTarget) 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 " + + "ScrolledDrawTarget") + } + + parentX, parentY := target.scrollCoords(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 ScrolledDrawTarget's drawable +// region. +func (target ScrolledDrawTarget) Print(x, y int, + foreground, background termbox.Attribute, text string, + args ...interface{}) { + parentX, parentY := target.scrollCoords(x, y) + target.parent.Print(parentX, parentY, foreground, background, text, + args...) +} + +func Scroll(parent DrawTarget, width, height int) (ScrolledDrawTarget, error) { + return ScrolledDrawTarget{ + width: width, + height: height, + + scrollLeft: 0, + scrollTop: 0, + + parent: parent, + }, nil +} + +func (target ScrolledDrawTarget) scrollCoords(x, y int) (int, int) { + return x - target.scrollLeft, y - target.scrollTop +} diff --git a/shared.go b/shared.go index d296ddb..1457cd9 100644 --- a/shared.go +++ b/shared.go @@ -58,7 +58,7 @@ func matchBinding(ev escapebox.Event, kb KeyBinding) bool { } type Control interface { - GetBounds() *Rect + GetBounds() Rect Draw(DrawTarget) } diff --git a/termbox_draw_target.go b/termbox_draw_target.go new file mode 100644 index 0000000..e9f82f8 --- /dev/null +++ b/termbox_draw_target.go @@ -0,0 +1,91 @@ +package tui + +import ( + "fmt" + "errors" + "github.com/nsf/termbox-go" + "golang.org/x/text/unicode/norm" + "unicode/utf8" +) + +type TermboxDrawTarget struct { + width, height int +} + +func (target TermboxDrawTarget) Width() int { + return target.width +} + +func (target TermboxDrawTarget) Height() int { + return target.height +} + +// Create a draw target that allows drawing to the entire terminal window. +func newTermboxDrawTarget() TermboxDrawTarget { + terminalWidth, terminalHeight := termbox.Size() + + return TermboxDrawTarget{ + width: terminalWidth, + height: terminalHeight, + } +} + +// Set one terminal cell. If (x, y) is out of bounds, an error will be returned +// and the terminal will be unchanged. +func (target TermboxDrawTarget) 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 " + + "TermboxDrawTarget") + } + + termbox.SetCell(x, y, 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 ScopedDrawTarget's drawable +// region. +func (target TermboxDrawTarget) 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) + } +} + +// 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 +} diff --git a/textbox.go b/textbox.go index 67bcdcc..183c85d 100644 --- a/textbox.go +++ b/textbox.go @@ -14,8 +14,8 @@ type TextBox struct { focus bool } -func (t *TextBox) GetBounds() *Rect { - return &t.Bounds +func (t *TextBox) GetBounds() Rect { + return t.Bounds } func (t *TextBox) maxVisibleChars() int {