Rest of the Donut widget files.

This commit is contained in:
Jakub Sobon 2019-01-21 17:02:23 -05:00
parent cd48efc885
commit 7f133bf611
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
5 changed files with 696 additions and 29 deletions

View File

@ -58,3 +58,10 @@ func MustBrailleLine(bc *braille.Canvas, start, end image.Point, opts ...draw.Br
panic(fmt.Sprintf("draw.BrailleLine => unexpected error: %v", err))
}
}
// MustBrailleCircle draws the braille circle or panics.
func MustBrailleCircle(bc *braille.Canvas, mid image.Point, radius int, opts ...draw.BrailleCircleOption) {
if err := draw.BrailleCircle(bc, mid, radius, opts...); err != nil {
panic(fmt.Sprintf("draw.BrailleCircle => unexpected error: %v", err))
}
}

View File

@ -22,8 +22,12 @@ import (
"image"
"sync"
runewidth "github.com/mattn/go-runewidth"
"github.com/mum4k/termdash/align"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/canvas/braille"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/numbers"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
@ -135,12 +139,92 @@ func (d *Donut) Percent(p int, opts ...Option) error {
return nil
}
// progressText returns the textual representation of the current progress.
func (d *Donut) progressText() string {
switch d.pt {
case progressTypePercent:
return fmt.Sprintf("%d%%", d.current)
case progressTypeAbsolute:
return fmt.Sprintf("%d/%d", d.current, d.total)
default:
return ""
}
}
// holeRadius calculates the radius of the "hole" in the donut.
// Returns zero if no hole should be drawn.
func (d *Donut) holeRadius(donutRadius int) int {
r := int(numbers.Round(float64(donutRadius) / 100 * float64(d.opts.donutHolePercent)))
if r < 2 { // Smallest possible circle radius.
return 0
}
return r
}
// drawText draws the text label showing the progress.
// The text is only drawn if the radius of the donut "hole" is large enough to
// accommodate it.
func (d *Donut) drawText(cvs *canvas.Canvas, mid image.Point, holeR int) error {
cells, first := availableCells(mid, holeR)
t := d.progressText()
needCells := runewidth.StringWidth(t)
if cells < needCells {
return nil
}
ar := image.Rect(first.X, first.Y, first.X+cells+2, first.Y+1)
start, err := align.Text(ar, t, align.HorizontalCenter, align.VerticalMiddle)
if err != nil {
return fmt.Errorf("align.Text => %v", err)
}
if err := draw.Text(cvs, t, start, draw.TextMaxX(start.X+needCells), draw.TextCellOpts(d.opts.textCellOpts...)); err != nil {
return fmt.Errorf("draw.Text => %v", err)
}
return nil
}
// Draw draws the Donut widget onto the canvas.
// Implements widgetapi.Widget.Draw.
func (d *Donut) Draw(cvs *canvas.Canvas) error {
d.mu.Lock()
defer d.mu.Unlock()
bc, err := braille.New(cvs.Area())
if err != nil {
return fmt.Errorf("braille.New => %v", err)
}
startA, endA := startEndAngles(d.current, d.total, d.opts.startAngle, d.opts.direction)
if startA == endA {
// No progress recorded, so nothing to do.
return nil
}
mid, r := midAndRadius(bc.Area())
if err := draw.BrailleCircle(bc, mid, r,
draw.BrailleCircleFilled(),
draw.BrailleCircleArcOnly(startA, endA),
draw.BrailleCircleCellOpts(d.opts.cellOpts...),
); err != nil {
return fmt.Errorf("failed to draw the outer circle: %v", err)
}
holeR := d.holeRadius(r)
if holeR != 0 {
if err := draw.BrailleCircle(bc, mid, holeR,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
); err != nil {
return fmt.Errorf("failed to draw the outer circle: %v", err)
}
}
if err := bc.CopyTo(cvs); err != nil {
return err
}
if !d.opts.hideTextProgress {
return d.drawText(cvs, mid, holeR)
}
return nil
}
@ -161,10 +245,8 @@ func (d *Donut) Options() widgetapi.Options {
// This is adjusted for the inequality of the braille canvas.
Ratio: image.Point{braille.RowMult, braille.ColMult},
// The smallest circle that "looks" like a circle on the canvas needs
// to have a radius of two. We need at least three columns and two rows
// of cells to display it.
MinimumSize: image.Point{3, 2},
// The smallest circle that "looks" like a circle on the canvas.
MinimumSize: image.Point{3, 3},
WantKeyboard: false,
WantMouse: false,
}

View File

@ -20,7 +20,13 @@ import (
"github.com/kylelemons/godebug/pretty"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/canvas/braille/testbraille"
"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/terminal/faketerm"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
@ -31,9 +37,99 @@ func TestDonut(t *testing.T) {
update func(*Donut) 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: "New fails on negative donut hole percent",
opts: []Option{
HolePercent(-1),
},
canvas: image.Rect(0, 0, 3, 3),
wantNewErr: true,
},
{
desc: "New fails on too large donut hole percent",
opts: []Option{
HolePercent(101),
},
canvas: image.Rect(0, 0, 3, 3),
wantNewErr: true,
},
{
desc: "New fails on too small start angle",
opts: []Option{
StartAngle(-1),
},
canvas: image.Rect(0, 0, 3, 3),
wantNewErr: true,
},
{
desc: "New fails on too large start angle",
opts: []Option{
StartAngle(360),
},
canvas: image.Rect(0, 0, 3, 3),
wantNewErr: true,
},
{
desc: "Percent fails on too small start angle",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Percent(100, StartAngle(-1))
},
wantUpdateErr: true,
},
{
desc: "Percent fails on negative percent",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Percent(-1)
},
wantUpdateErr: true,
},
{
desc: "Percent fails on value too large",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Percent(101)
},
wantUpdateErr: true,
},
{
desc: "Absolute fails on too small start angle",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Absolute(100, 100, StartAngle(-1))
},
wantUpdateErr: true,
},
{
desc: "Absolute fails on done to small",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Absolute(-1, 100)
},
wantUpdateErr: true,
},
{
desc: "Absolute fails on total to small",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Absolute(0, 0)
},
wantUpdateErr: true,
},
{
desc: "Absolute fails on done greater than total",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Absolute(2, 1)
},
wantUpdateErr: true,
},
{
desc: "draws empty for no data points",
canvas: image.Rect(0, 0, 1, 1),
@ -41,13 +137,455 @@ func TestDonut(t *testing.T) {
return faketerm.MustNew(size)
},
},
{
desc: "fails when canvas too small to draw a circle",
update: func(d *Donut) error {
return d.Percent(100)
},
canvas: image.Rect(0, 0, 1, 1),
wantDrawErr: true,
},
{
desc: "smallest valid donut, 100% progress",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Percent(100)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
bc := testbraille.MustNew(ft.Area())
testdraw.MustBrailleCircle(bc, image.Point{2, 5}, 2, draw.BrailleCircleFilled())
testbraille.MustApply(bc, ft)
return ft
},
},
{
desc: "New sets donut options",
opts: []Option{
CellOpts(
cell.FgColor(cell.ColorRed),
cell.BgColor(cell.ColorBlue),
),
},
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Percent(100)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
bc := testbraille.MustNew(ft.Area())
testdraw.MustBrailleCircle(bc, image.Point{2, 5}, 2,
draw.BrailleCircleFilled(),
draw.BrailleCircleCellOpts(
cell.FgColor(cell.ColorRed),
cell.BgColor(cell.ColorBlue),
),
)
testbraille.MustApply(bc, ft)
return ft
},
},
{
desc: "Percent sets donut options",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Percent(100,
CellOpts(
cell.FgColor(cell.ColorRed),
cell.BgColor(cell.ColorBlue),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
bc := testbraille.MustNew(ft.Area())
testdraw.MustBrailleCircle(bc, image.Point{2, 5}, 2,
draw.BrailleCircleFilled(),
draw.BrailleCircleCellOpts(
cell.FgColor(cell.ColorRed),
cell.BgColor(cell.ColorBlue),
),
)
testbraille.MustApply(bc, ft)
return ft
},
},
{
desc: "Absolute sets donut options",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Absolute(100, 100,
CellOpts(
cell.FgColor(cell.ColorRed),
cell.BgColor(cell.ColorBlue),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
bc := testbraille.MustNew(ft.Area())
testdraw.MustBrailleCircle(bc, image.Point{2, 5}, 2,
draw.BrailleCircleFilled(),
draw.BrailleCircleCellOpts(
cell.FgColor(cell.ColorRed),
cell.BgColor(cell.ColorBlue),
),
)
testbraille.MustApply(bc, ft)
return ft
},
},
{
desc: "smallest valid donut, 100 absolute",
canvas: image.Rect(0, 0, 3, 3),
update: func(d *Donut) error {
return d.Absolute(100, 100)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
bc := testbraille.MustNew(ft.Area())
testdraw.MustBrailleCircle(bc, image.Point{2, 5}, 2, draw.BrailleCircleFilled())
testbraille.MustApply(bc, ft)
return ft
},
},
{
desc: "smallest valid donut with a hole",
canvas: image.Rect(0, 0, 6, 6),
update: func(d *Donut) error {
return d.Percent(100)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
bc := testbraille.MustNew(ft.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 2,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustApply(bc, ft)
return ft
},
},
{
desc: "draws a larger hole",
canvas: image.Rect(0, 0, 6, 6),
update: func(d *Donut) error {
return d.Percent(100, HolePercent(50))
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
bc := testbraille.MustNew(ft.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 3,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustApply(bc, ft)
return ft
},
},
{
desc: "hole as large as donut",
canvas: image.Rect(0, 0, 6, 6),
update: func(d *Donut) error {
return d.Percent(100, HolePercent(100), HideTextProgress())
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
bc := testbraille.MustNew(ft.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustApply(bc, ft)
return ft
},
},
{
desc: "displays 100% progress",
canvas: image.Rect(0, 0, 7, 7),
update: func(d *Donut) error {
return d.Percent(100, HolePercent(80))
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testdraw.MustText(c, "100%", image.Point{2, 3})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "sets text cell options",
canvas: image.Rect(0, 0, 7, 7),
update: func(d *Donut) error {
return d.Percent(100, HolePercent(80), TextCellOpts(
cell.FgColor(cell.ColorGreen),
cell.BgColor(cell.ColorYellow),
))
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testdraw.MustText(c, "100%", image.Point{2, 3}, draw.TextCellOpts(
cell.FgColor(cell.ColorGreen),
cell.BgColor(cell.ColorYellow),
))
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "shows text again when hidden previously",
opts: []Option{
HideTextProgress(),
},
canvas: image.Rect(0, 0, 7, 7),
update: func(d *Donut) error {
return d.Percent(100, HolePercent(80), ShowTextProgress())
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testdraw.MustText(c, "100%", image.Point{2, 3})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "hides text when requested",
canvas: image.Rect(0, 0, 7, 7),
update: func(d *Donut) error {
return d.Percent(100, HolePercent(80), HideTextProgress())
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "hides text when hole is too small",
canvas: image.Rect(0, 0, 7, 7),
update: func(d *Donut) error {
return d.Percent(100, HolePercent(50))
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 3,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "displays 1% progress",
canvas: image.Rect(0, 0, 7, 7),
update: func(d *Donut) error {
return d.Percent(1, HolePercent(80))
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6,
draw.BrailleCircleFilled(),
draw.BrailleCircleArcOnly(89, 90),
)
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testdraw.MustText(c, "1%", image.Point{3, 3})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "displays 25% progress, clockwise",
canvas: image.Rect(0, 0, 7, 7),
update: func(d *Donut) error {
return d.Percent(25, HolePercent(80), Clockwise())
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6,
draw.BrailleCircleFilled(),
draw.BrailleCircleArcOnly(0, 90),
)
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testdraw.MustText(c, "25%", image.Point{2, 3})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "displays 25% progress, counter-clockwise",
canvas: image.Rect(0, 0, 7, 7),
update: func(d *Donut) error {
return d.Percent(25, HolePercent(80), CounterClockwise())
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6,
draw.BrailleCircleFilled(),
draw.BrailleCircleArcOnly(90, 180),
)
testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testdraw.MustText(c, "25%", image.Point{2, 3})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "displays 10/10 absolute progress",
canvas: image.Rect(0, 0, 8, 8),
update: func(d *Donut) error {
return d.Absolute(10, 10, HolePercent(80))
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{8, 17}, 7, draw.BrailleCircleFilled())
testdraw.MustBrailleCircle(bc, image.Point{8, 17}, 6,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testdraw.MustText(c, "10/10", image.Point{2, 4})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "displays 1/10 absolute progress",
canvas: image.Rect(0, 0, 8, 8),
update: func(d *Donut) error {
return d.Absolute(1, 10, HolePercent(80))
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
bc := testbraille.MustNew(c.Area())
testdraw.MustBrailleCircle(bc, image.Point{8, 17}, 7,
draw.BrailleCircleFilled(),
draw.BrailleCircleArcOnly(54, 90),
)
testdraw.MustBrailleCircle(bc, image.Point{8, 17}, 6,
draw.BrailleCircleFilled(),
draw.BrailleCircleClearPixels(),
)
testbraille.MustCopyTo(bc, c)
testdraw.MustText(c, "1/10", image.Point{2, 4})
testcanvas.MustApply(c, 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 {
t.Fatalf("New => unexpected error: %v", err)
return
}
c, err := canvas.New(tc.canvas)
@ -97,6 +635,26 @@ func TestDonut(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 {
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 {
t.Errorf("Mouse => got nil err, wanted one")
}
}
func TestOptions(t *testing.T) {
d, err := New()
if err != nil {
@ -106,7 +664,7 @@ func TestOptions(t *testing.T) {
got := d.Options()
want := widgetapi.Options{
Ratio: image.Point{4, 2},
MinimumSize: image.Point{3, 2},
MinimumSize: image.Point{3, 3},
WantKeyboard: false,
WantMouse: false,
}

View File

@ -21,6 +21,7 @@ import (
"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"
@ -41,6 +42,7 @@ const (
func playDonut(ctx context.Context, d *donut.Donut, step int, delay time.Duration, pt playType) {
progress := 0
mult := 1
start := 0
ticker := time.NewTicker(delay)
defer ticker.Stop()
@ -49,20 +51,26 @@ func playDonut(ctx context.Context, d *donut.Donut, step int, delay time.Duratio
case <-ticker.C:
switch pt {
case playTypePercent:
if err := d.Percent(progress); err != nil {
if err := d.Percent(progress, donut.StartAngle(start)); err != nil {
panic(err)
}
case playTypeAbsolute:
if err := d.Absolute(progress, 100); err != nil {
if err := d.Absolute(progress, 100, donut.StartAngle(start)); err != nil {
panic(err)
}
}
//progress = 20
//continue
progress += step * mult
if progress > 100 || 100-progress < step {
progress = 100
} else if progress < 0 || progress < step {
progress = 0
start += 10
if start >= 360 {
start = 0
}
}
if progress == 100 {
@ -85,11 +93,19 @@ func main() {
defer t.Close()
ctx, cancel := context.WithCancel(context.Background())
d, err := donut.New()
d, err := donut.New(
donut.HolePercent(35),
donut.CellOpts(
cell.FgColor(cell.ColorRed),
),
donut.TextCellOpts(
cell.FgColor(cell.ColorBlue),
),
)
if err != nil {
panic(err)
}
go playDonut(ctx, d, 10, 500*time.Millisecond, playTypePercent)
go playDonut(ctx, d, 1, 25*time.Millisecond, playTypePercent)
c, err := container.New(
t,
@ -107,7 +123,7 @@ func main() {
}
}
if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter)); err != nil {
if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(25*time.Millisecond)); err != nil {
panic(err)
}
}

View File

@ -41,13 +41,13 @@ type options struct {
donutHolePercent int
hideTextProgress bool
textCellOpts []cell.Option
donutCellOpts []cell.Option
textCellOpts []cell.Option
cellOpts []cell.Option
// The angle in degrees that represents 0 and 100% of the progress.
startAngle int
// The direction in which the donut completes as progress increases.
// Positive for clockwise, negative for counter-clockwise.
// Positive for counter-clockwise, negative for clockwise.
direction int
}
@ -57,8 +57,8 @@ func (o *options) validate() error {
return fmt.Errorf("invalid donut hole percent %d, must be in range %d <= p <= %d", o.donutHolePercent, min, max)
}
if min, max := 0, 360; o.startAngle < min || o.startAngle > max {
return fmt.Errorf("invalid start angle %d, must be in range %d <= angle <= %d", o.startAngle, min, max)
if min, max := 0, 360; o.startAngle < min || o.startAngle >= max {
return fmt.Errorf("invalid start angle %d, must be in range %d <= angle < %d", o.startAngle, min, max)
}
return nil
@ -67,21 +67,25 @@ func (o *options) validate() error {
// newOptions returns options with the default values set.
func newOptions() *options {
return &options{
donutHolePercent: DefaultDonutHolePercent,
donutHolePercent: DefaultHolePercent,
startAngle: DefaultStartAngle,
direction: 1,
direction: -1,
textCellOpts: []cell.Option{
cell.FgColor(cell.ColorDefault),
cell.BgColor(cell.ColorDefault),
},
}
}
// DefaultDonutHolePercent is the default value for the DonutHolePercent
// DefaultHolePercent is the default value for the HolePercent
// option.
const DefaultDonutHolePercent = 20
const DefaultHolePercent = 35
// DonutHolePercent sets the size of the "hole" inside the donut as a
// HolePercent sets the size of the "hole" inside the donut as a
// percentage of the donut's radius.
// Setting this to zero disables the hole so that the donut will become just a
// circle. Valid range is 0 <= p <= 100.
func DonutHolePercent(p int) Option {
func HolePercent(p int) Option {
return option(func(opts *options) {
opts.donutHolePercent = p
})
@ -96,7 +100,7 @@ func DonutHolePercent(p int) Option {
// The progress is only displayed if there is enough space for it in the middle
// of the drawn donut.
//
// Providing this option also sets DonutHolePercent to its default value.
// Providing this option also sets HolePercent to its default value.
func ShowTextProgress() Option {
return option(func(opts *options) {
opts.hideTextProgress = false
@ -118,10 +122,10 @@ func TextCellOpts(cOpts ...cell.Option) Option {
})
}
// DonutCellOpts sets cell options on cells that contain the donut.
func DonutCellOpts(cOpts ...cell.Option) Option {
// CellOpts sets cell options on cells that contain the donut.
func CellOpts(cOpts ...cell.Option) Option {
return option(func(opts *options) {
opts.donutCellOpts = cOpts
opts.cellOpts = cOpts
})
}
@ -130,7 +134,7 @@ const DefaultStartAngle = 90
// StartAngle sets the starting angle in degrees, i.e. the point that will
// represent both 0% and 100% of progress.
// Valid values are in range 0 <= angle <= 360.
// Valid values are in range 0 <= angle < 360.
// Angles start at the X axis and grow counter-clockwise.
func StartAngle(angle int) Option {
return option(func(opts *options) {
@ -142,7 +146,7 @@ func StartAngle(angle int) Option {
// direction. This is the default option.
func Clockwise() Option {
return option(func(opts *options) {
opts.direction = 1
opts.direction = -1
})
}
@ -150,6 +154,6 @@ func Clockwise() Option {
// direction.
func CounterClockwise() Option {
return option(func(opts *options) {
opts.direction = -1
opts.direction = 1
})
}