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

Merge pull request #56 from mum4k/draw-line

Function that draws horizontal and vertical lines.
This commit is contained in:
Jakub Sobon 2018-12-01 17:35:06 -05:00 committed by GitHub
commit 6cb9f54dda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1499 additions and 2 deletions

206
draw/hv_line.go Normal file
View File

@ -0,0 +1,206 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
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
}
// newHVLineOptions returns a new hVLineOptions instance.
func newHVLineOptions() *hVLineOptions {
return &hVLineOptions{
lineStyle: DefaultHVLineStyle,
}
}
// 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.
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 is the cell where the line starts.
start image.Point
// end is the cell where the line ends.
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
// or a cross). Each line must be at least two cells 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 {
opt := newHVLineOptions()
for _, o := range opts {
o.set(opt)
}
g := newHVLineGraph()
for _, l := range lines {
line, err := newHVLine(c, l.start, l.end, opt)
if err != nil {
return err
}
g.addLine(line)
switch {
case line.horizontal():
for curX := line.start.X; ; curX++ {
cur := image.Point{curX, line.start.Y}
if _, err := c.SetCell(cur, line.mainPart, opt.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, opt.cellOpts...); err != nil {
return err
}
if curY == line.end.Y {
break
}
}
}
}
for _, n := range g.multiEdgeNodes() {
r, err := n.rune(opt.lineStyle)
if err != nil {
return err
}
if _, err := c.SetCell(n.p, r, opt.cellOpts...); err != nil {
return err
}
}
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
// 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 if 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 *hVLineOptions) (*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)
}
parts, err := lineParts(opts.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,
mainPart: mainPart,
opts: opts,
}, nil
}
// horizontal determines if this is a horizontal line.
func (hvl *hVLine) horizontal() bool {
return hvl.mainPart == lineStyleChars[hvl.opts.lineStyle][hLine]
}
// vertical determines if this is a vertical line.
func (hvl *hVLine) vertical() bool {
return hvl.mainPart == lineStyleChars[hvl.opts.lineStyle][vLine]
}

204
draw/hv_line_graph.go Normal file
View File

