# Developing a new widget ## The API A widget is an object that implements the **widgetapi.Widget** interface. Apart from implementing this interface, each widget exposes other methods that allow the callers to change its content. E.g. the **gauge** widget enables the callers to set the displayed percentage. ## Thread safety All widget implementations must be thread safe, since the infrastructure calls the widget's **Options** and **Draw()** method concurrently with the user of the widget setting the displayed values. ## Drawing the widget's content When the widget's **Draw()** method is called, the infrastructure provides the widget with a canvas to draw on. This canvas is always zero based (the first point is at image.Point{0, 0}) regardless of the actual position of the widget on the terminal. ## Scaling Each time the widget's **Draw()** method is called, the widget must determine the size of the received canvas and scale accordingly. The size of the terminal might have been changed since the last call to **Draw()**. Correctly scaling the drawn content on each call also enables the widgets to size correctly regardless of the size and position of the container they are placed in. ## Limits Widget's should utilize the **widgetapi.Options** to set limits on the provided 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. 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, meta *widgetapi.Meta) 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 terminal. It creates an in-memory canvas where widgets can draw. The **faketerm** package also exports the **faketerm.Diff** function which allows the comparison of two fake terminals giving a human readable output for unit tests. A typical unit test creates the expected fake terminal, executes the widget to get the actual fake terminal and compares the two: ```go func TestWidget(t *testing.T) { tests := []struct { desc string canvas image.Rectangle meta *widgetapi.Meta opts []Option want func(size image.Point) *faketerm.Terminal wantErr bool }{ { desc: "a test case", // The metadata widget receives when drawn. meta: &widgetapi.Meta{}, // canvas determines the size of the allocated canvas in the test case. canvas: image.Rect(0,0,10,10), // want creates the expected content on the fake terminal. want: func(size image.Point) *faketerm.Terminal { ft := faketerm.MustNew(size) c := testcanvas.MustNew(ft.Area()) // Utilize functions in the testdraw package to create the expected content. testcanvas.MustApply(c, ft) return ft }, }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { c, err := canvas.New(tc.canvas) if err != nil { t.Fatalf("canvas.New => unexpected error: %v", err) } widget := New() err = widget.Draw(c, tc.meta) if (err != nil) != tc.wantErr { t.Errorf("Draw => unexpected error: %v, wantErr: %v", err, tc.wantErr) } if err != nil { return } got, err := faketerm.New(c.Size()) if err != nil { t.Fatalf("faketerm.New => unexpected error: %v", err) } if err := c.Apply(got); err != nil { t.Fatalf("Apply => unexpected error: %v", err) } if diff := faketerm.Diff(tc.want(c.Size()), got); diff != "" { t.Errorf("Draw => %v", diff) } }) } } ``` ## Demo and recording for the widget Once the widget is completed, add a demo into a **demo** sub directory under the widget's package and record a GIF of the demo. Place the recorded GIF into the [README](http://github.com/mum4k/termdash).