1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00

Implementing the text widget.

And adding a demo.
This commit is contained in:
Jakub Sobon 2018-05-14 22:16:14 +01:00
parent 4c54b8a46e
commit d6c153fbaf
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
3 changed files with 1132 additions and 0 deletions

View File

@ -0,0 +1,161 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Binary textdemo displays a couple of Text widgets.
// Exist when 'q' is pressed.
package main
import (
"context"
"fmt"
"math/rand"
"time"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgets/text"
)
// quotations are used as text that is rolled up in a text widget.
var quotations = []string{
"When some see coincidence, I see consequence. When others see chance, I see cost.",
"You cannot pass....I am a servant of the Secret Fire, wielder of the flame of Anor. You cannot pass. The dark fire will not avail you, flame of Udûn. Go back to the Shadow! You cannot pass.",
"I'm going to make him an offer he can't refuse.",
"May the Force be with you.",
"The stuff that dreams are made of.",
"There's no place like home.",
"Show me the money!",
"I want to be alone.",
"I'll be back.",
}
// writeLines writes a line of text to the text widget every delay.
// Exits when the context expires.
func writeLines(ctx context.Context, t *text.Text, delay time.Duration) {
s := rand.NewSource(time.Now().Unix())
r := rand.New(s)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
i := r.Intn(len(quotations))
if err := t.Write(fmt.Sprintf("%s\n", quotations[i])); err != nil {
panic(err)
}
case <-ctx.Done():
return
}
}
}
func main() {
t, err := termbox.New()
if err != nil {
panic(err)
}
defer t.Close()
ctx, cancel := context.WithCancel(context.Background())
borderless := text.New()
if err := borderless.Write("Text without border."); err != nil {
panic(err)
}
unicode := text.New()
if err := unicode.Write("你好,世界!"); err != nil {
panic(err)
}
trimmed := text.New()
if err := trimmed.Write("Trims lines that don't fit onto the canvas because they are too long for its width.."); err != nil {
panic(err)
}
wrapped := text.New(text.WrapAtRunes())
if err := wrapped.Write("Supports", text.WriteCellOpts(cell.FgColor(cell.ColorRed))); err != nil {
panic(err)
}
if err := wrapped.Write(" colors", text.WriteCellOpts(cell.FgColor(cell.ColorBlue))); err != nil {
panic(err)
}
if err := wrapped.Write(". Wraps long lines at rune boundaries if the WrapAtRunes() option is provided.\nSupports newline character to\ncreate\nnewlines\nmanually.\nTrims the content if it is too long.\n\n\n\nToo long."); err != nil {
panic(err)
}
rolled := text.New(text.RollContent(), text.WrapAtRunes())
if err := rolled.Write("Rolls the content upwards if RollContent() option is provided.\nSupports keyboard and mouse scrolling.\n\n"); err != nil {
panic(err)
}
go writeLines(ctx, rolled, 1*time.Second)
c := container.New(
t,
container.Border(draw.LineStyleLight),
container.BorderTitle("PRESS Q TO QUIT"),
container.SplitVertical(
container.Left(
container.SplitHorizontal(
container.Top(
container.SplitHorizontal(
container.Top(
container.SplitVertical(
container.Left(
container.PlaceWidget(borderless),
),
container.Right(
container.Border(draw.LineStyleLight),
container.BorderTitle("你好,世界!"),
container.PlaceWidget(unicode),
),
),
),
container.Bottom(
container.Border(draw.LineStyleLight),
container.BorderTitle("Trims lines"),
container.PlaceWidget(trimmed),
),
),
),
container.Bottom(
container.Border(draw.LineStyleLight),
container.BorderTitle("Wraps lines at rune boundaries"),
container.PlaceWidget(wrapped),
),
),
),
container.Right(
container.Border(draw.LineStyleLight),
container.BorderTitle("Rolls and scrolls content"),
container.PlaceWidget(rolled),
),
),
)
quitter := func(k *terminalapi.Keyboard) {
if k.Key == 'q' || k.Key == 'Q' {
cancel()
}
}
if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter)); err != nil {
panic(err)
}
}

267
widgets/text/text.go Normal file
View File