@ -0,0 +1,204 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
// hv_line_graph.go helps to keep track of locations where lines cross.
import (
"fmt"
"image"
)
// hVLineEdge is an edge between two points on the graph.
type hVLineEdge struct {
// from is the starting node of this edge.
// From is guaranteed to be less than to.
from image.Point
// to is the ending point of this edge.
to image.Point
}
// newHVLineEdge returns a new edge between the two points.
func newHVLineEdge(from, to image.Point) hVLineEdge {
return hVLineEdge{
from: from,
to: to,
}
}
// hVLineNode represents one node in the graph.
// I.e. one cell.
type hVLineNode struct {
// p is the point where this node is.
p image.Point
// edges are the edges between this node and the surrounding nodes.
// The code only supports horizontal and vertical lines so there can only
// ever be edges to nodes on these planes.
edges map[hVLineEdge]bool
}
// newHVLineNode creates a new newHVLineNode.
func newHVLineNode(p image.Point) *hVLineNode {
return &hVLineNode{
p: p,
edges: map[hVLineEdge]bool{},
}
}
// hasDown determines if this node has an edge to the one below it.
func (n *hVLineNode) hasDown() bool {
target := newHVLineEdge(n.p, image.Point{n.p.X, n.p.Y + 1})
_, ok := n.edges[target]
return ok
}
// hasUp determines if this node has an edge to the one above it.
func (n *hVLineNode) hasUp() bool {
target := newHVLineEdge(image.Point{n.p.X, n.p.Y - 1}, n.p)
_, ok := n.edges[target]
return ok
}
// hasLeft determines if this node has an edge to the next node on the left.
func (n *hVLineNode) hasLeft() bool {
target := newHVLineEdge(image.Point{n.p.X - 1, n.p.Y}, n.p)
_, ok := n.edges[target]
return ok
}
// hasRight determines if this node has an edge to the next node on the right.
func (n *hVLineNode) hasRight() bool {
target := newHVLineEdge(n.p, image.Point{n.p.X + 1, n.p.Y})
_, ok := n.edges[target]
return ok
}
// rune, given the selected line style returns the correct line character to
// represent this node.
// Only handles nodes with two or more edges, as returned by multiEdgeNodes().
func (n *hVLineNode) rune(ls LineStyle) (rune, error) {
parts, err := lineParts(ls)
if err != nil {
return -1, err
}
switch len(n.edges) {
case 2:
switch {
case n.hasLeft() && n.hasRight():
return parts[hLine], nil
case n.hasUp() && n.hasDown():
return parts[vLine], nil
case n.hasDown() && n.hasRight():
return parts[topLeftCorner], nil
case n.hasDown() && n.hasLeft():
return parts[topRightCorner], nil
case n.hasUp() && n.hasRight():
return parts[bottomLeftCorner], nil
case n.hasUp() && n.hasLeft():
return parts[bottomRightCorner], nil
default:
return -1, fmt.Errorf("unexpected two edges in node representing point %v: %v", n.p, n.edges)
}
case 3:
switch {
case n.hasUp() && n.hasLeft() && n.hasRight():
return parts[hAndUp], nil
case n.hasDown() && n.hasLeft() && n.hasRight():
return parts[hAndDown], nil
case n.hasUp() && n.hasDown() && n.hasRight():
return parts[vAndRight], nil
case n.hasUp() && n.hasDown() && n.hasLeft():
return parts[vAndLeft], nil
default:
return -1, fmt.Errorf("unexpected three edges in node representing point %v: %v", n.p, n.edges)
}
case 4:
return parts[vAndH], nil
default:
return -1, fmt.Errorf("unexpected number of edges(%d) in node representing point %v", len(n.edges), n.p)
}
}
// hVLineGraph represents lines on the canvas as a bidirectional graph of
// nodes. Helps to determine the characters that should be used where multiple
// lines cross.
type hVLineGraph struct {
nodes map[image.Point]*hVLineNode
}
// newHVLineGraph creates a new hVLineGraph.
func newHVLineGraph() *hVLineGraph {
return &hVLineGraph{
nodes: make(map[image.Point]*hVLineNode),
}
}
// getOrCreateNode gets an existing or creates a new node for the point.
func (g *hVLineGraph) getOrCreateNode(p image.Point) *hVLineNode {
if n, ok := g.nodes[p]; ok {
return n
}
n := newHVLineNode(p)
g.nodes[p] = n
return n
}
// addLine adds a line to the graph.
// This adds edges between all the points on the line.
func (g *hVLineGraph) addLine(line *hVLine) {
switch {
case line.horizontal():
for curX := line.start.X; curX < line.end.X; curX++ {
from := image.Point{curX, line.start.Y}
to := image.Point{curX + 1, line.start.Y}
n1 := g.getOrCreateNode(from)
n2 := g.getOrCreateNode(to)
edge := newHVLineEdge(from, to)
n1.edges[edge] = true
n2.edges[edge] = true
}
case line.vertical():
for curY := line.start.Y; curY < line.end.Y; curY++ {
from := image.Point{line.start.X, curY}
to := image.Point{line.start.X, curY + 1}
n1 := g.getOrCreateNode(from)
n2 := g.getOrCreateNode(to)
edge := newHVLineEdge(from, to)
n1.edges[edge] = true
n2.edges[edge] = true
}
}
}
// multiEdgeNodes returns all nodes that have more than one edge. These are
// the nodes where we might need to use different line characters to represent
// the crossing of multiple lines.
func (g *hVLineGraph) multiEdgeNodes() []*hVLineNode {
var nodes []*hVLineNode
for _, n := range g.nodes {
if len(n.edges) <= 1 {
continue
}
nodes = append(nodes, n)
}
return nodes
}

375
draw/hv_line_graph_test.go Normal file
View File

