mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
Implementing the segment display widget.
This commit is contained in:
parent
38aae8e556
commit
35f11d3a5e
@ -18,7 +18,6 @@
|
||||
package attrrange
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
@ -72,10 +71,6 @@ func (t *Tracker) Add(low, high, attrIdx int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrNotFound indicates that the requested position wasn't found in any of the
|
||||
// known ranges.
|
||||
var ErrNotFound = errors.New("range not found")
|
||||
|
||||
// ForPosition returns attribute index that apply to the specified position.
|
||||
// Returns ErrNotFound when the requested position wasn't found in any of the
|
||||
// known ranges.
|
||||
@ -102,7 +97,7 @@ func (t *Tracker) ForPosition(pos int) (*AttrRange, error) {
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
return nil, ErrNotFound
|
||||
return nil, fmt.Errorf("did not find attribute range for position %d", pos)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
@ -61,13 +61,13 @@ func TestForPosition(t *testing.T) {
|
||||
update func(*Tracker) error
|
||||
pos int
|
||||
want *AttrRange
|
||||
wantErr error
|
||||
wantErr bool
|
||||
wantUpdateErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails when no ranges given",
|
||||
pos: 0,
|
||||
wantErr: ErrNotFound,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails to add a duplicate",
|
||||
@ -88,7 +88,7 @@ func TestForPosition(t *testing.T) {
|
||||
return tr.Add(5, 10, 41)
|
||||
},
|
||||
pos: 1,
|
||||
wantErr: ErrNotFound,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls on the lower",
|
||||
@ -132,7 +132,7 @@ func TestForPosition(t *testing.T) {
|
||||
return tr.Add(5, 10, 41)
|
||||
},
|
||||
pos: 10,
|
||||
wantErr: ErrNotFound,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
@ -150,7 +150,7 @@ func TestForPosition(t *testing.T) {
|
||||
}
|
||||
|
||||
got, err := tr.ForPosition(tc.pos)
|
||||
if err != tc.wantErr {
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("ForPosition => unexpected error:%v, wantErr:%v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -50,3 +50,11 @@ func MustSetCell(c *canvas.Canvas, p image.Point, r rune, opts ...cell.Option) i
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
// MustCopyTo copies the content of the source canvas onto the destination
|
||||
// canvas or panics.
|
||||
func MustCopyTo(src, dst *canvas.Canvas) {
|
||||
if err := src.CopyTo(dst); err != nil {
|
||||
panic(fmt.Sprintf("canvas.CopyTo => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
23
draw/segdisp/sixteen/testsixteen/testsixteen.go
Normal file
23
draw/segdisp/sixteen/testsixteen/testsixteen.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Package testsixteen provides helpers for tests that use the sixteen package.
|
||||
package testsixteen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen"
|
||||
)
|
||||
|
||||
// MustSetCharacter sets the character on the display or panics.
|
||||
func MustSetCharacter(d *sixteen.Display, c rune) {
|
||||
if err := d.SetCharacter(c); err != nil {
|
||||
panic(fmt.Errorf("sixteen.Display.SetCharacter => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// MustDraw draws the display onto the canvas or panics.
|
||||
func MustDraw(d *sixteen.Display, cvs *canvas.Canvas, opts ...sixteen.Option) {
|
||||
if err := d.Draw(cvs, opts...); err != nil {
|
||||
panic(fmt.Errorf("sixteen.Display.Draw => unexpected error: %v", err))
|
||||
}
|
||||
}
|
@ -39,11 +39,6 @@ type options struct {
|
||||
maximizeSegSize bool
|
||||
}
|
||||
|
||||
// validate validates the provided options.
|
||||
func (o *options) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// newOptions returns options with the default values set.
|
||||
func newOptions() *options {
|
||||
return &options{
|
||||
@ -73,7 +68,7 @@ func AlignVertical(v align.Vertical) Option {
|
||||
// When this option is set and the user has provided more text than we can fit
|
||||
// on the canvas, the widget will prefer to maximize height of individual
|
||||
// characters which will result in earlier trimming of the text.
|
||||
func MaximizeSegmentHeight(v align.Vertical) Option {
|
||||
func MaximizeSegmentHeight() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.maximizeSegSize = true
|
||||
})
|
||||
@ -85,7 +80,7 @@ func MaximizeSegmentHeight(v align.Vertical) Option {
|
||||
// on the canvas, the widget will prefer to decrease the height of individual
|
||||
// characters and fit more of them on the canvas.
|
||||
// This is the default behavior.
|
||||
func MaximizeDisplayedText(v align.Vertical) Option {
|
||||
func MaximizeDisplayedText() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.maximizeSegSize = false
|
||||
})
|
||||
|
@ -19,9 +19,12 @@ package segmentdisplay
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/attrrange"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
@ -42,6 +45,11 @@ type SegmentDisplay struct {
|
||||
// buff contains the text to be displayed.
|
||||
buff bytes.Buffer
|
||||
|
||||
// givenWOpts are write options given for the text in buff.
|
||||
givenWOpts []*writeOptions
|
||||
// wOptsTracker tracks the positions in a buff to which the givenWOpts apply.
|
||||
wOptsTracker *attrrange.Tracker
|
||||
|
||||
// mu protects the widget.
|
||||
mu sync.Mutex
|
||||
|
||||
@ -50,26 +58,63 @@ type SegmentDisplay struct {
|
||||
}
|
||||
|
||||
// New returns a new SegmentDisplay.
|
||||
func New(opts ...Option) (*SegmentDisplay, error) {
|
||||
func New(opts ...Option) *SegmentDisplay {
|
||||
opt := newOptions()
|
||||
for _, o := range opts {
|
||||
o.set(opt)
|
||||
}
|
||||
if err := opt.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SegmentDisplay{
|
||||
opts: opt,
|
||||
}, nil
|
||||
wOptsTracker: attrrange.NewTracker(),
|
||||
opts: opt,
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes text for the widget to display. Multiple calls append
|
||||
// additional text.
|
||||
// TextChunk is a part of or the full text that will be displayed.
|
||||
type TextChunk struct {
|
||||
text string
|
||||
wOpts *writeOptions
|
||||
}
|
||||
|
||||
// NewChunk creates a new text chunk.
|
||||
func NewChunk(text string, wOpts ...WriteOption) *TextChunk {
|
||||
return &TextChunk{
|
||||
text: text,
|
||||
wOpts: newWriteOptions(wOpts...),
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes text for the widget to display. Subsequent calls replace text
|
||||
// written previously. All the provided text chunks are broken into characters
|
||||
// and each character is displayed in one segment.
|
||||
//
|
||||
// The provided write options determine the behavior when text contains
|
||||
// unsupported characters.
|
||||
func (sd *SegmentDisplay) Write(text string /* TODO wOpts ...WriteOption */) error {
|
||||
// unsupported characters and set cell options for cells that contain
|
||||
// individual display segments.
|
||||
//
|
||||
// Each of the text chunks can have its own options. At least one chunk must be
|
||||
// specified.
|
||||
func (sd *SegmentDisplay) Write(chunks ...*TextChunk) error {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
|
||||
if len(chunks) == 0 {
|
||||
return errors.New("at least one text chunk must be specified")
|
||||
}
|
||||
|
||||
for i, tc := range chunks {
|
||||
if ok, badRunes := sixteen.SupportsChars(tc.text); !ok && tc.wOpts.errOnUnsupported {
|
||||
return fmt.Errorf("text chunk[%d] contains unsupported characters %v, clean the text or provide the WriteSanitize option", i, badRunes)
|
||||
}
|
||||
text := sixteen.Sanitize(tc.text)
|
||||
|
||||
pos := sd.buff.Len()
|
||||
sd.givenWOpts = append(sd.givenWOpts, tc.wOpts)
|
||||
wOptsIdx := len(sd.givenWOpts) - 1
|
||||
if err := sd.wOptsTracker.Add(pos, pos+len(text), wOptsIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
sd.buff.WriteString(text)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -77,6 +122,76 @@ func (sd *SegmentDisplay) Write(text string /* TODO wOpts ...WriteOption */) err
|
||||
func (sd *SegmentDisplay) Reset() {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
|
||||
sd.buff.Reset()
|
||||
sd.givenWOpts = nil
|
||||
sd.wOptsTracker = attrrange.NewTracker()
|
||||
}
|
||||
|
||||
// segArea given an area available for drawing returns the area required for a
|
||||
// single segment and the number of segments we can fit.
|
||||
func (sd *SegmentDisplay) segArea(ar image.Rectangle) (image.Rectangle, int, error) {
|
||||
segAr, err := sixteen.Required(ar)
|
||||
if err != nil {
|
||||
return image.ZR, 0, fmt.Errorf("sixteen.Required => %v", err)
|
||||
}
|
||||
|
||||
canFit := ar.Dx() / segAr.Dx()
|
||||
return segAr, canFit, nil
|
||||
}
|
||||
|
||||
// maximizeFit finds the largest individual segment size that enables us to fit
|
||||
// the most characters onto a canvas with the provided area. Returns the area
|
||||
// required for a single segment and the number of segments we can fit.
|
||||
func (sd *SegmentDisplay) maximizeFit(ar image.Rectangle) (image.Rectangle, int, error) {
|
||||
bestSegAr := image.ZR
|
||||
bestCanFit := 0
|
||||
need := sd.buff.Len()
|
||||
for height := ar.Dy(); height >= sixteen.MinRows; height-- {
|
||||
ar := image.Rect(ar.Min.X, ar.Min.Y, ar.Max.X, ar.Min.Y+height)
|
||||
segAr, canFit, err := sd.segArea(ar)
|
||||
if err != nil {
|
||||
return image.ZR, 0, err
|
||||
}
|
||||
|
||||
if canFit >= need {
|
||||
return segAr, canFit, nil
|
||||
}
|
||||
bestSegAr = segAr
|
||||
bestCanFit = canFit
|
||||
}
|
||||
|
||||
if bestSegAr.Eq(image.ZR) || bestCanFit == 0 {
|
||||
return image.ZR, 0, fmt.Errorf("failed to maximize character fit for area: %v", ar)
|
||||
}
|
||||
return bestSegAr, bestCanFit, nil
|
||||
}
|
||||
|
||||
// preprocess determines the size of individual segments maximizing their
|
||||
// height or the amount of displayed characters based on the specified options.
|
||||
// Returns the area required for a single segment and the text that we can fit.
|
||||
func (sd *SegmentDisplay) preprocess(cvsAr image.Rectangle) (image.Rectangle, string, error) {
|
||||
segAr, canFit, err := sd.segArea(cvsAr)
|
||||
if err != nil {
|
||||
return image.ZR, "", err
|
||||
}
|
||||
|
||||
text := sd.buff.String()
|
||||
need := len(text)
|
||||
|
||||
if need <= canFit {
|
||||
return segAr, text, nil
|
||||
}
|
||||
|
||||
if sd.opts.maximizeSegSize {
|
||||
return segAr, text[:canFit], nil
|
||||
}
|
||||
|
||||
bestAr, bestFit, err := sd.maximizeFit(cvsAr)
|
||||
if err != nil {
|
||||
return image.ZR, "", err
|
||||
}
|
||||
return bestAr, text[:bestFit], nil
|
||||
}
|
||||
|
||||
// Draw draws the SegmentDisplay widget onto the canvas.
|
||||
@ -85,7 +200,60 @@ func (sd *SegmentDisplay) Draw(cvs *canvas.Canvas) error {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
|
||||
return errors.New("unimplemented")
|
||||
if sd.buff.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
segAr, text, err := sd.preprocess(cvs.Area())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
needAr := image.Rect(0, 0, segAr.Dx()*len(text), segAr.Dy())
|
||||
aligned, err := align.Rectangle(cvs.Area(), needAr, sd.opts.hAlign, sd.opts.vAlign)
|
||||
if err != nil {
|
||||
return fmt.Errorf("align.Rectangle => %v", err)
|
||||
}
|
||||
|
||||
optRange, err := sd.wOptsTracker.ForPosition(0) // Text options for the current byte.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, c := range text {
|
||||
disp := sixteen.New()
|
||||
if err := disp.SetCharacter(c); err != nil {
|
||||
return fmt.Errorf("disp.SetCharacter => %v", err)
|
||||
}
|
||||
|
||||
ar := image.Rect(
|
||||
aligned.Min.X+segAr.Dx()*i, aligned.Min.Y,
|
||||
aligned.Min.X+segAr.Dx()*(i+1), aligned.Max.Y,
|
||||
)
|
||||
|
||||
dCvs, err := canvas.New(ar)
|
||||
if err != nil {
|
||||
return fmt.Errorf("canvas.New => %v", err)
|
||||
}
|
||||
|
||||
if i >= optRange.High { // Get the next write options.
|
||||
or, err := sd.wOptsTracker.ForPosition(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
optRange = or
|
||||
}
|
||||
wOpts := sd.givenWOpts[optRange.AttrIdx]
|
||||
|
||||
if err := disp.Draw(dCvs, sixteen.CellOpts(wOpts.cellOpts...)); err != nil {
|
||||
return fmt.Errorf("disp.Draw => %v", err)
|
||||
}
|
||||
|
||||
if err := dCvs.CopyTo(cvs); err != nil {
|
||||
return fmt.Errorf("dCvs.CopyTo => %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keyboard input isn't supported on the SegmentDisplay widget.
|
||||
@ -102,8 +270,6 @@ func (*SegmentDisplay) Mouse(m *terminalapi.Mouse) error {
|
||||
func (sd *SegmentDisplay) Options() widgetapi.Options {
|
||||
return widgetapi.Options{
|
||||
// The smallest supported size of a display segment.
|
||||
//
|
||||
// TODO: Return size required based on the text length and options.
|
||||
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
||||
WantKeyboard: false,
|
||||
WantMouse: false,
|
||||
|
@ -19,13 +19,26 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/testcanvas"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen"
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen/testsixteen"
|
||||
"github.com/mum4k/termdash/terminal/faketerm"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
// mustDrawChar draws the provided character in the area of the canvas or panics.
|
||||
func mustDrawChar(cvs *canvas.Canvas, char rune, ar image.Rectangle, opts ...sixteen.Option) {
|
||||
d := sixteen.New()
|
||||
testsixteen.MustSetCharacter(d, char)
|
||||
c := testcanvas.MustNew(ar)
|
||||
testsixteen.MustDraw(d, c, opts...)
|
||||
testcanvas.MustCopyTo(c, cvs)
|
||||
}
|
||||
|
||||
func TestSegmentDisplay(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
@ -33,28 +46,452 @@ func TestSegmentDisplay(t *testing.T) {
|
||||
update func(*SegmentDisplay) error // update gets called before drawing of the widget.
|
||||
canvas image.Rectangle
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantNewErr bool
|
||||
wantUpdateErr bool // whether to expect an error on a call to the update function
|
||||
wantDrawErr bool
|
||||
}{}
|
||||
}{
|
||||
{
|
||||
desc: "fails on area too small for a segment",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols-1, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("1"))
|
||||
},
|
||||
wantDrawErr: true,
|
||||
},
|
||||
{
|
||||
desc: "write fails without chunks",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write()
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "write fails on unsupported characters when requested",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk(".", WriteErrOnUnsupported()))
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "draws empty without text",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, all fit exactly",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("123"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
|
||||
{'2', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows)},
|
||||
{'3', image.Rect(sixteen.MinCols*2, 0, sixteen.MinCols*3, sixteen.MinRows)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "write sanitizes text by default",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk(".1"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "write sanitizes text with option",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk(".1", WriteSanitize()))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment vertical middle by default",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("1"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "subsequent calls to write overwrite previous text",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
if err := sd.Write(NewChunk("123")); err != nil {
|
||||
return err
|
||||
}
|
||||
return sd.Write(NewChunk("1"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets cell options per text chunk",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(
|
||||
NewChunk("1", WriteCellOpts(
|
||||
cell.FgColor(cell.ColorRed),
|
||||
cell.BgColor(cell.ColorBlue),
|
||||
)),
|
||||
NewChunk("2", WriteCellOpts(
|
||||
cell.FgColor(cell.ColorGreen),
|
||||
cell.BgColor(cell.ColorYellow),
|
||||
)),
|
||||
)
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(
|
||||
cvs, '1',
|
||||
image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
sixteen.CellOpts(
|
||||
cell.FgColor(cell.ColorRed),
|
||||
cell.BgColor(cell.ColorBlue),
|
||||
),
|
||||
)
|
||||
mustDrawChar(
|
||||
cvs, '2',
|
||||
image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
sixteen.CellOpts(
|
||||
cell.FgColor(cell.ColorGreen),
|
||||
cell.BgColor(cell.ColorYellow),
|
||||
),
|
||||
)
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "reset resets the text content",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
if err := sd.Write(NewChunk("123")); err != nil {
|
||||
return err
|
||||
}
|
||||
sd.Reset()
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "reset resets provided cell options",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
if err := sd.Write(
|
||||
NewChunk("1", WriteCellOpts(
|
||||
cell.FgColor(cell.ColorRed),
|
||||
cell.BgColor(cell.ColorBlue),
|
||||
)),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
sd.Reset()
|
||||
return sd.Write(NewChunk("1"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment vertical middle with option",
|
||||
opts: []Option{
|
||||
AlignVertical(align.VerticalMiddle),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("1"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment vertical top with option",
|
||||
opts: []Option{
|
||||
AlignVertical(align.VerticalTop),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("1"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment vertical bottom with option",
|
||||
opts: []Option{
|
||||
AlignVertical(align.VerticalBottom),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("1"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 2, sixteen.MinCols, sixteen.MinRows+2))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment horizontal center by default",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("8"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '8', image.Rect(1, 0, sixteen.MinCols+1, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment horizontal center with option",
|
||||
opts: []Option{
|
||||
AlignHorizontal(align.HorizontalCenter),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("8"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '8', image.Rect(1, 0, sixteen.MinCols+1, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment horizontal left with option",
|
||||
opts: []Option{
|
||||
AlignHorizontal(align.HorizontalLeft),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("8"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '8', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment horizontal right with option",
|
||||
opts: []Option{
|
||||
AlignHorizontal(align.HorizontalRight),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("8"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '8', image.Rect(2, 0, sixteen.MinCols+2, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, not enough space, maximizes segment height with option",
|
||||
opts: []Option{
|
||||
MaximizeSegmentHeight(),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("123"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
|
||||
{'2', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, not enough space, maximizes displayed text by default and fits all",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("123"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 7, 6, 12)},
|
||||
{'2', image.Rect(6, 7, 12, 12)},
|
||||
{'3', image.Rect(12, 7, 18, 12)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, not enough space, maximizes displayed text but cannot fit all",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("1234"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 7, 6, 12)},
|
||||
{'2', image.Rect(6, 7, 12, 12)},
|
||||
{'3', image.Rect(12, 7, 18, 12)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, not enough space, maximizes displayed text with option",
|
||||
opts: []Option{
|
||||
MaximizeDisplayedText(),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(NewChunk("123"))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 7, 6, 12)},
|
||||
{'2', image.Rect(6, 7, 12, 12)},
|
||||
{'3', image.Rect(12, 7, 18, 12)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
d, err := New(tc.opts...)
|
||||
if (err != nil) != tc.wantNewErr {
|
||||
t.Errorf("New => unexpected error: %v, wantNewErr: %v", err, tc.wantNewErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sd := New(tc.opts...)
|
||||
c, err := canvas.New(tc.canvas)
|
||||
if err != nil {
|
||||
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if tc.update != nil {
|
||||
err = tc.update(d)
|
||||
err = tc.update(sd)
|
||||
if (err != nil) != tc.wantUpdateErr {
|
||||
t.Errorf("update => unexpected error: %v, wantUpdateErr: %v", err, tc.wantUpdateErr)
|
||||
|
||||
@ -64,7 +501,7 @@ func TestSegmentDisplay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Draw(c)
|
||||
err = sd.Draw(c)
|
||||
if (err != nil) != tc.wantDrawErr {
|
||||
t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
|
||||
}
|
||||
@ -96,32 +533,22 @@ func TestSegmentDisplay(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestKeyboard(t *testing.T) {
|
||||
d, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
if err := d.Keyboard(&terminalapi.Keyboard{}); err == nil {
|
||||
sd := New()
|
||||
if err := sd.Keyboard(&terminalapi.Keyboard{}); err == nil {
|
||||
t.Errorf("Keyboard => got nil err, wanted one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMouse(t *testing.T) {
|
||||
d, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
if err := d.Mouse(&terminalapi.Mouse{}); err == nil {
|
||||
sd := New()
|
||||
if err := sd.Mouse(&terminalapi.Mouse{}); err == nil {
|
||||
t.Errorf("Mouse => got nil err, wanted one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
d, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := d.Options()
|
||||
sd := New()
|
||||
got := sd.Options()
|
||||
want := widgetapi.Options{
|
||||
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
||||
WantKeyboard: false,
|
||||
|
59
widgets/segmentdisplay/write_options.go
Normal file
59
widgets/segmentdisplay/write_options.go
Normal file
@ -0,0 +1,59 @@
|
||||
package segmentdisplay
|
||||
|
||||
// write_options.go contains options used when writing content to the widget.
|
||||
|
||||
import "github.com/mum4k/termdash/cell"
|
||||
|
||||
// WriteOption is used to provide options to Write().
|
||||
type WriteOption interface {
|
||||
// set sets the provided option.
|
||||
set(*writeOptions)
|
||||
}
|
||||
|
||||
// writeOptions stores the provided options.
|
||||
type writeOptions struct {
|
||||
cellOpts []cell.Option
|
||||
errOnUnsupported bool
|
||||
}
|
||||
|
||||
// newWriteOptions returns new writeOptions instance.
|
||||
func newWriteOptions(wOpts ...WriteOption) *writeOptions {
|
||||
wo := &writeOptions{}
|
||||
for _, o := range wOpts {
|
||||
o.set(wo)
|
||||
}
|
||||
return wo
|
||||
}
|
||||
|
||||
// writeOption implements WriteOption.
|
||||
type writeOption func(*writeOptions)
|
||||
|
||||
// set implements WriteOption.set.
|
||||
func (wo writeOption) set(wOpts *writeOptions) {
|
||||
wo(wOpts)
|
||||
}
|
||||
|
||||
// WriteCellOpts sets options on the cells that contain the text.
|
||||
func WriteCellOpts(opts ...cell.Option) WriteOption {
|
||||
return writeOption(func(wOpts *writeOptions) {
|
||||
wOpts.cellOpts = opts
|
||||
})
|
||||
}
|
||||
|
||||
// WriteSanitize instructs Write to sanitize the text, replacing all characters
|
||||
// the display doesn't support with a space ' ' character.
|
||||
// This is the default behavior.
|
||||
func WriteSanitize(opts ...cell.Option) WriteOption {
|
||||
return writeOption(func(wOpts *writeOptions) {
|
||||
wOpts.errOnUnsupported = false
|
||||
})
|
||||
}
|
||||
|
||||
// WriteErrOnUnsupported instructs Write to return an error when the text
|
||||
// contains a character the display doesn't support.
|
||||
// The default behavior is to sanitize the text, see WriteSanitize().
|
||||
func WriteErrOnUnsupported(opts ...cell.Option) WriteOption {
|
||||
return writeOption(func(wOpts *writeOptions) {
|
||||
wOpts.errOnUnsupported = true
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user