From 6b592b7d34baabc5354459184ad5b0018bf044c4 Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Fri, 30 Mar 2018 01:41:22 +0300 Subject: [PATCH] Adding the draw library. And a function that draws boxes. --- container/options.go | 44 ++------ draw/box.go | 68 ++++++++++++ draw/box_test.go | 188 ++++++++++++++++++++++++++++++++++ draw/draw.go | 3 + draw/line_style.go | 77 ++++++++++++++ terminal/faketerm/faketerm.go | 20 ++++ 6 files changed, 364 insertions(+), 36 deletions(-) create mode 100644 draw/box.go create mode 100644 draw/box_test.go create mode 100644 draw/draw.go create mode 100644 draw/line_style.go diff --git a/container/options.go b/container/options.go index a477b04..0d526d1 100644 --- a/container/options.go +++ b/container/options.go @@ -2,7 +2,10 @@ package container // options.go defines container options. -import "github.com/mum4k/termdash/widget" +import ( + "github.com/mum4k/termdash/draw" + "github.com/mum4k/termdash/widget" +) // Option is used to provide options. type Option interface { @@ -25,7 +28,7 @@ type options struct { vAlign vAlignType // border is the border around the container. - border borderType + border draw.LineStyle } // option implements Option. @@ -113,19 +116,10 @@ func VerticalAlignBottom() Option { }) } -// BorderNone configures the container to have no border. -// This is the default if none of the Border options is specified. -func BorderNone() Option { +// Border configures the container to have a border of the specified style. +func Border(ls draw.LineStyle) Option { return option(func(opts *options) { - opts.border = borderTypeNone - }) -} - -// BorderSolid configures the container to have a border made with a solid -// line. -func BorderSolid() Option { - return option(func(opts *options) { - opts.border = borderTypeSolid + opts.border = ls }) } @@ -198,25 +192,3 @@ const ( vAlignTypeMiddle vAlignTypeBottom ) - -// borderType represents -type borderType int - -// String implements fmt.Stringer() -func (bt borderType) String() string { - if n, ok := borderTypeNames[bt]; ok { - return n - } - return "borderTypeUnknown" -} - -// borderTypeNames maps borderType values to human readable names. -var borderTypeNames = map[borderType]string{ - borderTypeNone: "borderTypeNone", - borderTypeSolid: "borderTypeSolid", -} - -const ( - borderTypeNone borderType = iota - borderTypeSolid -) diff --git a/draw/box.go b/draw/box.go new file mode 100644 index 0000000..0c9d56e --- /dev/null +++ b/draw/box.go @@ -0,0 +1,68 @@ +package draw + +// box.go contains code that draws boxes. + +import ( + "fmt" + "image" + + "github.com/mum4k/termdash/area" + "github.com/mum4k/termdash/canvas" + "github.com/mum4k/termdash/cell" +) + +// boxChar returns the correct box character from the parts for the use at the +// specified point of the box. Returns -1 if no character should be at this point. +func boxChar(p image.Point, box image.Rectangle, parts map[linePart]rune) rune { + switch { + case p.X == box.Min.X && p.Y == box.Min.Y: + return parts[topLeftCorner] + case p.X == box.Max.X-1 && p.Y == box.Min.Y: + return parts[topRightCorner] + case p.X == box.Min.X && p.Y == box.Max.Y-1: + return parts[bottomLeftCorner] + case p.X == box.Max.X-1 && p.Y == box.Max.Y-1: + return parts[bottomRightCorner] + case p.X == box.Min.X || p.X == box.Max.X-1: + return parts[vLine] + case p.Y == box.Min.Y || p.Y == box.Max.Y-1: + return parts[hLine] + } + return -1 +} + +// Box draws a box on the canvas. +func Box(c *canvas.Canvas, box image.Rectangle, ls LineStyle, opts ...cell.Option) error { + ar, err := area.FromSize(c.Size()) + if err != nil { + return err + } + if !box.In(ar) { + return fmt.Errorf("the requested box %+v falls outside of the provided canvas %+v", box, ar) + } + + const minSize = 2 + if box.Dx() < minSize || box.Dy() < minSize { + return fmt.Errorf("the smallest supported box is %dx%d, got: %dx%d", minSize, minSize, box.Dx(), box.Dy()) + } + + parts, err := lineParts(ls) + if err != nil { + return err + } + + for col := box.Min.X; col < box.Max.X; col++ { + for row := box.Min.Y; row < box.Max.Y; row++ { + p := image.Point{col, row} + r := boxChar(p, box, parts) + if r == -1 { + continue + } + + if err := c.SetCell(p, r, opts...); err != nil { + return err + } + } + } + return nil +} diff --git a/draw/box_test.go b/draw/box_test.go new file mode 100644 index 0000000..c5e550b --- /dev/null +++ b/draw/box_test.go @@ -0,0 +1,188 @@ +package draw + +import ( + "image" + "testing" + + "github.com/kylelemons/godebug/pretty" + "github.com/mum4k/termdash/canvas" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/terminal/faketerm" +) + +func TestBox(t *testing.T) { + tests := []struct { + desc string + canvas image.Rectangle + box image.Rectangle + ls LineStyle + opts []cell.Option + want cell.Buffer + wantErr bool + }{ + { + desc: "box is larger than canvas", + canvas: image.Rect(0, 0, 1, 1), + box: image.Rect(0, 0, 2, 2), + ls: LineStyleLight, + wantErr: true, + }, + { + desc: "box is too small", + canvas: image.Rect(0, 0, 2, 2), + box: image.Rect(0, 0, 1, 1), + ls: LineStyleLight, + wantErr: true, + }, + { + desc: "unsupported line style", + canvas: image.Rect(0, 0, 4, 4), + box: image.Rect(0, 0, 2, 2), + ls: lineStyleUnknown, + wantErr: true, + }, + { + desc: "draws box around the canvas", + canvas: image.Rect(0, 0, 4, 4), + box: image.Rect(0, 0, 4, 4), + ls: LineStyleLight, + want: cell.Buffer{ + { + cell.New(lineStyleChars[LineStyleLight][topLeftCorner]), + cell.New(lineStyleChars[LineStyleLight][vLine]), + cell.New(lineStyleChars[LineStyleLight][vLine]), + cell.New(lineStyleChars[LineStyleLight][bottomLeftCorner]), + }, + { + cell.New(lineStyleChars[LineStyleLight][hLine]), + cell.New(0), + cell.New(0), + cell.New(lineStyleChars[LineStyleLight][hLine]), + }, + { + cell.New(lineStyleChars[LineStyleLight][hLine]), + cell.New(0), + cell.New(0), + cell.New(lineStyleChars[LineStyleLight][hLine]), + }, + { + cell.New(lineStyleChars[LineStyleLight][topRightCorner]), + cell.New(lineStyleChars[LineStyleLight][vLine]), + cell.New(lineStyleChars[LineStyleLight][vLine]), + cell.New(lineStyleChars[LineStyleLight][bottomRightCorner]), + }, + }, + }, + { + desc: "draws box in the canvas", + canvas: image.Rect(0, 0, 4, 4), + box: image.Rect(1, 1, 3, 3), + ls: LineStyleLight, + want: cell.Buffer{ + { + cell.New(0), + cell.New(0), + cell.New(0), + cell.New(0), + }, + { + cell.New(0), + cell.New(lineStyleChars[LineStyleLight][topLeftCorner]), + cell.New(lineStyleChars[LineStyleLight][bottomLeftCorner]), + cell.New(0), + }, + { + cell.New(0), + cell.New(lineStyleChars[LineStyleLight][topRightCorner]), + cell.New(lineStyleChars[LineStyleLight][bottomRightCorner]), + cell.New(0), + }, + { + cell.New(0), + cell.New(0), + cell.New(0), + cell.New(0), + }, + }, + }, + { + desc: "draws box with cell options", + canvas: image.Rect(0, 0, 4, 4), + box: image.Rect(1, 1, 3, 3), + ls: LineStyleLight, + opts: []cell.Option{ + cell.FgColor(cell.ColorRed), + }, + want: cell.Buffer{ + { + cell.New(0), + cell.New(0), + cell.New(0), + cell.New(0), + }, + { + cell.New(0), + cell.New( + lineStyleChars[LineStyleLight][topLeftCorner], + cell.FgColor(cell.ColorRed), + ), + cell.New( + lineStyleChars[LineStyleLight][bottomLeftCorner], + cell.FgColor(cell.ColorRed), + ), + cell.New(0), + }, + { + cell.New(0), + cell.New( + lineStyleChars[LineStyleLight][topRightCorner], + cell.FgColor(cell.ColorRed), + ), + cell.New( + lineStyleChars[LineStyleLight][bottomRightCorner], + cell.FgColor(cell.ColorRed), + ), + cell.New(0), + }, + { + cell.New(0), + cell.New(0), + cell.New(0), + cell.New(0), + }, + }, + }, + } + + 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 = Box(c, tc.box, tc.ls, tc.opts...) + if (err != nil) != tc.wantErr { + t.Errorf("Box => unexpected error: %v, wantErr: %v", err, tc.wantErr) + } + if err != nil { + return + } + + ft, err := faketerm.New(c.Size()) + if err != nil { + t.Fatalf("faketerm.New => unexpected error: %v", err) + } + + if err := c.Apply(ft); err != nil { + t.Fatalf("Apply => unexpected error: %v", err) + } + + got := ft.BackBuffer() + if diff := pretty.Compare(tc.want, got); diff != "" { + t.Logf("Box => got output:\n%s", ft) + t.Errorf("Box => unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} diff --git a/draw/draw.go b/draw/draw.go new file mode 100644 index 0000000..62218e2 --- /dev/null +++ b/draw/draw.go @@ -0,0 +1,3 @@ +// Package draw provides functions that draw lines, shapes, etc on 2-D terminal +// like canvases. +package draw diff --git a/draw/line_style.go b/draw/line_style.go new file mode 100644 index 0000000..321cd1b --- /dev/null +++ b/draw/line_style.go @@ -0,0 +1,77 @@ +package draw + +import "fmt" + +// line_style.go contains the Unicode characters used for drawing lines of +// different styles. + +// lineStyleChars maps the line styles to the corresponding component characters. +var lineStyleChars = map[LineStyle]map[linePart]rune{ + LineStyleLight: map[linePart]rune{ + hLine: '─', + vLine: '│', + topLeftCorner: '┌', + topRightCorner: '┐', + bottomLeftCorner: '└', + bottomRightCorner: '┘', + }, +} + +// lineParts returns the line component characters for the provided line style. +func lineParts(ls LineStyle) (map[linePart]rune, error) { + parts, ok := lineStyleChars[ls] + if !ok { + return nil, fmt.Errorf("unsupported line style %v", ls) + } + return parts, nil +} + +// LineStyle defines the supported line styles.Q +type LineStyle int + +// String implements fmt.Stringer() +func (ls LineStyle) String() string { + if n, ok := lineStyleNames[ls]; ok { + return n + } + return "LineStyleUnknown" +} + +// lineStyleNames maps LineStyle values to human readable names. +var lineStyleNames = map[LineStyle]string{ + LineStyleLight: "LineStyleLight", +} + +const ( + lineStyleUnknown LineStyle = iota + LineStyleLight +) + +// linePart identifies individual line parts. +type linePart int + +// String implements fmt.Stringer() +func (lp linePart) String() string { + if n, ok := linePartNames[lp]; ok { + return n + } + return "linePartUnknown" +} + +// linePartNames maps linePart values to human readable names. +var linePartNames = map[linePart]string{ + vLine: "linePartVLine", + topLeftCorner: "linePartTopLeftCorner", + topRightCorner: "linePartTopRightCorner", + bottomLeftCorner: "linePartBottomLeftCorner", + bottomRightCorner: "linePartBottomRightCorner", +} + +const ( + hLine linePart = iota + vLine + topLeftCorner + topRightCorner + bottomLeftCorner + bottomRightCorner +) diff --git a/terminal/faketerm/faketerm.go b/terminal/faketerm/faketerm.go index edf5f75..c8e0e6f 100644 --- a/terminal/faketerm/faketerm.go +++ b/terminal/faketerm/faketerm.go @@ -2,6 +2,7 @@ package faketerm import ( + "bytes" "context" "errors" "fmt" @@ -55,6 +56,25 @@ func (t *Terminal) BackBuffer() cell.Buffer { return t.buffer } +// String prints out the buffer into a string. +// TODO(mum4k): Support printing of options. +// Implements fmt.Stringer. +func (t *Terminal) String() string { + size := t.Size() + var b bytes.Buffer + for row := 0; row < size.Y; row++ { + for col := 0; col < size.X; col++ { + r := t.buffer[col][row].Rune + if r == 0 { + r = ' ' + } + b.WriteRune(r) + } + b.WriteRune('\n') + } + return b.String() +} + // Implements terminalapi.Terminal.Size. func (t *Terminal) Size() image.Point { return t.buffer.Size()