Adding the draw library.

And a function that draws boxes.
This commit is contained in:
Jakub Sobon 2018-03-30 01:41:22 +03:00
parent dc1f2c5a29
commit 6b592b7d34
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
6 changed files with 364 additions and 36 deletions

View File

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

68
draw/box.go Normal file
View File

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

188
draw/box_test.go Normal file
View File

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

3
draw/draw.go Normal file
View File

@ -0,0 +1,3 @@
// Package draw provides functions that draw lines, shapes, etc on 2-D terminal
// like canvases.
package draw

77
draw/line_style.go Normal file
View File

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

View File

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