@ -0,0 +1,375 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package draw
import (
"image"
"sort"
"testing"
"github.com/kylelemons/godebug/pretty"
"github.com/mum4k/termdash/canvas"
)
func TestMultiEdgeNodes(t *testing.T) {
tests := []struct {
desc string
lines []HVLine
want []*hVLineNode
}{
{
desc: "no lines added",
},
{
desc: "single-edge nodes only",
lines: []HVLine{
{
start: image.Point{0, 0},
end: image.Point{0, 1},
},
{
start: image.Point{1, 0},
end: image.Point{1, 1},
},
},
},
{
desc: "lines don't cross",
lines: []HVLine{
{
start: image.Point{0, 0},
end: image.Point{0, 2},
},
{
start: image.Point{1, 0},
end: image.Point{1, 2},
},
},
want: []*hVLineNode{
{
p: image.Point{0, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
newHVLineEdge(image.Point{0, 1}, image.Point{0, 2}): true,
},
},
{
p: image.Point{1, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
},
},
},
},
{
desc: "lines cross, node has two edges",
lines: []HVLine{
{
start: image.Point{0, 0},
end: image.Point{0, 1},
},
{
start: image.Point{0, 0},
end: image.Point{1, 0},
},
},
want: []*hVLineNode{
{
p: image.Point{0, 0},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
newHVLineEdge(image.Point{0, 0}, image.Point{1, 0}): true,
},
},
},
},
{
desc: "lines cross, node has three edges",
lines: []HVLine{
{
start: image.Point{0, 0},
end: image.Point{0, 2},
},
{
start: image.Point{0, 1},
end: image.Point{1, 1},
},
},
want: []*hVLineNode{
{
p: image.Point{0, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{0, 1}, image.Point{0, 2}): true,
},
},
},
},
{
desc: "lines cross, node has four edges",
lines: []HVLine{
{
start: image.Point{1, 0},
end: image.Point{1, 2},
},
{
start: image.Point{0, 1},
end: image.Point{2, 1},
},
},
want: []*hVLineNode{
{
p: image.Point{1, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{2, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
c, err := canvas.New(image.Rect(0, 0, 3, 3))
if err != nil {
t.Fatalf("canvas.New => unexpected error: %v", err)
}
g := newHVLineGraph()
for i, l := range tc.lines {
line, err := newHVLine(c, l.start, l.end, newHVLineOptions())
if err != nil {
t.Fatalf("newHVLine[%d] => unexpected error: %v", i, err)
}
g.addLine(line)
}
got := g.multiEdgeNodes()
lessFn := func(i, j int) bool {
return got[i].p.X < got[j].p.X || got[i].p.Y < got[j].p.Y
}
sort.Slice(got, lessFn)
sort.Slice(tc.want, lessFn)
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("multiEdgeNodes => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestNodeRune(t *testing.T) {
tests := []struct {
desc string
node *hVLineNode
ls LineStyle
want rune
wantErr bool
}{
{
desc: "fails on node with no edges",
node: &hVLineNode{},
wantErr: true,
},
{
desc: "fails on unsupported two edge combination",
node: &hVLineNode{
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 0}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{2, 2}): true,
},
},
ls: LineStyleLight,
wantErr: true,
},
{
desc: "fails on unsupported three edge combination",
node: &hVLineNode{
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 0}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{2, 2}): true,
},
},
ls: LineStyleLight,
wantErr: true,
},
{
desc: "fails on unsupported line style",
node: &hVLineNode{},
ls: LineStyle(-1),
wantErr: true,
},
{
desc: "horizontal line",
node: &hVLineNode{
p: image.Point{1, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{2, 1}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][hLine],
},
{
desc: "vertical line",
node: &hVLineNode{
p: image.Point{1, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][vLine],
},
{
desc: "top left corner",
node: &hVLineNode{
p: image.Point{0, 0},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 0}, image.Point{1, 0}): true,
newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][topLeftCorner],
},
{
desc: "top right corner",
node: &hVLineNode{
p: image.Point{2, 0},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{1, 0}, image.Point{2, 0}): true,
newHVLineEdge(image.Point{2, 0}, image.Point{2, 1}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][topRightCorner],
},
{
desc: "bottom left corner",
node: &hVLineNode{
p: image.Point{0, 2},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 1}, image.Point{0, 2}): true,
newHVLineEdge(image.Point{0, 2}, image.Point{1, 2}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][bottomLeftCorner],
},
{
desc: "bottom right corner",
node: &hVLineNode{
p: image.Point{2, 2},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{1, 2}, image.Point{2, 2}): true,
newHVLineEdge(image.Point{2, 1}, image.Point{2, 2}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][bottomRightCorner],
},
{
desc: "T horizontal and up",
node: &hVLineNode{
p: image.Point{1, 2},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
newHVLineEdge(image.Point{0, 2}, image.Point{1, 2}): true,
newHVLineEdge(image.Point{1, 2}, image.Point{2, 2}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][hAndUp],
},
{
desc: "T horizontal and down",
node: &hVLineNode{
p: image.Point{1, 0},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 0}, image.Point{1, 0}): true,
newHVLineEdge(image.Point{1, 0}, image.Point{2, 0}): true,
newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][hAndDown],
},
{
desc: "T vertical and right",
node: &hVLineNode{
p: image.Point{0, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{0, 1}, image.Point{0, 2}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][vAndRight],
},
{
desc: "T vertical and left",
node: &hVLineNode{
p: image.Point{2, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{2, 0}, image.Point{2, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{2, 1}): true,
newHVLineEdge(image.Point{2, 1}, image.Point{2, 2}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][vAndLeft],
},
{
desc: "cross",
node: &hVLineNode{
p: image.Point{1, 1},
edges: map[hVLineEdge]bool{
newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{2, 1}): true,
newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
},
},
ls: LineStyleLight,
want: lineStyleChars[LineStyleLight][vAndH],
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.node.rune(tc.ls)
if (err != nil) != tc.wantErr {
t.Errorf("rune => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if got != tc.want {
t.Errorf("rune => got %c, want %c", got, tc.want)
}
})
}
}

673
draw/hv_line_test.go Normal file
View File

@ -0,0 +1,673 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
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, 2, 2),
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)
}
})
}
}

View File

@ -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,14 +33,31 @@ 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))
}
}
}
}
// 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 nil, fmt.Errorf("unsupported line style %d", ls)
}
return parts, nil
}
@ -80,6 +102,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 +116,9 @@ const (
topRightCorner
bottomLeftCorner
bottomRightCorner
hAndUp
hAndDown
vAndLeft
vAndRight
vAndH
)

View File

@ -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))
}
}