mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-27 13:48:49 +08:00
Merge pull request #98 from mum4k/thread-safe-widgets
Thread safe widgets
This commit is contained in:
commit
c80cb19c07
@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- The LineChart now has an option to change the behavior of the Y axis from
|
||||
zero anchored to adaptive.
|
||||
- Lint errors reported on the Go report card.
|
||||
- Widgets now correctly handle a race when new user data are supplied between
|
||||
calls to their Options() and Draw() methods.
|
||||
|
||||
## [0.5.0] - 21-Jan-2019
|
||||
|
||||
|
@ -135,8 +135,7 @@ func drawResize(c *Container, area image.Rectangle) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := draw.Text(cvs, "⇄", image.Point{0, 0}); err != nil {
|
||||
if err := draw.ResizeNeeded(cvs); err != nil {
|
||||
return err
|
||||
}
|
||||
return cvs.Apply(c.term)
|
||||
|
@ -10,8 +10,8 @@ callers to set the displayed percentage.
|
||||
## Thread safety
|
||||
|
||||
All widget implementations must be thread safe, since the infrastructure calls
|
||||
the widget's **Draw()** method concurrently with the user of the widget setting
|
||||
the displayed values.
|
||||
the widget's **Options** and **Draw()** method concurrently with the user of
|
||||
the widget setting the displayed values.
|
||||
|
||||
## Drawing the widget's content
|
||||
|
||||
@ -38,12 +38,36 @@ canvas in order to handle under sized or over sized terminals gracefully.
|
||||
If the current size of the terminal and the configured container splits result
|
||||
in a canvas smaller than the **MinimumSize**, the infrastructure won't call the
|
||||
widget's **Draw()** method. The widgets can use this to prevent impossible
|
||||
scenarios where an error would have to be returned.
|
||||
scenarios where an error would have to be returned. Note that if the values
|
||||
returned on a call to the **Options** method aren't static, but depend on the
|
||||
user data provided to the widget, the widget **must** protect against the
|
||||
scenario where the infrastructure provides a canvas that doesn't match the
|
||||
returned options. This is because the infrastructure cannot guarantee the user
|
||||
won't change the data between calls to **Options** and **Draw**.
|
||||
|
||||
A widget can draw a character indicating that a resize is needed in such cases:
|
||||
|
||||
```go
|
||||
func (w *Widget) Draw(cvs *canvas.Canvas) error {
|
||||
min := w.minSize() // Output depends on the current state.
|
||||
needAr, err := area.FromSize(min)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !needAr.In(cvs.Area()) {
|
||||
return draw.ResizeNeeded(cvs)
|
||||
}
|
||||
|
||||
// Draw the widget.
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
If the container configuration results in a canvas larger than **MaximumSize**
|
||||
the canvas will be limited to the specified size. Widgets can either specify a
|
||||
limit for both the maximum width and height or limit just one of them.
|
||||
|
||||
|
||||
## Unit tests
|
||||
|
||||
Unit tests utilize the **faketerm** package which is a fake implementation of a
|
||||
|
@ -65,3 +65,10 @@ func MustBrailleCircle(bc *braille.Canvas, mid image.Point, radius int, opts ...
|
||||
panic(fmt.Sprintf("draw.BrailleCircle => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// MustResizeNeeded draws the character or panics.
|
||||
func MustResizeNeeded(cvs *canvas.Canvas) {
|
||||
if err := draw.ResizeNeeded(cvs); err != nil {
|
||||
panic(fmt.Sprintf("draw.ResizeNeeded => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
@ -187,3 +187,9 @@ func Text(c *canvas.Canvas, text string, start image.Point, opts ...TextOption)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResizeNeeded draws an unicode character indicating that the canvas size is
|
||||
// too small to draw meaningful content.
|
||||
func ResizeNeeded(cvs *canvas.Canvas) error {
|
||||
return Text(cvs, "⇄", image.Point{0, 0})
|
||||
}
|
||||
|
@ -601,3 +601,56 @@ func TestText(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResizeNeeded(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
canvas image.Rectangle
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
}{
|
||||
{
|
||||
desc: "draws the resize needed character",
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testcanvas.MustSetCell(cvs, image.Point{0, 0}, '⇄')
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
cvs, err := canvas.New(tc.canvas)
|
||||
if err != nil {
|
||||
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := ResizeNeeded(cvs); err != nil {
|
||||
t.Fatalf("ResizeNeeded => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got, err := faketerm.New(cvs.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
if err := cvs.Apply(got); err != nil {
|
||||
t.Fatalf("Apply => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
want, err := faketerm.New(cvs.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
if tc.want != nil {
|
||||
want = tc.want(cvs.Size())
|
||||
}
|
||||
|
||||
if diff := faketerm.Diff(want, got); diff != "" {
|
||||
t.Errorf("ResizeNeeded => %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -83,5 +83,13 @@ type Widget interface {
|
||||
// This is how the widget indicates to the infrastructure whether it is
|
||||
// interested in keyboard or mouse shortcuts, what is its minimum canvas
|
||||
// size, etc.
|
||||
//
|
||||
// Most widgets will return statically compiled options (minimum and
|
||||
// maximum size, etc.). If the returned options depend on the runtime state
|
||||
// of the widget (e.g. the user data provided to the widget), the widget
|
||||
// must protect against a case where the infrastructure calls the Draw
|
||||
// method with a canvas that doesn't meet the requested options. This is
|
||||
// because the data in the widget might change between calls to Options and
|
||||
// Draw.
|
||||
Options() Options
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/area"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
@ -68,6 +69,14 @@ func (bc *BarChart) Draw(cvs *canvas.Canvas) error {
|
||||
bc.mu.Lock()
|
||||
defer bc.mu.Unlock()
|
||||
|
||||
needAr, err := area.FromSize(bc.minSize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !needAr.In(cvs.Area()) {
|
||||
return draw.ResizeNeeded(cvs)
|
||||
}
|
||||
|
||||
for i, v := range bc.values {
|
||||
r, err := bc.barRect(cvs, i, v)
|
||||
if err != nil {
|
||||
|
@ -107,6 +107,24 @@ func TestGauge(t *testing.T) {
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "draws resize needed character when canvas is smaller than requested",
|
||||
bc: New(
|
||||
Char('o'),
|
||||
),
|
||||
update: func(bc *BarChart) error {
|
||||
return bc.Values([]int{0, 2, 5, 10}, 10)
|
||||
},
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustResizeNeeded(c)
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "displays bars",
|
||||
bc: New(
|
||||
|
@ -249,6 +249,14 @@ func (g *Gauge) Draw(cvs *canvas.Canvas) error {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
needAr, err := area.FromSize(g.minSize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !needAr.In(cvs.Area()) {
|
||||
return draw.ResizeNeeded(cvs)
|
||||
}
|
||||
|
||||
if g.hasBorder() {
|
||||
if err := draw.Border(cvs, cvs.Area(),
|
||||
draw.BorderLineStyle(g.opts.border),
|
||||
|
@ -74,6 +74,23 @@ func TestGauge(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws resize needed character when canvas is smaller than requested",
|
||||
gauge: New(
|
||||
Char('o'),
|
||||
Border(draw.LineStyleLight),
|
||||
),
|
||||
percent: &percentCall{p: 35},
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustResizeNeeded(c)
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns the progress text top and left",
|
||||
gauge: New(
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/area"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/braille"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
@ -173,6 +174,14 @@ func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
|
||||
needAr, err := area.FromSize(lc.minSize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !needAr.In(cvs.Area()) {
|
||||
return draw.ResizeNeeded(cvs)
|
||||
}
|
||||
|
||||
yd, err := lc.yAxis.Details(cvs.Area(), lc.opts.yAxisMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lc.yAxis.Details => %v", err)
|
||||
@ -285,19 +294,24 @@ func (lc *LineChart) Mouse(m *terminalapi.Mouse) error {
|
||||
return errors.New("the LineChart widget doesn't support mouse events")
|
||||
}
|
||||
|
||||
// Options implements widgetapi.Widget.Options.
|
||||
func (lc *LineChart) Options() widgetapi.Options {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
|
||||
// minSize determines the minimum required size to draw the line chart.
|
||||
func (lc *LineChart) minSize() image.Point {
|
||||
// At the very least we need:
|
||||
// - n cells width for the Y axis and its labels as reported by it.
|
||||
// - at least 1 cell width for the graph.
|
||||
reqWidth := lc.yAxis.RequiredWidth() + 1
|
||||
// - 2 cells height the X axis and its values and 2 for min and max labels on Y.
|
||||
const reqHeight = 4
|
||||
return image.Point{reqWidth, reqHeight}
|
||||
}
|
||||
|
||||
// Options implements widgetapi.Widget.Options.
|
||||
func (lc *LineChart) Options() widgetapi.Options {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
|
||||
return widgetapi.Options{
|
||||
MinimumSize: image.Point{reqWidth, reqHeight},
|
||||
MinimumSize: lc.minSize(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,14 +64,16 @@ func TestLineChartDraws(t *testing.T) {
|
||||
wantWriteErr: true,
|
||||
},
|
||||
{
|
||||
desc: "draw fails when canvas not wide enough",
|
||||
canvas: image.Rect(0, 0, 2, 4),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "draw fails when canvas not tall enough",
|
||||
canvas: image.Rect(0, 0, 3, 3),
|
||||
wantErr: true,
|
||||
desc: "draws resize needed character when canvas is smaller than requested",
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustResizeNeeded(c)
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "empty without series",
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/area"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
@ -62,6 +63,14 @@ func (sl *SparkLine) Draw(cvs *canvas.Canvas) error {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
needAr, err := area.FromSize(sl.minSize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !needAr.In(cvs.Area()) {
|
||||
return draw.ResizeNeeded(cvs)
|
||||
}
|
||||
|
||||
ar := sl.area(cvs)
|
||||
visible, max := visibleMax(sl.data, ar.Dx())
|
||||
var curX int
|
||||
|
@ -291,6 +291,24 @@ func TestSparkLine(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws resize needed character when canvas is smaller than requested",
|
||||
sparkLine: New(
|
||||
Height(2),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 100, 50, 85})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustResizeNeeded(c)
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "respects fixed height with label",
|
||||
sparkLine: New(
|
||||
|
Loading…
x
Reference in New Issue
Block a user