@ -0,0 +1,267 @@
// Package text contains a widget that displays textual data.
package text
import (
"bytes"
"errors"
"fmt"
"image"
"sync"
"unicode"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
// Text displays a block of text.
//
// Each line of the text is either trimmed or wrapped according to the provided
// options. The entire text content is either trimmed or rolled up through the
// canvas according to the provided options.
//
// By default the widget supports scrolling of content with either the keyboard
// or mouse. See the options for the default keys and mouse buttons.
//
// Implements widgetapi.Widget. This object is thread-safe.
type Text struct {
// buff contains the text to be displayed in the widget.
buff bytes.Buffer
// givenWOpts are write options given for the text.
givenWOpts givenWOpts
// scroll tracks scrolling the position.
scroll *scrollTracker
// lastWidth stores the width of the last canvas the widget drew on.
// Used to determine if the previous line wrapping was invalidated.
lastWidth int
// newText indicates if new text was added to the widget.
// Used to determine if the previous line wrapping was invalidated.
newText bool
// lines stores the starting locations in bytes of all the lines in the
// buffer. I.e. positions of newline characters and of any calculated line wraps.
lines []int
// mu protects the Text widget.
mu sync.Mutex
// opts are the provided options.
opts *options
}
// New returns a new text widget.
func New(opts ...Option) *Text {
opt := newOptions(opts...)
return &Text{
givenWOpts: newGivenWOpts(),
scroll: newScrollTracker(opt),
opts: opt,
}
}
// Reset resets the widget back to empty content.
func (t *Text) Reset() {
t.mu.Lock()
defer t.mu.Unlock()
t.buff.Reset()
t.givenWOpts = newGivenWOpts()
t.scroll = newScrollTracker(t.opts)
t.lastWidth = 0
t.newText = true
t.lines = nil
}
// Write writes text for the widget to display. Multiple calls append
// additional text. The text cannot control characters (unicode.IsControl) or
// space character (unicode.IsSpace) other than:
// ' ', '\n'
// Any newline ('\n') characters are interpreted as newlines when displaying
// the text.
func (t *Text) Write(text string, wOpts ...WriteOption) error {
t.mu.Lock()
defer t.mu.Unlock()
if err := validText(text); err != nil {
return err
}
pos := t.buff.Len()
t.givenWOpts[pos] = newOptsRange(pos, pos+len(text), newWriteOptions(wOpts...))
if _, err := t.buff.WriteString(text); err != nil {
return err
}
t.newText = true
return nil
}
// lineTrimNeeded returns true if the text on this line needs to be trimmed.
// I.e. if the Text gadget was configured to trim lines, the current point falls
// outside of the canvas and the current rune doesn't start a new line.
func (t *Text) lineTrimNeeded(cur image.Point, cvs *canvas.Canvas, r rune) bool {
if cur.X < cvs.Area().Dx() || r == '\n' {
return false
}
return !t.opts.wrapAtRunes
}
// minLinesForMarkers are the minimum amount of lines required on the canvas in
// order to draw the scroll markers ('⇧' and '⇩').
const minLinesForMarkers = 3
// draw draws the text context on the canvas starting at the specified line.
// Argument starts are the starting positions of all the lines in the text.
func (t *Text) draw(text string, cvs *canvas.Canvas, starts []int, fromLine int) error {
var cur image.Point // Tracks the current drawing position on the canvas.
lines := len(starts)
height := cvs.Area().Dy()
optRange := t.givenWOpts.forPosition(0) // Text options for the current byte.
startPos := starts[fromLine]
for i, r := range text {
if i < startPos {
continue
}
// Draw the scroll up marker on the first line if there is more text
// above the canvas.
if cur.Y == 0 && height >= minLinesForMarkers && fromLine > 0 {
if err := cvs.SetCell(cur, '⇧'); err != nil {
return err
}
cur = image.Point{0, cur.Y + 1} // Move to the next line.
startPos = starts[fromLine+1] // Skip one line of text, the marker replaced it.
continue
}
if r == '\n' || wrapNeeded(r, cur.X, cvs.Area().Dx(), t.opts) {
cur = image.Point{0, cur.Y + 1} // Move to the next line.
}
// Draw the scroll down marker on the last line if there is more text
// below the canvas.
if cur.Y == height-1 && height >= minLinesForMarkers && height < lines-fromLine {
if height >= minLinesForMarkers {
if err := cvs.SetCell(cur, '⇩'); err != nil {
return err
}
}
break
}
if r == '\n' {
continue // Don't print the newline runes, just interpret them above.
}
if t.lineTrimNeeded(cur, cvs, r) {
// Trim by replacing the last printed rune.
prev := image.Point{cur.X - 1, cur.Y}
if prev.In(cvs.Area()) {
if err := cvs.SetCell(prev, '…'); err != nil {
return err
}
}
}
if !cur.In(cvs.Area()) {
continue // Skip any runes belonging to the trimmed area on a line.
}
if i >= optRange.high { // Get the next write options.
optRange = t.givenWOpts.forPosition(i)
}
if err := cvs.SetCell(cur, r, optRange.opts.cellOpts); err != nil {
return err
}
cur = image.Point{cur.X + 1, cur.Y} // Move within the same line.
}
return nil
}
// Draw draws the text onto the canvas.
// Implements widgetapi.Widget.Draw.
func (t *Text) Draw(cvs *canvas.Canvas) error {
t.mu.Lock()
defer t.mu.Unlock()
text := t.buff.String()
width := cvs.Area().Dx()
if t.newText || t.lastWidth != width {
// The previous text preprocessing (line wrapping) is invalidated when
// new text is added or the width of the canvas changed.
t.lines = findLines(text, width, t.opts)
}
t.lastWidth = width
if len(t.lines) == 0 {
return nil // Nothing to draw if there's no text.
}
height := cvs.Area().Dy()
fromLine := t.scroll.firstLine(len(t.lines), height)
if err := t.draw(text, cvs, t.lines, fromLine); err != nil {
return err
}
t.newText = false
return nil
}
// Implements widgetapi.Widget.Keyboard.
func (t *Text) Keyboard(k *terminalapi.Keyboard) error {
t.mu.Lock()
defer t.mu.Unlock()
switch {
case k.Key == t.opts.keyUp:
t.scroll.upOneLine()
case k.Key == t.opts.keyDown:
t.scroll.downOneLine()
case k.Key == t.opts.keyPgUp:
t.scroll.upOnePage()
case k.Key == t.opts.keyPgDown:
t.scroll.downOnePage()
}
return nil
}
// Implements widgetapi.Widget.Mouse.
func (t *Text) Mouse(m *terminalapi.Mouse) error {
t.mu.Lock()
defer t.mu.Unlock()
switch b := m.Button; {
case b == t.opts.mouseUpButton:
t.scroll.upOneLine()
case b == t.opts.mouseDownButton:
t.scroll.downOneLine()
}
return nil
}
func (t *Text) Options() widgetapi.Options {
return widgetapi.Options{
MinimumSize: image.Point{1, 1},
WantMouse: !t.opts.disableScrolling,
WantKeyboard: !t.opts.disableScrolling,
}
}
// validText validates the provided text.
func validText(text string) error {
if text == "" {
return errors.New("the text cannot be empty")
}
for _, c := range text {
if c == ' ' || c == '\n' { // Allowed space and control runes.
continue
}
if unicode.IsControl(c) {
return fmt.Errorf("the provided text %q cannot contain control characters, found: %q", text, c)
}
if unicode.IsSpace(c) {
return fmt.Errorf("the provided text %q cannot contain space character %q", text, c)
}
}
return nil
}

704
widgets/text/text_test.go Normal file
View File

@ -0,0 +1,704 @@
package text
import (
"image"
"testing"
"github.com/kylelemons/godebug/pretty"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/canvas/testcanvas"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/draw/testdraw"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/mouse"
"github.com/mum4k/termdash/terminal/faketerm"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
func TestTextDraws(t *testing.T) {
tests := []struct {
desc string
canvas image.Rectangle
opts []Option
writes func(*Text) error
events func(*Text)
want func(size image.Point) *faketerm.Terminal
wantWriteErr bool
}{
{
desc: "empty when no written text",
canvas: image.Rect(0, 0, 1, 1),
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
},
{
desc: "write fails for invalid text",
canvas: image.Rect(0, 0, 1, 1),
writes: func(widget *Text) error {
return widget.Write("\thello")
},
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantWriteErr: true,
},
{
desc: "draws line of text",
canvas: image.Rect(0, 0, 10, 1),
writes: func(widget *Text) error {
return widget.Write("hello")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "hello", image.Point{0, 0})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "multiple writes append",
canvas: image.Rect(0, 0, 12, 1),
writes: func(widget *Text) error {
if err := widget.Write("hello"); err != nil {
return err
}
if err := widget.Write(" "); err != nil {
return err
}
if err := widget.Write("world"); err != nil {
return err
}
return nil
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "hello world", image.Point{0, 0})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "reset clears the content",
canvas: image.Rect(0, 0, 12, 1),
writes: func(widget *Text) error {
if err := widget.Write("hello", WriteCellOpts(cell.FgColor(cell.ColorRed))); err != nil {
return err
}
if err := widget.Write(" "); err != nil {
return err
}
widget.Reset()
if err := widget.Write("world"); err != nil {
return err
}
return nil
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "world", image.Point{0, 0})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "respects newlines in the input text",
canvas: image.Rect(0, 0, 10, 10),
writes: func(widget *Text) error {
return widget.Write("\n\nhello\n\nworld\n\n")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "hello", image.Point{0, 2})
testdraw.MustText(c, "world", image.Point{0, 4})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "respects write options",
canvas: image.Rect(0, 0, 10, 3),
writes: func(widget *Text) error {
if err := widget.Write("default\n"); err != nil {
return err
}
if err := widget.Write("red\n", WriteCellOpts(cell.FgColor(cell.ColorRed))); err != nil {
return err
}
if err := widget.Write("blue\n", WriteCellOpts(cell.FgColor(cell.ColorBlue))); err != nil {
return err
}
return nil
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "default", image.Point{0, 0})
testdraw.MustText(c, "red", image.Point{0, 1}, draw.TextCellOpts(cell.FgColor(cell.ColorRed)))
testdraw.MustText(c, "blue", image.Point{0, 2}, draw.TextCellOpts(cell.FgColor(cell.ColorBlue)))
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "trims long lines",
canvas: image.Rect(0, 0, 10, 3),
writes: func(widget *Text) error {
return widget.Write("hello world\nshort\nand long again")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "hello wor…", image.Point{0, 0})
testdraw.MustText(c, "short", image.Point{0, 1})
testdraw.MustText(c, "and long …", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "trims content when longer than canvas, no scroll marker on small canvas",
canvas: image.Rect(0, 0, 10, 2),
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "line0", image.Point{0, 0})
testdraw.MustText(c, "line1", image.Point{0, 1})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "trims content when longer than canvas and draws bottom scroll marker",
canvas: image.Rect(0, 0, 10, 3),
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "line0", image.Point{0, 0})
testdraw.MustText(c, "line1", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls down on mouse wheel down a line at a time",
canvas: image.Rect(0, 0, 10, 3),
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3")
},
events: func(widget *Text) {
widget.Mouse(&terminalapi.Mouse{
Button: mouse.ButtonWheelDown,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "line3", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "doesn't draw the scroll up marker on small canvas",
canvas: image.Rect(0, 0, 10, 2),
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3")
},
events: func(widget *Text) {
widget.Mouse(&terminalapi.Mouse{
Button: mouse.ButtonWheelDown,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "line1", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls down on down arrow a line at a time",
canvas: image.Rect(0, 0, 10, 3),
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3")
},
events: func(widget *Text) {
widget.Keyboard(&terminalapi.Keyboard{
Key: keyboard.KeyArrowDown,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "line3", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls down on pageDn a page at a time",
canvas: image.Rect(0, 0, 10, 3),
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3\nline4\nline5\nline6")
},
events: func(widget *Text) {
widget.Keyboard(&terminalapi.Keyboard{
Key: keyboard.KeyPgDn,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line4", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls down using custom mouse button a line at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
ScrollMouseButtons(mouse.ButtonLeft, mouse.ButtonRight),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3")
},
events: func(widget *Text) {
widget.Mouse(&terminalapi.Mouse{
Button: mouse.ButtonRight,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "line3", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls down using custom key a line at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
ScrollKeys('u', 'd', 'k', 'l'),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3")
},
events: func(widget *Text) {
widget.Keyboard(&terminalapi.Keyboard{
Key: 'd',
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "line3", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls down using custom key a page at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
ScrollKeys('u', 'd', 'k', 'l'),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3\nline4\nline5\nline6")
},
events: func(widget *Text) {
widget.Keyboard(&terminalapi.Keyboard{
Key: 'l',
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line4", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "wraps lines at rune boundaries",
canvas: image.Rect(0, 0, 10, 5),
opts: []Option{
WrapAtRunes(),
},
writes: func(widget *Text) error {
return widget.Write("hello world\nshort\nand long again")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "hello worl", image.Point{0, 0})
testdraw.MustText(c, "d", image.Point{0, 1})
testdraw.MustText(c, "short", image.Point{0, 2})
testdraw.MustText(c, "and long a", image.Point{0, 3})
testdraw.MustText(c, "gain", image.Point{0, 4})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "rolls content upwards and trims lines",
canvas: image.Rect(0, 0, 10, 2),
opts: []Option{
RollContent(),
},
writes: func(widget *Text) error {
return widget.Write("hello world\nshort\nand long again")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "short", image.Point{0, 0})
testdraw.MustText(c, "and long …", image.Point{0, 1})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "rolls content upwards and draws an up scroll marker",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
RollContent(),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "line3", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "rolls content upwards and wraps lines at rune boundaries",
canvas: image.Rect(0, 0, 10, 2),
opts: []Option{
RollContent(),
WrapAtRunes(),
},
writes: func(widget *Text) error {
return widget.Write("hello world\nshort\nand long again")
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "and long a", image.Point{0, 0})
testdraw.MustText(c, "gain", image.Point{0, 1})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls up on mouse wheel up a line at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
RollContent(),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3\nline4")
},
events: func(widget *Text) {
// Draw once to roll the content all the way down before we scroll.
if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
panic(err)
}
widget.Mouse(&terminalapi.Mouse{
Button: mouse.ButtonWheelUp,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls up on up arrow a line at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
RollContent(),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3\nline4")
},
events: func(widget *Text) {
// Draw once to roll the content all the way down before we scroll.
if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
panic(err)
}
widget.Keyboard(&terminalapi.Keyboard{
Key: keyboard.KeyArrowUp,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls up on pageUp a page at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
RollContent(),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3\nline4")
},
events: func(widget *Text) {
// Draw once to roll the content all the way down before we scroll.
if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
panic(err)
}
widget.Keyboard(&terminalapi.Keyboard{
Key: keyboard.KeyPgUp,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "line0", image.Point{0, 0})
testdraw.MustText(c, "line1", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls up using custom mouse button a line at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
RollContent(),
ScrollMouseButtons(mouse.ButtonLeft, mouse.ButtonRight),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3\nline4")
},
events: func(widget *Text) {
// Draw once to roll the content all the way down before we scroll.
if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
panic(err)
}
widget.Mouse(&terminalapi.Mouse{
Button: mouse.ButtonLeft,
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls up using custom key a line at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
ScrollKeys('u', 'd', 'k', 'l'),
RollContent(),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3\nline4")
},
events: func(widget *Text) {
// Draw once to roll the content all the way down before we scroll.
if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
panic(err)
}
widget.Keyboard(&terminalapi.Keyboard{
Key: 'u',
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "⇧", image.Point{0, 0})
testdraw.MustText(c, "line2", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "scrolls up using custom key a page at a time",
canvas: image.Rect(0, 0, 10, 3),
opts: []Option{
ScrollKeys('u', 'd', 'k', 'l'),
RollContent(),
},
writes: func(widget *Text) error {
return widget.Write("line0\nline1\nline2\nline3\nline4")
},
events: func(widget *Text) {
// Draw once to roll the content all the way down before we scroll.
if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
panic(err)
}
widget.Keyboard(&terminalapi.Keyboard{
Key: 'k',
})
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "line0", image.Point{0, 0})
testdraw.MustText(c, "line1", image.Point{0, 1})
testdraw.MustText(c, "⇩", image.Point{0, 2})
testcanvas.MustApply(c, ft)
return ft
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
c, err := canvas.New(tc.canvas)
if err != nil {
t.Fatalf("canvas.New => unexpected error: %v", err)
}
widget := New(tc.opts...)
if tc.writes != nil {
err := tc.writes(widget)
if (err != nil) != tc.wantWriteErr {
t.Errorf("Write => unexpected error: %v, wantWriteErr: %v", err, tc.wantWriteErr)
}
if err != nil {
return
}
}
if tc.events != nil {
tc.events(widget)
}
if err := widget.Draw(c); err != nil {
t.Fatalf("Draw => unexpected error: %v", err)
}
got, err := faketerm.New(c.Size())
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
if err := c.Apply(got); err != nil {
t.Fatalf("Apply => unexpected error: %v", err)
}
if diff := faketerm.Diff(tc.want(c.Size()), got); diff != "" {
t.Errorf("Draw => %v", diff)
}
})
}
}
func TestOptions(t *testing.T) {
tests := []struct {
desc string
opts []Option
want widgetapi.Options
}{
{
desc: "minimum size for one character",
want: widgetapi.Options{
MinimumSize: image.Point{1, 1},
WantKeyboard: true,
WantMouse: true,
},
},
{
desc: "disabling scrolling removes keyboard and mouse",
opts: []Option{
DisableScrolling(),
},
want: widgetapi.Options{
MinimumSize: image.Point{1, 1},
WantKeyboard: false,
WantMouse: false,
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
text := New(tc.opts...)
got := text.Options()
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}