1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00

A function that draws a rectangle.

Also:
- implementing text trimming and wrapping.
- Switching log.Fatalf to panic() for more understandable test errors.
This commit is contained in:
Jakub Sobon 2018-05-06 19:28:52 +01:00
parent b42236a117
commit 2473cd46d6
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
9 changed files with 168 additions and 23 deletions

View File

@ -51,9 +51,15 @@ Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines related
to the Google's CLA, and code review requirements.
As stated above the primary goal of this project is to develop readable, well
designed code, the functionality and efficiency comes second. With that please
expect rather more detailed code review. Please read the [design
guidelines](doc/design_guidelines.md) before contributing.
designed code, the functionality and efficiency come second. This is achieved
through detailed code reviews, design discussions and following of the [design
guidelines](doc/design_guidelines.md). Please familiarize yourself with these
before contributing.
## Contributing widgets
If you're developing a new widget, please see the [widget
development](doc/widget_development.md) section.
# Project status

View File

@ -16,8 +16,8 @@
package testcanvas
import (
"fmt"
"image"
"log"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/cell"
@ -28,7 +28,7 @@ import (
func MustNew(area image.Rectangle) *canvas.Canvas {
cvs, err := canvas.New(area)
if err != nil {
log.Fatalf("canvas.New => unexpected error: %v", err)
panic(fmt.Sprintf("canvas.New => unexpected error: %v", err))
}
return cvs
}
@ -36,13 +36,13 @@ func MustNew(area image.Rectangle) *canvas.Canvas {
// MustApply applies the canvas on the terminal or panics.
func MustApply(c *canvas.Canvas, t *faketerm.Terminal) {
if err := c.Apply(t); err != nil {
log.Fatalf("canvas.Apply => unexpected error: %v", err)
panic(fmt.Sprintf("canvas.Apply => unexpected error: %v", err))
}
}
// MustSetCell sets the cell value or panics.
func MustSetCell(c *canvas.Canvas, p image.Point, r rune, opts ...cell.Option) {
if err := c.SetCell(p, r, opts...); err != nil {
log.Fatalf("canvas.SetCell => unexpected error: %v", err)
panic(fmt.Sprintf("canvas.SetCell => unexpected error: %v", err))
}
}

View File

@ -1,6 +1,21 @@
# Design guidelines
TODO(mum4k): Write the design guidelines, notes:
- low level draw functions in the draw package - each with unit tests and a
test utility.
## Don't clutter the widget code with drawing primitives
The widget implementations should contain high level code only. Low level
drawing primitives should be in separate packages. That way the widgets remain
easy to understand, enhance and test.
E.g. the **gauge** widget contains code that calculates the size of the
rectangle that needs to be drawn. It doesn't contain code that draws the
rectangle itself as that belongs into the **draw** package.
## Provide test helpers for all functions in the draw package
To simplify unit tests of widgets, a test helper should be provided to all
functions in the **draw** package.
E.g. a function called **Rectangle()** that draws a rectangle should come with
a helper caller **MustRectangle()**. Tests of a widget that uses
**Rectangle()** can just specify the expected rectangle by calling
**MustRectangle()** on the test canvas.

View File

@ -44,7 +44,7 @@ func boxChar(p image.Point, box image.Rectangle, parts map[linePart]rune) rune {
return -1
}
// Box draws a box on the canvas.
// Box draws a box (i.e. a border) on the canvas.
func Box(c *canvas.Canvas, box image.Rectangle, ls LineStyle, opts ...cell.Option) error {
if ar := c.Area(); !box.In(ar) {
return fmt.Errorf("the requested box %+v falls outside of the provided canvas %+v", box, ar)

View File

@ -16,8 +16,8 @@
package testdraw
import (
"fmt"
"image"
"log"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/cell"
@ -27,13 +27,13 @@ import (
// MustBox draws box on the canvas or panics.
func MustBox(c *canvas.Canvas, box image.Rectangle, ls draw.LineStyle, opts ...cell.Option) {
if err := draw.Box(c, box, ls, opts...); err != nil {
log.Fatalf("draw.Box => unexpected error: %v", err)
panic(fmt.Sprintf("draw.Box => unexpected error: %v", err))
}
}
// MustText draws the text on the canvas or panics.
func MustText(c *canvas.Canvas, text string, tb draw.TextBounds, opts ...cell.Option) {
if err := draw.Text(c, text, tb, opts...); err != nil {
log.Fatalf("draw.Text => unexpected error: %v", err)
panic(fmt.Sprintf("draw.Text => unexpected error: %v", err))
}
}

View File

@ -38,7 +38,9 @@ func (om OverrunMode) String() string {
// overrunModeNames maps OverrunMode values to human readable names.
var overrunModeNames = map[OverrunMode]string{
OverrunModeStrict: "OverrunModeStrict",
OverrunModeStrict: "OverrunModeStrict",
OverrunModeTrim: "OverrunModeTrim",
OverrunModeThreeDot: "OverrunModeThreeDot",
}
const (
@ -46,7 +48,12 @@ const (
// returns an error if it doesn't.
OverrunModeStrict OverrunMode = iota
// TODO(mum4k): Support other overrun modes, like Trim, ThreeDot or LineWrap.
// OverrunModeTrim trims the part of the text that doesn't fit.
OverrunModeTrim
// OverrunModeThreeDot trims the text and places the horizontal ellipsis
// '…' character at the end.
OverrunModeThreeDot
)
// TextBounds specifies the limits (start and end cells) that the text must
@ -67,6 +74,27 @@ type TextBounds struct {
Overrun OverrunMode
}
// bounds enforces the text bounds based on the specified overrun mode.
// Returns test that can be safely drawn within the bounds.
func bounds(text string, maxRunes int, om OverrunMode) (string, error) {
runes := utf8.RuneCountInString(text)
if runes <= maxRunes {
return text, nil
}
switch om {
case OverrunModeStrict:
return "", fmt.Errorf("the requested text %q takes %d runes to draw, space is available for only %d runes and overrun mode is %v", text, runes, maxRunes, om)
case OverrunModeTrim:
return text[:maxRunes], nil
case OverrunModeThreeDot:
return fmt.Sprintf("%s…", text[:maxRunes-1]), nil
default:
return "", fmt.Errorf("unsupported overrun mode %v", om)
}
}
// Text prints the provided text on the canvas.
func Text(c *canvas.Canvas, text string, tb TextBounds, opts ...cell.Option) error {
ar := c.Area()
@ -84,13 +112,15 @@ func Text(c *canvas.Canvas, text string, tb TextBounds, opts ...cell.Option) err
} else {
wantMaxX = tb.MaxX
}
runes := utf8.RuneCountInString(text)
if maxX := tb.Start.X + runes; maxX > wantMaxX && tb.Overrun == OverrunModeStrict {
return fmt.Errorf("the requested text %q would end at X coordinate %v which falls outside of the maximum %v", text, maxX, wantMaxX)
maxRunes := wantMaxX - tb.Start.X
trimmed, err := bounds(text, maxRunes, tb.Overrun)
if err != nil {
return err
}
cur := tb.Start
for _, r := range text {
for _, r := range trimmed {
if err := c.SetCell(cur, r, opts...); err != nil {
return err
}

View File

@ -45,6 +45,31 @@ func TestText(t *testing.T) {
},
wantErr: true,
},
{
desc: "unsupported overrun mode specified",
canvas: image.Rect(0, 0, 1, 1),
text: "ab",
tb: TextBounds{
Start: image.Point{0, 0},
Overrun: OverrunMode(-1),
},
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantErr: true,
},
{
desc: "zero text",
canvas: image.Rect(0, 0, 1, 1),
text: "",
tb: TextBounds{
Start: image.Point{0, 0},
Overrun: OverrunModeStrict,
},
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
},
{
desc: "text falls outside of the canvas on OverrunModeStrict",
canvas: image.Rect(0, 0, 1, 1),
@ -58,6 +83,76 @@ func TestText(t *testing.T) {
},
wantErr: true,
},
{
desc: "text falls outside of the canvas on OverrunModeTrim",
canvas: image.Rect(0, 0, 1, 1),
text: "ab",
tb: TextBounds{
Start: image.Point{0, 0},
Overrun: OverrunModeTrim,
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "OverrunModeTrim trims longer text",
canvas: image.Rect(0, 0, 2, 1),
text: "abcdef",
tb: TextBounds{
Start: image.Point{0, 0},
Overrun: OverrunModeTrim,
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
testcanvas.MustSetCell(c, image.Point{1, 0}, 'b')
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "text falls outside of the canvas on OverrunModeThreeDot",
canvas: image.Rect(0, 0, 1, 1),
text: "ab",
tb: TextBounds{
Start: image.Point{0, 0},
Overrun: OverrunModeThreeDot,
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testcanvas.MustSetCell(c, image.Point{0, 0}, '…')
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "OverrunModeThreeDot trims longer text",
canvas: image.Rect(0, 0, 2, 1),
text: "abcdef",
tb: TextBounds{
Start: image.Point{0, 0},
Overrun: OverrunModeThreeDot,
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testcanvas.MustSetCell(c, image.Point{0, 0}, 'a')
testcanvas.MustSetCell(c, image.Point{1, 0}, '…')
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "requested MaxX is negative",
canvas: image.Rect(0, 0, 1, 1),

View File

@ -85,7 +85,7 @@ func New(size image.Point, opts ...Option) (*Terminal, error) {
func MustNew(size image.Point, opts ...Option) *Terminal {
ft, err := New(size, opts...)
if err != nil {
log.Fatalf("New => unexpected error: %v", err)
panic(fmt.Sprintf("New => unexpected error: %v", err))
}
return ft
}

View File

@ -19,7 +19,6 @@ package fakewidget
import (
"fmt"
"image"
"log"
"sync"
"github.com/mum4k/termdash/area"
@ -183,6 +182,6 @@ func Draw(t terminalapi.Terminal, cvs *canvas.Canvas, opts widgetapi.Options, ev
// MustDraw is like Draw, but panics on all errors.
func MustDraw(t terminalapi.Terminal, cvs *canvas.Canvas, opts widgetapi.Options, events ...terminalapi.Event) {
if err := Draw(t, cvs, opts, events...); err != nil {
log.Fatalf("Draw => %v", err)
panic(fmt.Sprintf("Draw => %v", err))
}
}