From 1bc6a7ccd4e7b70b477f94b2cbbdaad38d7a5d03 Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Sun, 24 Jun 2018 21:49:30 -0400 Subject: [PATCH] Skeleton for function that draws lines. - tests. - testdraw wrapper. - basic functionality that just draws lines, not crossings yet. --- draw/hv_line.go | 179 +++++++++++ draw/hv_line_test.go | 659 ++++++++++++++++++++++++++++++++++++++ draw/line_style.go | 36 ++- draw/testdraw/testdraw.go | 7 + 4 files changed, 880 insertions(+), 1 deletion(-) create mode 100644 draw/hv_line.go create mode 100644 draw/hv_line_test.go diff --git a/draw/hv_line.go b/draw/hv_line.go new file mode 100644 index 0000000..383f6ef --- /dev/null +++ b/draw/hv_line.go @@ -0,0 +1,179 @@ +package draw + +// hv_line.go contains code that draws horizontal and vertical lines. + +import ( + "fmt" + "image" + + "github.com/mum4k/termdash/canvas" + "github.com/mum4k/termdash/cell" +) + +// HVLineOption is used to provide options to HVLine(). +type HVLineOption interface { + // set sets the provided option. + set(*hVLineOptions) +} + +// hVLineOptions stores the provided options. +type hVLineOptions struct { + cellOpts []cell.Option + lineStyle LineStyle +} + +// hVLineOption implements HVLineOption. +type hVLineOption func(*hVLineOptions) + +// set implements HVLineOption.set. +func (o hVLineOption) set(opts *hVLineOptions) { + o(opts) +} + +// DefaultHVLineStyle is the default value for the HVLineStyle option. +const DefaultHVLineStyle = LineStyleLight + +// HVLineStyle sets the style of the line. +// Defaults to DefaultHVLineStyle. +func HVLineStyle(ls LineStyle) HVLineOption { + return hVLineOption(func(opts *hVLineOptions) { + opts.lineStyle = ls + }) +} + +// HVLineCellOpts sets options on the cells that contain the line. +// Where two lines cross, the cell representing the crossing point inherits +// options set on the line that was drawn last. +func HVLineCellOpts(cOpts ...cell.Option) HVLineOption { + return hVLineOption(func(opts *hVLineOptions) { + opts.cellOpts = cOpts + }) +} + +// HVLine represents one horizontal or vertical line. +type HVLine struct { + start image.Point + end image.Point +} + +// HVLines draws horizontal or vertical lines. Handles drawing of the correct +// characters for locations where any two lines cross (e.g. a corner, a T shape +// a cross). Each line must be at least one cell long. Both start and end +// must be on the same horizontal (same X coordinate) or same vertical (same Y +// coordinate) line. +func HVLines(c *canvas.Canvas, lines []HVLine, opts ...HVLineOption) error { + for _, l := range lines { + line, err := newHVLine(c, l.start, l.end, opts...) + if err != nil { + return err + } + + switch { + case line.horizontal(): + for curX := line.start.X; ; curX++ { + cur := image.Point{curX, line.start.Y} + if _, err := c.SetCell(cur, line.mainPart, line.opts.cellOpts...); err != nil { + return err + } + + if curX == line.end.X { + break + } + } + + case line.vertical(): + for curY := line.start.Y; ; curY++ { + cur := image.Point{line.start.X, curY} + if _, err := c.SetCell(cur, line.mainPart, line.opts.cellOpts...); err != nil { + return err + } + + if curY == line.end.Y { + break + } + } + } + } + return nil +} + +// hVLine represents a line that will be drawn on the canvas. +type hVLine struct { + // start is the starting point of the line. + start image.Point + + // end is the ending point of the line. + end image.Point + + // parts are characters that represent parts of the line of the style + // chosen in the options. + parts map[linePart]rune + + // mainPart is either parts[vLine] or parts[hLine] depending on whether + // this is horizontal or vertical line. + mainPart rune + + // opts are the options provided in a call to HVLine(). + opts *hVLineOptions +} + +// newHVLine creates a new hVLine instance. +// Swaps start and end iof necessary, so that horizontal drawing is always left +// to right and vertical is always top down. +func newHVLine(c *canvas.Canvas, start, end image.Point, opts ...HVLineOption) (*hVLine, error) { + if ar := c.Area(); !start.In(ar) || !end.In(ar) { + return nil, fmt.Errorf("both the start%v and the end%v must be in the canvas area: %v", start, end, ar) + } + + opt := &hVLineOptions{ + lineStyle: DefaultHVLineStyle, + } + for _, o := range opts { + o.set(opt) + } + + parts, err := lineParts(opt.lineStyle) + if err != nil { + return nil, err + } + + var mainPart rune + switch { + case start.X != end.X && start.Y != end.Y: + return nil, fmt.Errorf("can only draw horizontal (same X coordinates) or vertical (same Y coordinates), got start:%v end:%v", start, end) + + case start.X == end.X && start.Y == end.Y: + return nil, fmt.Errorf("the line must at least one cell long, got start%v, end%v", start, end) + + case start.X == end.X: + mainPart = parts[vLine] + if start.Y > end.Y { + start, end = end, start + } + + case start.Y == end.Y: + mainPart = parts[hLine] + if start.X > end.X { + start, end = end, start + } + + } + + return &hVLine{ + start: start, + end: end, + parts: parts, + mainPart: mainPart, + opts: opt, + }, nil +} + +// horizontal determines if this is a horizontal line. +func (hvl *hVLine) horizontal() bool { + return hvl.mainPart == hvl.parts[hLine] +} + +// vertical determines if this is a vertical line. +func (hvl *hVLine) vertical() bool { + return hvl.mainPart == hvl.parts[vLine] +} diff --git a/draw/hv_line_test.go b/draw/hv_line_test.go new file mode 100644 index 0000000..7a0c2fb --- /dev/null +++ b/draw/hv_line_test.go @@ -0,0 +1,659 @@ +package draw + +import ( + "image" + "testing" + + "github.com/mum4k/termdash/canvas" + "github.com/mum4k/termdash/canvas/testcanvas" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/terminal/faketerm" +) + +func TestHVLines(t *testing.T) { + tests := []struct { + desc string + canvas image.Rectangle // Size of the canvas for the test. + lines []HVLine + opts []HVLineOption + want func(size image.Point) *faketerm.Terminal + wantErr bool + }{ + { + desc: "fails when line isn't horizontal or vertical", + canvas: image.Rect(0, 0, 1, 1), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{1, 1}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + return faketerm.MustNew(size) + }, + wantErr: true, + }, + { + desc: "fails when start isn't in the canvas", + canvas: image.Rect(0, 0, 1, 1), + lines: []HVLine{ + { + start: image.Point{2, 0}, + end: image.Point{0, 0}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + return faketerm.MustNew(size) + }, + wantErr: true, + }, + { + desc: "fails when end isn't in the canvas", + canvas: image.Rect(0, 0, 1, 1), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{0, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + return faketerm.MustNew(size) + }, + wantErr: true, + }, + { + desc: "fails when the line has zero length", + canvas: image.Rect(0, 0, 1, 1), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{0, 0}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + return faketerm.MustNew(size) + }, + wantErr: true, + }, + { + desc: "draws single horizontal line", + canvas: image.Rect(0, 0, 3, 1), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{2, 0}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "respects line style set explicitly", + canvas: image.Rect(0, 0, 3, 1), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{2, 0}, + }, + }, + opts: []HVLineOption{ + HVLineStyle(LineStyleLight), + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "respects cell options", + canvas: image.Rect(0, 0, 3, 1), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{2, 0}, + }, + }, + opts: []HVLineOption{ + HVLineCellOpts( + cell.FgColor(cell.ColorYellow), + cell.BgColor(cell.ColorBlue), + ), + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine], + cell.FgColor(cell.ColorYellow), + cell.BgColor(cell.ColorBlue), + ) + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine], + cell.FgColor(cell.ColorYellow), + cell.BgColor(cell.ColorBlue), + ) + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine], + cell.FgColor(cell.ColorYellow), + cell.BgColor(cell.ColorBlue), + ) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws single horizontal line, supplied in reverse direction", + canvas: image.Rect(0, 0, 3, 1), + lines: []HVLine{ + { + start: image.Point{1, 0}, + end: image.Point{0, 0}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws single vertical line", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{1, 0}, + end: image.Point{1, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[vLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws single vertical line, supplied in reverse direction", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{1, 1}, + end: image.Point{1, 0}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "parallel horizontal lines don't affect each other", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{2, 0}, + }, + { + start: image.Point{0, 1}, + end: image.Point{2, 1}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine]) + + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "parallel vertical lines don't affect each other", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{0, 2}, + }, + { + start: image.Point{1, 0}, + end: image.Point{1, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[vLine]) + + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[vLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "perpendicular lines that don't cross don't affect each other", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{0, 2}, + }, + { + start: image.Point{1, 1}, + end: image.Point{2, 1}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[vLine]) + + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws top left corner", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{0, 2}, + }, + { + start: image.Point{0, 0}, + end: image.Point{2, 0}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[topLeftCorner]) + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[vLine]) + + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws top right corner", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{2, 0}, + end: image.Point{2, 2}, + }, + { + start: image.Point{0, 0}, + end: image.Point{2, 0}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[topRightCorner]) + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[vLine]) + + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws bottom left corner", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{0, 2}, + }, + { + start: image.Point{0, 2}, + end: image.Point{2, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[bottomLeftCorner]) + + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws bottom right corner", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{2, 0}, + end: image.Point{2, 2}, + }, + { + start: image.Point{0, 2}, + end: image.Point{2, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[bottomRightCorner]) + + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[hLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws T horizontal and up", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 2}, + end: image.Point{2, 2}, + }, + { + start: image.Point{1, 0}, + end: image.Point{1, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[hAndUp]) + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[hLine]) + + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws T horizontal and down", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 0}, + end: image.Point{2, 0}, + }, + { + start: image.Point{1, 0}, + end: image.Point{1, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hAndDown]) + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine]) + + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[vLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws T vertical and left", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 1}, + end: image.Point{2, 1}, + }, + { + start: image.Point{2, 0}, + end: image.Point{2, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[vAndLeft]) + + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[vLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws T vertical and right", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 1}, + end: image.Point{2, 1}, + }, + { + start: image.Point{0, 0}, + end: image.Point{0, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vAndRight]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[hLine]) + + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[vLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws a cross", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + { + start: image.Point{0, 1}, + end: image.Point{2, 1}, + }, + { + start: image.Point{1, 0}, + end: image.Point{1, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[hLine]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vAndH]) + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[hLine]) + + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine]) + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[vLine]) + + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws multiple crossings", + canvas: image.Rect(0, 0, 3, 3), + lines: []HVLine{ + // Three horizontal lines. + { + start: image.Point{0, 0}, + end: image.Point{2, 0}, + }, + { + start: image.Point{0, 1}, + end: image.Point{2, 1}, + }, + { + start: image.Point{0, 2}, + end: image.Point{2, 2}, + }, + // Three vertical lines. + { + start: image.Point{0, 0}, + end: image.Point{0, 2}, + }, + { + start: image.Point{1, 0}, + end: image.Point{1, 2}, + }, + { + start: image.Point{2, 0}, + end: image.Point{2, 2}, + }, + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + + parts := lineStyleChars[LineStyleLight] + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[topLeftCorner]) + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hAndDown]) + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[topRightCorner]) + + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vAndRight]) + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vAndH]) + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[vAndLeft]) + + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[bottomLeftCorner]) + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[hAndUp]) + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[bottomRightCorner]) + + 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) + } + + err = HVLines(c, tc.lines, tc.opts...) + if (err != nil) != tc.wantErr { + t.Errorf("HVLines => 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("HVLines => %v", diff) + } + }) + } +} diff --git a/draw/line_style.go b/draw/line_style.go index f8dfa48..66c811d 100644 --- a/draw/line_style.go +++ b/draw/line_style.go @@ -14,12 +14,17 @@ package draw -import "fmt" +import ( + "fmt" + + runewidth "github.com/mattn/go-runewidth" +) // line_style.go contains the Unicode characters used for drawing lines of // different styles. // lineStyleChars maps the line styles to the corresponding component characters. +// Source: http://en.wikipedia.org/wiki/Box-drawing_character. var lineStyleChars = map[LineStyle]map[linePart]rune{ LineStyleLight: { hLine: '─', @@ -28,9 +33,28 @@ var lineStyleChars = map[LineStyle]map[linePart]rune{ topRightCorner: '┐', bottomLeftCorner: '└', bottomRightCorner: '┘', + hAndUp: '┴', + hAndDown: '┬', + vAndLeft: '┤', + vAndRight: '├', + vAndH: '┼', }, } +// init verifies that all line parts are half-width runes (occupy only one +// cell). +func init() { + for ls, parts := range lineStyleChars { + for part, r := range parts { + if got := runewidth.RuneWidth(r); got > 1 { + panic(fmt.Errorf("line style %v line part %v is a rune %c with width %v, all parts must be half-width runes (width of one)", ls, part, r, got)) + } + } + } +} + +// TODO(mum4k): Check inside init() that all of these are half-width runes. + // lineParts returns the line component characters for the provided line style. func lineParts(ls LineStyle) (map[linePart]rune, error) { parts, ok := lineStyleChars[ls] @@ -80,6 +104,11 @@ var linePartNames = map[linePart]string{ topRightCorner: "linePartTopRightCorner", bottomLeftCorner: "linePartBottomLeftCorner", bottomRightCorner: "linePartBottomRightCorner", + hAndUp: "linePartHAndUp", + hAndDown: "linePartHAndDown", + vAndLeft: "linePartVAndLeft", + vAndRight: "linePartVAndRight", + vAndH: "linePartVAndH", } const ( @@ -89,4 +118,9 @@ const ( topRightCorner bottomLeftCorner bottomRightCorner + hAndUp + hAndDown + vAndLeft + vAndRight + vAndH ) diff --git a/draw/testdraw/testdraw.go b/draw/testdraw/testdraw.go index b47d490..674c20a 100644 --- a/draw/testdraw/testdraw.go +++ b/draw/testdraw/testdraw.go @@ -43,3 +43,10 @@ func MustRectangle(c *canvas.Canvas, r image.Rectangle, opts ...draw.RectangleOp panic(fmt.Sprintf("draw.Rectangle => unexpected error: %v", err)) } } + +// MustHVLines draws the vertical / horizontal lines or panics. +func MustHVLines(c *canvas.Canvas, lines []draw.HVLine, opts ...draw.HVLineOption) { + if err := draw.HVLines(c, lines, opts...); err != nil { + panic(fmt.Sprintf("draw.HVLines => unexpected error: %v", err)) + } +}