mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
Merge pull request #194 from mum4k/donut-label
Support a text label under the Donut widget.
This commit is contained in:
commit
9a82474aee
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- The `TextInput` widget, an input field allowing interactive text input.
|
||||
- The `Donut` widget can now display an optional text label under the donut.
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -101,6 +101,29 @@ func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right i
|
||||
return left, right, nil
|
||||
}
|
||||
|
||||
// HSplitCells returns two new areas created by splitting the provided area
|
||||
// after the specified amount of cells of its height. The number of cells must
|
||||
// be a zero or a positive integer. Providing a zero returns top=image.ZR,
|
||||
// bottom=area. Providing a number equal or larger to area's height returns
|
||||
// top=area, bottom=image.ZR.
|
||||
func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
if min := 0; cells < min {
|
||||
return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
|
||||
}
|
||||
if cells == 0 {
|
||||
return image.ZR, area, nil
|
||||
}
|
||||
|
||||
height := area.Dy()
|
||||
if cells >= height {
|
||||
return area, image.ZR, nil
|
||||
}
|
||||
|
||||
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+cells)
|
||||
bottom = image.Rect(area.Min.X, area.Min.Y+cells, area.Max.X, area.Max.Y)
|
||||
return top, bottom, nil
|
||||
}
|
||||
|
||||
// ExcludeBorder returns a new area created by subtracting a border around the
|
||||
// provided area. Return the zero area if there isn't enough space to exclude
|
||||
// the border.
|
||||
|
@ -367,6 +367,91 @@ func TestVSplitCells(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHSplitCells(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
area image.Rectangle
|
||||
cells int
|
||||
wantTop image.Rectangle
|
||||
wantBottom image.Rectangle
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails on negative cells",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: -1,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "returns area as top on cells too large",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 2,
|
||||
wantTop: image.Rect(1, 1, 2, 2),
|
||||
wantBottom: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "returns area as top on cells equal area width",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 1,
|
||||
wantTop: image.Rect(1, 1, 2, 2),
|
||||
wantBottom: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "returns area as bottom on zero cells",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 0,
|
||||
wantBottom: image.Rect(1, 1, 2, 2),
|
||||
wantTop: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "zero area to begin with",
|
||||
area: image.ZR,
|
||||
cells: 0,
|
||||
wantTop: image.ZR,
|
||||
wantBottom: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "splits area with even height",
|
||||
area: image.Rect(1, 1, 3, 3),
|
||||
cells: 1,
|
||||
wantTop: image.Rect(1, 1, 3, 2),
|
||||
wantBottom: image.Rect(1, 2, 3, 3),
|
||||
},
|
||||
{
|
||||
desc: "splits area with odd width",
|
||||
area: image.Rect(1, 1, 4, 4),
|
||||
cells: 1,
|
||||
wantTop: image.Rect(1, 1, 4, 2),
|
||||
wantBottom: image.Rect(1, 2, 4, 4),
|
||||
},
|
||||
{
|
||||
desc: "splits to unequal areas",
|
||||
area: image.Rect(0, 0, 4, 4),
|
||||
cells: 3,
|
||||
wantTop: image.Rect(0, 0, 4, 3),
|
||||
wantBottom: image.Rect(0, 3, 4, 4),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
gotTop, gotBottom, err := HSplitCells(tc.area, tc.cells)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("HSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantTop, gotTop); diff != "" {
|
||||
t.Errorf("HSplitCells => left value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantBottom, gotBottom); diff != "" {
|
||||
t.Errorf("HSplitCells => right value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExcludeBorder(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/internal/alignfor"
|
||||
"github.com/mum4k/termdash/internal/area"
|
||||
"github.com/mum4k/termdash/internal/canvas"
|
||||
"github.com/mum4k/termdash/internal/canvas/braille"
|
||||
"github.com/mum4k/termdash/internal/draw"
|
||||
@ -165,7 +166,9 @@ func (d *Donut) holeRadius(donutRadius int) int {
|
||||
// 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 {
|
||||
// The mid point addresses coordinates in pixels on a braille canvas.
|
||||
// The donutAr is the cell area for the donut itself.
|
||||
func (d *Donut) drawText(cvs *canvas.Canvas, donutAr image.Rectangle, mid image.Point, holeR int) error {
|
||||
cells, first := availableCells(mid, holeR)
|
||||
t := d.progressText()
|
||||
needCells := runewidth.StringWidth(t)
|
||||
@ -173,6 +176,13 @@ func (d *Donut) drawText(cvs *canvas.Canvas, mid image.Point, holeR int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if donutAr.Min.Y > 0 {
|
||||
// donutAr is what the braille canvas is created from, mid is relative
|
||||
// to it.
|
||||
// donutAr might have non-zero Y coordinate if we are displaying a text
|
||||
// label.
|
||||
first.Y += donutAr.Min.Y
|
||||
}
|
||||
ar := image.Rect(first.X, first.Y, first.X+cells+2, first.Y+1)
|
||||
start, err := alignfor.Text(ar, t, align.HorizontalCenter, align.VerticalMiddle)
|
||||
if err != nil {
|
||||
@ -184,23 +194,59 @@ func (d *Donut) drawText(cvs *canvas.Canvas, mid image.Point, holeR int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawLabel draws the text label in the area.
|
||||
func (d *Donut) drawLabel(cvs *canvas.Canvas, labelAr image.Rectangle) error {
|
||||
start, err := alignfor.Text(labelAr, d.opts.label, d.opts.labelAlign, align.VerticalMiddle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := draw.Text(
|
||||
cvs, d.opts.label, start,
|
||||
draw.TextOverrunMode(draw.OverrunModeThreeDot),
|
||||
draw.TextMaxX(labelAr.Max.X),
|
||||
draw.TextCellOpts(d.opts.labelCellOpts...),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Draw draws the Donut widget onto the canvas.
|
||||
// Implements widgetapi.Widget.Draw.
|
||||
func (d *Donut) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) 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
|
||||
}
|
||||
|
||||
var donutAr, labelAr image.Rectangle
|
||||
if len(d.opts.label) > 0 {
|
||||
d, l, err := donutAndLabel(cvs.Area())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
donutAr = d
|
||||
labelAr = l
|
||||
|
||||
} else {
|
||||
donutAr = cvs.Area()
|
||||
}
|
||||
|
||||
if donutAr.Dx() < minSize.X || donutAr.Dy() < minSize.Y {
|
||||
// Reserving area for the label might have resulted in donutAr being
|
||||
// too small.
|
||||
return draw.ResizeNeeded(cvs)
|
||||
}
|
||||
|
||||
bc, err := braille.New(donutAr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("braille.New => %v", err)
|
||||
}
|
||||
|
||||
mid, r := midAndRadius(bc.Area())
|
||||
if err := draw.BrailleCircle(bc, mid, r,
|
||||
draw.BrailleCircleFilled(),
|
||||
@ -224,7 +270,15 @@ func (d *Donut) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
|
||||
}
|
||||
|
||||
if !d.opts.hideTextProgress {
|
||||
return d.drawText(cvs, mid, holeR)
|
||||
if err := d.drawText(cvs, donutAr, mid, holeR); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !labelAr.Empty() {
|
||||
if err := d.drawLabel(cvs, labelAr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -239,6 +293,9 @@ func (*Donut) Mouse(m *terminalapi.Mouse) error {
|
||||
return errors.New("the Donut widget doesn't support mouse events")
|
||||
}
|
||||
|
||||
// minSize is the smallest area we can draw donut on.
|
||||
var minSize = image.Point{3, 3}
|
||||
|
||||
// Options implements widgetapi.Widget.Options.
|
||||
func (d *Donut) Options() widgetapi.Options {
|
||||
return widgetapi.Options{
|
||||
@ -247,8 +304,28 @@ func (d *Donut) Options() widgetapi.Options {
|
||||
Ratio: image.Point{braille.RowMult, braille.ColMult},
|
||||
|
||||
// The smallest circle that "looks" like a circle on the canvas.
|
||||
MinimumSize: image.Point{3, 3},
|
||||
MinimumSize: minSize,
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
}
|
||||
}
|
||||
|
||||
// donutAndLabel splits the canvas area into square area for the donut and an
|
||||
// area under the donut for the text label.
|
||||
func donutAndLabel(cvsAr image.Rectangle) (donAr, labelAr image.Rectangle, err error) {
|
||||
height := cvsAr.Dy()
|
||||
// One line for the text label at the bottom.
|
||||
top, labelAr, err := area.HSplitCells(cvsAr, height-1)
|
||||
if err != nil {
|
||||
return image.ZR, image.ZR, err
|
||||
}
|
||||
|
||||
// Remove one line from the top too so the donut area remains square.
|
||||
// When using braille, this effectively removes 4 pixels from both the top
|
||||
// and the bottom. See braille.RowMult.
|
||||
donAr, err = area.Shrink(top, 1, 0, 0, 0)
|
||||
if err != nil {
|
||||
return image.ZR, image.ZR, err
|
||||
}
|
||||
return donAr, labelAr, nil
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/internal/canvas"
|
||||
"github.com/mum4k/termdash/internal/canvas/braille/testbraille"
|
||||
@ -143,8 +144,14 @@ func TestDonut(t *testing.T) {
|
||||
update: func(d *Donut) error {
|
||||
return d.Percent(100)
|
||||
},
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
wantDrawErr: true,
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustResizeNeeded(cvs)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "smallest valid donut, 100% progress",
|
||||
@ -162,6 +169,23 @@ func TestDonut(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "adding label to the smallest canvas makes it too small",
|
||||
opts: []Option{
|
||||
Label("hi"),
|
||||
},
|
||||
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)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustResizeNeeded(cvs)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "New sets donut options",
|
||||
opts: []Option{
|
||||
@ -300,6 +324,33 @@ func TestDonut(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws hole and label",
|
||||
opts: []Option{
|
||||
Label("hi"),
|
||||
},
|
||||
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)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
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.MustCopyTo(bc, c)
|
||||
|
||||
testdraw.MustText(c, "hi", image.Point{2, 5})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "hole as large as donut",
|
||||
canvas: image.Rect(0, 0, 6, 6),
|
||||
@ -573,6 +624,197 @@ func TestDonut(t *testing.T) {
|
||||
|
||||
testdraw.MustText(c, "1/10", image.Point{2, 4})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "displays text label under the donut",
|
||||
opts: []Option{
|
||||
Label("hi"),
|
||||
},
|
||||
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})
|
||||
|
||||
testdraw.MustText(c, "hi", image.Point{2, 6})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns text label center with option",
|
||||
opts: []Option{
|
||||
Label("hi"),
|
||||
LabelAlign(align.HorizontalCenter),
|
||||
},
|
||||
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})
|
||||
|
||||
testdraw.MustText(c, "hi", image.Point{2, 6})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns text label left",
|
||||
opts: []Option{
|
||||
Label("hi"),
|
||||
LabelAlign(align.HorizontalLeft),
|
||||
},
|
||||
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})
|
||||
|
||||
testdraw.MustText(c, "hi", image.Point{0, 6})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns text label right",
|
||||
opts: []Option{
|
||||
Label("hi"),
|
||||
LabelAlign(align.HorizontalRight),
|
||||
},
|
||||
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})
|
||||
|
||||
testdraw.MustText(c, "hi", image.Point{5, 6})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets cell options on text label",
|
||||
opts: []Option{
|
||||
Label(
|
||||
"hi",
|
||||
cell.FgColor(cell.ColorRed),
|
||||
cell.BgColor(cell.ColorBlue),
|
||||
),
|
||||
},
|
||||
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})
|
||||
|
||||
testdraw.MustText(
|
||||
c,
|
||||
"hi",
|
||||
image.Point{2, 6},
|
||||
draw.TextCellOpts(
|
||||
cell.FgColor(cell.ColorRed),
|
||||
cell.BgColor(cell.ColorBlue),
|
||||
),
|
||||
)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "text label too long, gets trimmed",
|
||||
opts: []Option{
|
||||
Label(
|
||||
"hello world",
|
||||
),
|
||||
},
|
||||
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})
|
||||
|
||||
testdraw.MustText(c, "hello …", image.Point{0, 6})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
|
@ -86,7 +86,10 @@ func main() {
|
||||
defer t.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
green, err := donut.New(donut.CellOpts(cell.FgColor(cell.ColorGreen)))
|
||||
green, err := donut.New(
|
||||
donut.CellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
donut.Label("text label", cell.FgColor(cell.ColorGreen)),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package donut
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
)
|
||||
|
||||
@ -44,6 +45,10 @@ type options struct {
|
||||
textCellOpts []cell.Option
|
||||
cellOpts []cell.Option
|
||||
|
||||
labelCellOpts []cell.Option
|
||||
labelAlign align.Horizontal
|
||||
label string
|
||||
|
||||
// The angle in degrees that represents 0 and 100% of the progress.
|
||||
startAngle int
|
||||
// The direction in which the donut completes as progress increases.
|
||||
@ -74,6 +79,7 @@ func newOptions() *options {
|
||||
cell.FgColor(cell.ColorDefault),
|
||||
cell.BgColor(cell.ColorDefault),
|
||||
},
|
||||
labelAlign: DefaultLabelAlign,
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,3 +163,21 @@ func CounterClockwise() Option {
|
||||
opts.direction = 1
|
||||
})
|
||||
}
|
||||
|
||||
// Label sets a text label to be displayed under the donut.
|
||||
func Label(text string, cOpts ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.label = text
|
||||
opts.labelCellOpts = cOpts
|
||||
})
|
||||
}
|
||||
|
||||
// DefaultLabelAlign is the default value for the LabelAlign option.
|
||||
const DefaultLabelAlign = align.HorizontalCenter
|
||||
|
||||
// LabelAlign sets the alignment of the label under the donut.
|
||||
func LabelAlign(la align.Horizontal) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.labelAlign = la
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user