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:
commit
6cb9f54dda
206
draw/hv_line.go
Normal file
206
draw/hv_line.go
Normal 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
204
draw/hv_line_graph.go
Normal 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
375
draw/hv_line_graph_test.go
Normal 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
673
draw/hv_line_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user