diff --git a/draw/testdraw/testdraw.go b/draw/testdraw/testdraw.go index 4f057e7..a83f7d4 100644 --- a/draw/testdraw/testdraw.go +++ b/draw/testdraw/testdraw.go @@ -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)) + } +} diff --git a/widgets/donut/donut.go b/widgets/donut/donut.go index 1b27544..ef3a11a 100644 --- a/widgets/donut/donut.go +++ b/widgets/donut/donut.go @@ -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, } diff --git a/widgets/donut/donut_test.go b/widgets/donut/donut_test.go index 10b1c1c..eafcc82 100644 --- a/widgets/donut/donut_test.go +++ b/widgets/donut/donut_test.go @@ -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, } diff --git a/widgets/donut/donutdemo/donutdemo.go b/widgets/donut/donutdemo/donutdemo.go index 1d5636f..cb5ad32 100644 --- a/widgets/donut/donutdemo/donutdemo.go +++ b/widgets/donut/donutdemo/donutdemo.go @@ -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) } } diff --git a/widgets/donut/options.go b/widgets/donut/options.go index 20e192e..b09b8ab 100644 --- a/widgets/donut/options.go +++ b/widgets/donut/options.go @@ -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 }) }