mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
commit
e44d70b568
12
README.md
12
README.md
@ -40,7 +40,6 @@ To install this library, run the following:
|
||||
|
||||
```
|
||||
go get -u github.com/mum4k/termdash
|
||||
|
||||
```
|
||||
|
||||
# Usage
|
||||
@ -129,6 +128,17 @@ go run github.com/mum4k/termdash/widgets/linechart/linechartdemo/linechartdemo.g
|
||||
|
||||
[<img src="./images/linechartdemo.gif" alt="linechartdemo" type="image/gif">](widgets/linechart/linechartdemo/linechartdemo.go)
|
||||
|
||||
### The SegmentDisplay
|
||||
|
||||
Displays text by simulating a 16-segment display. Run the
|
||||
[linechartdemo](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go).
|
||||
|
||||
```go
|
||||
go run github.com/mum4k/termdash/widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
|
||||
```
|
||||
|
||||
[<img src="./images/segmentdisplaydemo.gif" alt="segmentdisplaydemo" type="image/gif">](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go)
|
||||
|
||||
# Contributing
|
||||
|
||||
If you are willing to contribute, improve the infrastructure or develop a
|
||||
|
18
area/area.go
18
area/area.go
@ -18,6 +18,8 @@ package area
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/mum4k/termdash/numbers"
|
||||
)
|
||||
|
||||
// Size returns the size of the provided area.
|
||||
@ -76,14 +78,6 @@ func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right im
|
||||
return left, right, nil
|
||||
}
|
||||
|
||||
// abs returns the absolute value of x.
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ExcludeBorder returns a new area created by subtracting a border around the
|
||||
// provided area. Return the zero area if there isn't enough space to exclude
|
||||
// the border.
|
||||
@ -95,10 +89,10 @@ func ExcludeBorder(area image.Rectangle) image.Rectangle {
|
||||
return image.ZR
|
||||
}
|
||||
return image.Rect(
|
||||
abs(area.Min.X+1),
|
||||
abs(area.Min.Y+1),
|
||||
abs(area.Max.X-1),
|
||||
abs(area.Max.Y-1),
|
||||
numbers.Abs(area.Min.X+1),
|
||||
numbers.Abs(area.Min.Y+1),
|
||||
numbers.Abs(area.Max.X-1),
|
||||
numbers.Abs(area.Max.Y-1),
|
||||
)
|
||||
}
|
||||
|
||||
|
103
attrrange/attrrange.go
Normal file
103
attrrange/attrrange.go
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright 2019 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 attrrange simplifies tracking of attributes that apply to a range of
|
||||
// items.
|
||||
// Refer to the examples if the test file for details on usage.
|
||||
package attrrange
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// AttrRange is a range of items that share the same attributes.
|
||||
type AttrRange struct {
|
||||
// Low is the first position where these attributes apply.
|
||||
Low int
|
||||
|
||||
// High is the end of the range. The attributes apply to all items in range
|
||||
// Low <= b < high.
|
||||
High int
|
||||
|
||||
// AttrIdx is the index of the attributes that apply to this range.
|
||||
AttrIdx int
|
||||
}
|
||||
|
||||
// newAttrRange returns a new AttrRange instance.
|
||||
func newAttrRange(low, high, attrIdx int) *AttrRange {
|
||||
return &AttrRange{
|
||||
Low: low,
|
||||
High: high,
|
||||
AttrIdx: attrIdx,
|
||||
}
|
||||
}
|
||||
|
||||
// Tracker tracks attributes that apply to a range of items.
|
||||
// This object is not thread safe.
|
||||
type Tracker struct {
|
||||
// ranges maps low indices of ranges to the attribute ranges.
|
||||
ranges map[int]*AttrRange
|
||||
}
|
||||
|
||||
// NewTracker returns a new tracker of ranges that share the same attributes.
|
||||
func NewTracker() *Tracker {
|
||||
return &Tracker{
|
||||
ranges: map[int]*AttrRange{},
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a new range of items that share attributes with the specified
|
||||
// index.
|
||||
// The low position of the range must not overlap with low position of any
|
||||
// existing range.
|
||||
func (t *Tracker) Add(low, high, attrIdx int) error {
|
||||
ar := newAttrRange(low, high, attrIdx)
|
||||
if ar, ok := t.ranges[low]; ok {
|
||||
return fmt.Errorf("already have range starting on low:%d, existing:%+v", low, ar)
|
||||
}
|
||||
t.ranges[low] = ar
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForPosition returns attribute index that apply to the specified position.
|
||||
// Returns ErrNotFound when the requested position wasn't found in any of the
|
||||
// known ranges.
|
||||
func (t *Tracker) ForPosition(pos int) (*AttrRange, error) {
|
||||
if ar, ok := t.ranges[pos]; ok {
|
||||
return ar, nil
|
||||
}
|
||||
|
||||
var keys []int
|
||||
for k := range t.ranges {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Ints(keys)
|
||||
|
||||
var res *AttrRange
|
||||
for _, k := range keys {
|
||||
ar := t.ranges[k]
|
||||
if ar.Low > pos {
|
||||
break
|
||||
}
|
||||
if ar.High > pos {
|
||||
res = ar
|
||||
}
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
return nil, fmt.Errorf("did not find attribute range for position %d", pos)
|
||||
}
|
||||
return res, nil
|
||||
}
|
165
attrrange/attrrange_test.go
Normal file
165
attrrange/attrrange_test.go
Normal file
@ -0,0 +1,165 @@
|
||||
// Copyright 2019 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 attrrange
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
// Caller has a slice of some attributes, like a cell color that applies
|
||||
// to a portion of text.
|
||||
attrs := []cell.Color{cell.ColorRed, cell.ColorBlue}
|
||||
redIdx := 0
|
||||
blueIdx := 1
|
||||
|
||||
// This is the text the colors apply to.
|
||||
const text = "HelloWorld"
|
||||
|
||||
// Assuming that we want the word "Hello" in red and the word "World" in
|
||||
// green, we can set our ranges as follows:
|
||||
tr := NewTracker()
|
||||
if err := tr.Add(0, len("Hello"), redIdx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := tr.Add(len("Hello")+1, len(text), blueIdx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Now to get the index into attrs (i.e. the color) for a particular
|
||||
// character, we can do:
|
||||
for i, c := range text {
|
||||
ar, err := tr.ForPosition(i)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("character at text[%d] = %q, color index %d = %v, range low:%d, high:%d", i, c, ar.AttrIdx, attrs[ar.AttrIdx], ar.Low, ar.High)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForPosition(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
// if not nil, called before calling ForPosition.
|
||||
// Can add ranges.
|
||||
update func(*Tracker) error
|
||||
pos int
|
||||
want *AttrRange
|
||||
wantErr bool
|
||||
wantUpdateErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails when no ranges given",
|
||||
pos: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails to add a duplicate",
|
||||
update: func(tr *Tracker) error {
|
||||
if err := tr.Add(2, 5, 40); err != nil {
|
||||
return err
|
||||
}
|
||||
return tr.Add(2, 3, 41)
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails when multiple given ranges, position falls before them",
|
||||
update: func(tr *Tracker) error {
|
||||
if err := tr.Add(2, 5, 40); err != nil {
|
||||
return err
|
||||
}
|
||||
return tr.Add(5, 10, 41)
|
||||
},
|
||||
pos: 1,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls on the lower",
|
||||
update: func(tr *Tracker) error {
|
||||
if err := tr.Add(2, 5, 40); err != nil {
|
||||
return err
|
||||
}
|
||||
return tr.Add(5, 10, 41)
|
||||
},
|
||||
pos: 2,
|
||||
want: newAttrRange(2, 5, 40),
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls between them",
|
||||
update: func(tr *Tracker) error {
|
||||
if err := tr.Add(2, 5, 40); err != nil {
|
||||
return err
|
||||
}
|
||||
return tr.Add(5, 10, 41)
|
||||
},
|
||||
pos: 4,
|
||||
want: newAttrRange(2, 5, 40),
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls on the higher",
|
||||
update: func(tr *Tracker) error {
|
||||
if err := tr.Add(2, 5, 40); err != nil {
|
||||
return err
|
||||
}
|
||||
return tr.Add(5, 10, 41)
|
||||
},
|
||||
pos: 5,
|
||||
want: newAttrRange(5, 10, 41),
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls after them",
|
||||
update: func(tr *Tracker) error {
|
||||
if err := tr.Add(2, 5, 40); err != nil {
|
||||
return err
|
||||
}
|
||||
return tr.Add(5, 10, 41)
|
||||
},
|
||||
pos: 10,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
tr := NewTracker()
|
||||
if tc.update != nil {
|
||||
err := tc.update(tr)
|
||||
if (err != nil) != tc.wantUpdateErr {
|
||||
t.Errorf("tc.update => unexpected error:%v, wantUpdateErr:%v", err, tc.wantUpdateErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
got, err := tr.ForPosition(tc.pos)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("ForPosition => unexpected error:%v, wantErr:%v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("ForPosition => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -50,3 +50,11 @@ func MustSetCell(c *canvas.Canvas, p image.Point, r rune, opts ...cell.Option) i
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
// MustCopyTo copies the content of the source canvas onto the destination
|
||||
// canvas or panics.
|
||||
func MustCopyTo(src, dst *canvas.Canvas) {
|
||||
if err := src.CopyTo(dst); err != nil {
|
||||
panic(fmt.Sprintf("canvas.CopyTo => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/mum4k/termdash/canvas/braille"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/numbers"
|
||||
)
|
||||
|
||||
// braillePixelChange represents an action on a pixel on the braille canvas.
|
||||
@ -133,8 +134,8 @@ func brailleLinePoints(start, end image.Point) []image.Point {
|
||||
// Implements Bresenham's line algorithm.
|
||||
// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
|
||||
|
||||
vertProj := abs(end.Y - start.Y)
|
||||
horizProj := abs(end.X - start.X)
|
||||
vertProj := numbers.Abs(end.Y - start.Y)
|
||||
horizProj := numbers.Abs(end.X - start.X)
|
||||
if vertProj < horizProj {
|
||||
if start.X > end.X {
|
||||
return lineLow(end.X, end.Y, start.X, start.Y)
|
||||
@ -201,11 +202,3 @@ func lineHigh(x0, y0, x1, y1 int) []image.Point {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// abs returns the absolute value of x.
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
472
draw/segdisp/segment/segment.go
Normal file
472
draw/segdisp/segment/segment.go
Normal file
@ -0,0 +1,472 @@
|
||||
// Copyright 2019 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 segment provides functions that draw a single segment.
|
||||
package segment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/mum4k/termdash/canvas/braille"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
)
|
||||
|
||||
// Type identifies the type of the segment that is drawn.
|
||||
type Type int
|
||||
|
||||
// String implements fmt.Stringer()
|
||||
func (st Type) String() string {
|
||||
if n, ok := segmentTypeNames[st]; ok {
|
||||
return n
|
||||
}
|
||||
return "TypeUnknown"
|
||||
}
|
||||
|
||||
// segmentTypeNames maps Type values to human readable names.
|
||||
var segmentTypeNames = map[Type]string{
|
||||
Horizontal: "Horizontal",
|
||||
Vertical: "Vertical",
|
||||
}
|
||||
|
||||
const (
|
||||
segmentTypeUnknown Type = iota
|
||||
|
||||
// Horizontal is a horizontal segment.
|
||||
Horizontal
|
||||
// Vertical is a vertical segment.
|
||||
Vertical
|
||||
|
||||
segmentTypeMax // Used for validation.
|
||||
)
|
||||
|
||||
// Option is used to provide options.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
set(*options)
|
||||
}
|
||||
|
||||
// options stores the provided options.
|
||||
type options struct {
|
||||
cellOpts []cell.Option
|
||||
skipSlopesLTE int
|
||||
reverseSlopes bool
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*options)
|
||||
|
||||
// set implements Option.set.
|
||||
func (o option) set(opts *options) {
|
||||
o(opts)
|
||||
}
|
||||
|
||||
// CellOpts sets options on the cells that contain the segment.
|
||||
// Cell options on a braille canvas can only be set on the entire cell, not per
|
||||
// pixel.
|
||||
func CellOpts(cOpts ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.cellOpts = cOpts
|
||||
})
|
||||
}
|
||||
|
||||
// SkipSlopesLTE if provided instructs HV to not create slopes at the ends of a
|
||||
// segment if the height of the horizontal or the width of the vertical segment
|
||||
// is less or equal to the provided value.
|
||||
func SkipSlopesLTE(v int) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.skipSlopesLTE = v
|
||||
})
|
||||
}
|
||||
|
||||
// ReverseSlopes if provided reverses the order in which slopes are drawn.
|
||||
// This only has a visible effect when the horizontal segment has height of two
|
||||
// or the vertical segment has width of two.
|
||||
// Without this option segments with height / width of two look like this:
|
||||
// - |
|
||||
// --- ||
|
||||
// |
|
||||
//
|
||||
// With this option:
|
||||
// --- |
|
||||
// - ||
|
||||
// |
|
||||
func ReverseSlopes() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.reverseSlopes = true
|
||||
})
|
||||
}
|
||||
|
||||
// validArea validates the provided area.
|
||||
func validArea(ar image.Rectangle) error {
|
||||
if ar.Min.X < 0 || ar.Min.Y < 0 {
|
||||
return fmt.Errorf("the start coordinates cannot be negative, got: %v", ar)
|
||||
}
|
||||
if ar.Max.X < 0 || ar.Max.Y < 0 {
|
||||
return fmt.Errorf("the end coordinates cannot be negative, got: %v", ar)
|
||||
}
|
||||
if ar.Dx() < 1 || ar.Dy() < 1 {
|
||||
return fmt.Errorf("the area for the segment must be at least 1x1 pixels, got %vx%v in area:%v", ar.Dx(), ar.Dy(), ar)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HV draws a horizontal or a vertical display segment, filling the provided area.
|
||||
// The segment will have slopes on both of its ends.
|
||||
func HV(bc *braille.Canvas, ar image.Rectangle, st Type, opts ...Option) error {
|
||||
if err := validArea(ar); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt := &options{}
|
||||
for _, o := range opts {
|
||||
o.set(opt)
|
||||
}
|
||||
|
||||
var nextLine nextHVLineFn
|
||||
var lines int
|
||||
switch st {
|
||||
case Horizontal:
|
||||
lines = ar.Dy()
|
||||
nextLine = nextHorizLine
|
||||
|
||||
case Vertical:
|
||||
lines = ar.Dx()
|
||||
nextLine = nextVertLine
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported segment type %v(%d)", st, st)
|
||||
}
|
||||
|
||||
for i := 0; i < lines; i++ {
|
||||
start, end := nextLine(i, ar, opt)
|
||||
if err := draw.BrailleLine(bc, start, end, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextHVLineFn is a function that determines the start and end points of a line
|
||||
// number num in a horizontal or a vertical segment.
|
||||
type nextHVLineFn func(num int, ar image.Rectangle, opt *options) (image.Point, image.Point)
|
||||
|
||||
// nextHorizLine determines the start and end point of individual lines in a
|
||||
// horizontal segment.
|
||||
func nextHorizLine(num int, ar image.Rectangle, opt *options) (image.Point, image.Point) {
|
||||
// Start and end points of the full row without adjustments for slopes.
|
||||
start := image.Point{ar.Min.X, ar.Min.Y + num}
|
||||
end := image.Point{ar.Max.X - 1, ar.Min.Y + num}
|
||||
|
||||
height := ar.Dy()
|
||||
width := ar.Dx()
|
||||
if height <= opt.skipSlopesLTE || height < 2 || width < 3 {
|
||||
// No slopes under these dimensions as we don't have the resolution.
|
||||
return start, end
|
||||
}
|
||||
|
||||
// Don't adjust rows that fall exactly in the middle of the segment height.
|
||||
// E.g when height divides oddly, we want the middle row to take the full
|
||||
// width:
|
||||
// --
|
||||
// ----
|
||||
// --
|
||||
//
|
||||
// And when the height divides oddly, we want the two middle rows to take
|
||||
// the full width:
|
||||
// --
|
||||
// ----
|
||||
// ----
|
||||
// --
|
||||
// We only do this for segments that are at least three rows tall.
|
||||
// For smaller segments we still want this behavior:
|
||||
// --
|
||||
// ----
|
||||
halfHeight := height / 2
|
||||
if height > 2 {
|
||||
if num == halfHeight || (height%2 == 0 && num == halfHeight-1) {
|
||||
return start, end
|
||||
}
|
||||
}
|
||||
if height == 2 && opt.reverseSlopes {
|
||||
return adjustHoriz(start, end, width, num)
|
||||
}
|
||||
|
||||
if num < halfHeight {
|
||||
adjust := halfHeight - num
|
||||
if height%2 == 0 && height > 2 {
|
||||
// On evenly divided height, we need one less adjustment on every
|
||||
// row above the half, since two rows are taking the full width
|
||||
// as shown above.
|
||||
adjust--
|
||||
}
|
||||
return adjustHoriz(start, end, width, adjust)
|
||||
}
|
||||
adjust := num - halfHeight
|
||||
return adjustHoriz(start, end, width, adjust)
|
||||
}
|
||||
|
||||
// nextVertLine determines the start and end point of individual lines in a
|
||||
// vertical segment.
|
||||
func nextVertLine(num int, ar image.Rectangle, opt *options) (image.Point, image.Point) {
|
||||
// Start and end points of the full column without adjustments for slopes.
|
||||
start := image.Point{ar.Min.X + num, ar.Min.Y}
|
||||
end := image.Point{ar.Min.X + num, ar.Max.Y - 1}
|
||||
|
||||
height := ar.Dy()
|
||||
width := ar.Dx()
|
||||
if width <= opt.skipSlopesLTE || height < 3 || width < 2 {
|
||||
// No slopes under these dimensions as we don't have the resolution.
|
||||
return start, end
|
||||
}
|
||||
|
||||
// Don't adjust lines that fall exactly in the middle of the segment height.
|
||||
// E.g when width divides oddly, we want the middle line to take the full
|
||||
// height:
|
||||
// |
|
||||
// |||
|
||||
// |||
|
||||
// |
|
||||
//
|
||||
// And when the width divides oddly, we want the two middle columns to take
|
||||
// the full height:
|
||||
// ||
|
||||
// ||||
|
||||
// ||||
|
||||
// ||
|
||||
//
|
||||
// We only do this for segments that are at least three columns wide.
|
||||
// For smaller segments we still want this behavior:
|
||||
// |
|
||||
// ||
|
||||
// ||
|
||||
// |
|
||||
halfWidth := width / 2
|
||||
if width > 2 {
|
||||
if num == halfWidth || (width%2 == 0 && num == halfWidth-1) {
|
||||
return start, end
|
||||
}
|
||||
}
|
||||
if width == 2 && opt.reverseSlopes {
|
||||
return adjustVert(start, end, width, num)
|
||||
}
|
||||
|
||||
if num < halfWidth {
|
||||
adjust := halfWidth - num
|
||||
if width%2 == 0 && width > 2 {
|
||||
// On evenly divided width, we need one less adjustment on every
|
||||
// column above the half, since two lines are taking the full
|
||||
// height as shown above.
|
||||
adjust--
|
||||
}
|
||||
return adjustVert(start, end, height, adjust)
|
||||
}
|
||||
adjust := num - halfWidth
|
||||
return adjustVert(start, end, height, adjust)
|
||||
}
|
||||
|
||||
// adjustHoriz given start and end points that identify a horizontal line,
|
||||
// returns points that are adjusted towards each other on the line by the
|
||||
// specified amount.
|
||||
// I.e. the start is moved to the right and the end is moved to the left.
|
||||
// The points won't be allowed to cross each other.
|
||||
// The segWidth is the full width of the segment we are drawing.
|
||||
func adjustHoriz(start, end image.Point, segWidth int, adjust int) (image.Point, image.Point) {
|
||||
ns := start.Add(image.Point{adjust, 0})
|
||||
ne := end.Sub(image.Point{adjust, 0})
|
||||
|
||||
if ns.X <= ne.X {
|
||||
return ns, ne
|
||||
}
|
||||
|
||||
halfWidth := segWidth / 2
|
||||
if segWidth%2 == 0 {
|
||||
// The width of the segment divides evenly, place start and end next to each other.
|
||||
// E.g: 0 1 2 3 4 5
|
||||
// - - ns ne - -
|
||||
ns = image.Point{halfWidth - 1, start.Y}
|
||||
ne = image.Point{halfWidth, end.Y}
|
||||
} else {
|
||||
// The width of the segment divides oddly, place both start and end on the mid point.
|
||||
// E.g: 0 1 2 3 4
|
||||
// - - nsne - -
|
||||
ns = image.Point{halfWidth, start.Y}
|
||||
ne = ns
|
||||
}
|
||||
return ns, ne
|
||||
}
|
||||
|
||||
// adjustVert given start and end points that identify a vertical line,
|
||||
// returns points that are adjusted towards each other on the line by the
|
||||
// specified amount.
|
||||
// I.e. the start is moved down and the end is moved up.
|
||||
// The points won't be allowed to cross each other.
|
||||
// The segHeight is the full height of the segment we are drawing.
|
||||
func adjustVert(start, end image.Point, segHeight int, adjust int) (image.Point, image.Point) {
|
||||
adjStart, adjEnd := adjustHoriz(swapCoord(start), swapCoord(end), segHeight, adjust)
|
||||
return swapCoord(adjStart), swapCoord(adjEnd)
|
||||
}
|
||||
|
||||
// swapCoord returns a point with its X and Y coordinates swapped.
|
||||
func swapCoord(p image.Point) image.Point {
|
||||
return image.Point{p.Y, p.X}
|
||||
}
|
||||
|
||||
// DiagonalType determines the type of diagonal segment.
|
||||
type DiagonalType int
|
||||
|
||||
// String implements fmt.Stringer()
|
||||
func (dt DiagonalType) String() string {
|
||||
if n, ok := diagonalTypeNames[dt]; ok {
|
||||
return n
|
||||
}
|
||||
return "DiagonalTypeUnknown"
|
||||
}
|
||||
|
||||
// diagonalTypeNames maps DiagonalType values to human readable names.
|
||||
var diagonalTypeNames = map[DiagonalType]string{
|
||||
LeftToRight: "LeftToRight",
|
||||
RightToLeft: "RightToLeft",
|
||||
}
|
||||
|
||||
const (
|
||||
diagonalTypeUnknown DiagonalType = iota
|
||||
// LeftToRight is a diagonal segment from top left to bottom right.
|
||||
LeftToRight
|
||||
// RightToLeft is a diagonal segment from top right to bottom left.
|
||||
RightToLeft
|
||||
|
||||
diagonalTypeMax // Used for validation.
|
||||
)
|
||||
|
||||
// nextDiagLineFn is a function that determines the start and end points of a line
|
||||
// number num in a diagonal segment.
|
||||
// Points start and end define the first diagonal exactly in the middle.
|
||||
// Points prevStart and prevEnd define line num-1.
|
||||
type nextDiagLineFn func(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point)
|
||||
|
||||
// DiagonalOption is used to provide options.
|
||||
type DiagonalOption interface {
|
||||
// set sets the provided option.
|
||||
set(*diagonalOptions)
|
||||
}
|
||||
|
||||
// diagonalOptions stores the provided diagonal options.
|
||||
type diagonalOptions struct {
|
||||
cellOpts []cell.Option
|
||||
}
|
||||
|
||||
// diagonalOption implements DiagonalOption.
|
||||
type diagonalOption func(*diagonalOptions)
|
||||
|
||||
// set implements DiagonalOption.set.
|
||||
func (o diagonalOption) set(opts *diagonalOptions) {
|
||||
o(opts)
|
||||
}
|
||||
|
||||
// DiagonalCellOpts sets options on the cells that contain the diagonal
|
||||
// segment.
|
||||
// Cell options on a braille canvas can only be set on the entire cell, not per
|
||||
// pixel.
|
||||
func DiagonalCellOpts(cOpts ...cell.Option) DiagonalOption {
|
||||
return diagonalOption(func(opts *diagonalOptions) {
|
||||
opts.cellOpts = cOpts
|
||||
})
|
||||
}
|
||||
|
||||
// Diagonal draws a diagonal segment of the specified width filling the area.
|
||||
func Diagonal(bc *braille.Canvas, ar image.Rectangle, width int, dt DiagonalType, opts ...DiagonalOption) error {
|
||||
if err := validArea(ar); err != nil {
|
||||
return err
|
||||
}
|
||||
if min := 1; width < min {
|
||||
return fmt.Errorf("invalid width %d, must be width >= %d", width, min)
|
||||
}
|
||||
opt := &diagonalOptions{}
|
||||
for _, o := range opts {
|
||||
o.set(opt)
|
||||
}
|
||||
|
||||
var start, end image.Point
|
||||
var nextFn nextDiagLineFn
|
||||
switch dt {
|
||||
case LeftToRight:
|
||||
start = ar.Min
|
||||
end = image.Point{ar.Max.X - 1, ar.Max.Y - 1}
|
||||
nextFn = nextLRLine
|
||||
|
||||
case RightToLeft:
|
||||
start = image.Point{ar.Max.X - 1, ar.Min.Y}
|
||||
end = image.Point{ar.Min.X, ar.Max.Y - 1}
|
||||
nextFn = nextRLLine
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported diagonal type %v(%d)", dt, dt)
|
||||
}
|
||||
|
||||
if err := draw.BrailleLine(bc, start, end, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns := start
|
||||
ne := end
|
||||
for i := 1; i < width; i++ {
|
||||
ns, ne = nextFn(i, start, end, ns, ne)
|
||||
|
||||
if !ns.In(ar) || !ne.In(ar) {
|
||||
return fmt.Errorf("cannot draw diagonal segment of width %d in area %v, the area isn't large enough for line %v-%v", width, ar, ns, ne)
|
||||
}
|
||||
|
||||
if err := draw.BrailleLine(bc, ns, ne, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextLRLine is a function that determines the start and end points of the
|
||||
// next line of a left-to-right diagonal segment.
|
||||
func nextLRLine(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point) {
|
||||
dist := num / 2
|
||||
if num%2 != 0 {
|
||||
// Every odd line is placed above the mid diagonal.
|
||||
ns := image.Point{start.X + dist + 1, start.Y}
|
||||
ne := image.Point{end.X, end.Y - dist - 1}
|
||||
return ns, ne
|
||||
}
|
||||
|
||||
// Every even line is placed under the mid diagonal.
|
||||
ns := image.Point{start.X, start.Y + dist}
|
||||
ne := image.Point{end.X - dist, end.Y}
|
||||
return ns, ne
|
||||
}
|
||||
|
||||
// nextRLLine is a function that determines the start and end points of the
|
||||
// next line of a right-to-left diagonal segment.
|
||||
func nextRLLine(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point) {
|
||||
dist := num / 2
|
||||
if num%2 != 0 {
|
||||
// Every odd line is placed above the mid diagonal.
|
||||
ns := image.Point{start.X - dist - 1, start.Y}
|
||||
ne := image.Point{end.X, end.Y - dist - 1}
|
||||
return ns, ne
|
||||
}
|
||||
|
||||
// Every even line is placed under the mid diagonal.
|
||||
ns := image.Point{start.X, start.Y + dist}
|
||||
ne := image.Point{end.X + dist, end.Y}
|
||||
return ns, ne
|
||||
}
|
1760
draw/segdisp/segment/segment_test.go
Normal file
1760
draw/segdisp/segment/segment_test.go
Normal file
File diff suppressed because it is too large
Load Diff
38
draw/segdisp/segment/testsegment/testsegment.go
Normal file
38
draw/segdisp/segment/testsegment/testsegment.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2019 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 testsegment provides helpers for tests that use the segment package.
|
||||
package testsegment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/mum4k/termdash/canvas/braille"
|
||||
"github.com/mum4k/termdash/draw/segdisp/segment"
|
||||
)
|
||||
|
||||
// MustHV draws the segment or panics.
|
||||
func MustHV(bc *braille.Canvas, ar image.Rectangle, st segment.Type, opts ...segment.Option) {
|
||||
if err := segment.HV(bc, ar, st, opts...); err != nil {
|
||||
panic(fmt.Sprintf("segment.HV => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// MustDiagonal draws the segment or panics.
|
||||
func MustDiagonal(bc *braille.Canvas, ar image.Rectangle, width int, dt segment.DiagonalType, opts ...segment.DiagonalOption) {
|
||||
if err := segment.Diagonal(bc, ar, width, dt, opts...); err != nil {
|
||||
panic(fmt.Sprintf("segment.Diagonal => unexpected error: %v", err))
|
||||
}
|
||||
}
|
300
draw/segdisp/sixteen/attributes.go
Normal file
300
draw/segdisp/sixteen/attributes.go
Normal file
@ -0,0 +1,300 @@
|
||||
// Copyright 2019 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 sixteen
|
||||
|
||||
// attributes.go calculates attributes needed when determining placement of
|
||||
// segments.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"github.com/mum4k/termdash/draw/segdisp/segment"
|
||||
"github.com/mum4k/termdash/numbers"
|
||||
)
|
||||
|
||||
// hvSegType maps horizontal and vertical segments to their type.
|
||||
var hvSegType = map[Segment]segment.Type{
|
||||
A1: segment.Horizontal,
|
||||
A2: segment.Horizontal,
|
||||
B: segment.Vertical,
|
||||
C: segment.Vertical,
|
||||
D1: segment.Horizontal,
|
||||
D2: segment.Horizontal,
|
||||
E: segment.Vertical,
|
||||
F: segment.Vertical,
|
||||
G1: segment.Horizontal,
|
||||
G2: segment.Horizontal,
|
||||
J: segment.Vertical,
|
||||
M: segment.Vertical,
|
||||
}
|
||||
|
||||
// diaSegType maps diagonal segments to their type.
|
||||
var diaSegType = map[Segment]segment.DiagonalType{
|
||||
H: segment.LeftToRight,
|
||||
K: segment.RightToLeft,
|
||||
N: segment.RightToLeft,
|
||||
L: segment.LeftToRight,
|
||||
}
|
||||
|
||||
// segmentSize given an area for the display determines the size of individual
|
||||
// segments, i.e. the width of a vertical or the height of a horizontal
|
||||
// segment.
|
||||
func segmentSize(ar image.Rectangle) int {
|
||||
// widthPerc is the relative width of a segment to the width of the canvas.
|
||||
const widthPerc = 9
|
||||
s := int(numbers.Round(float64(ar.Dx()) * widthPerc / 100))
|
||||
if s > 3 && s%2 == 0 {
|
||||
// Segments with odd number of pixels in their width/height look
|
||||
// better, since the spike at the top of their slopes has only one
|
||||
// pixel.
|
||||
s++
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// attributes contains attributes needed to draw the segment display.
|
||||
// Refer to doc/segment_placement.svg for a visual aid and explanation of the
|
||||
// usage of the square roots.
|
||||
type attributes struct {
|
||||
// segSize is the width of a vertical or height of a horizontal segment.
|
||||
segSize int
|
||||
|
||||
// diaGap is the shortest distance between slopes on two neighboring
|
||||
// perpendicular segments.
|
||||
diaGap float64
|
||||
|
||||
// segPeakDist is the distance between the peak of the slope on a segment
|
||||
// and the point where the slope ends.
|
||||
segPeakDist float64
|
||||
|
||||
// diaLeg is the leg of a square whose hypotenuse is the diaGap.
|
||||
diaLeg float64
|
||||
|
||||
// peakToPeak is a horizontal or vertical distance between peaks of two
|
||||
// segments.
|
||||
peakToPeak int
|
||||
|
||||
// shortLen is length of the shorter segment, e.g. D1.
|
||||
shortLen int
|
||||
|
||||
// longLen is length of the longer segment, e.g. F.
|
||||
longLen int
|
||||
|
||||
// horizLeftX is the X coordinate where the area of the segment horizontally
|
||||
// on the left starts, i.e. X coordinate of F and E.
|
||||
horizLeftX int
|
||||
// horizMidX is the X coordinate where the area of the segment horizontally in
|
||||
// the middle starts, i.e. X coordinate of J and M.
|
||||
horizMidX int
|
||||
// horizRightX is the X coordinate where the area of the segment horizontally
|
||||
// on the right starts, i.e. X coordinate of B and C.
|
||||
horizRightX int
|
||||
|
||||
// vertCenY is the Y coordinate where the area of the segment vertically
|
||||
// in the center starts, i.e. Y coordinate of G1 and G2.
|
||||
vertCenY int
|
||||
// vertBotY is the Y coordinate where the area of the segment vertically
|
||||
// at the bottom starts, i.e. Y coordinate of D1 and D2.
|
||||
vertBotY int
|
||||
}
|
||||
|
||||
// newAttributes calculates attributes needed to place the segments for the
|
||||
// provided pixel area.
|
||||
func newAttributes(bcAr image.Rectangle) *attributes {
|
||||
segSize := segmentSize(bcAr)
|
||||
|
||||
// diaPerc is the size of the diaGap in percentage of the segment's size.
|
||||
const diaPerc = 40
|
||||
// Ensure there is at least one pixel diagonally between segments so they
|
||||
// don't visually blend.
|
||||
_, dg := numbers.MinMaxInts([]int{
|
||||
int(float64(segSize) * diaPerc / 100),
|
||||
1,
|
||||
})
|
||||
diaGap := float64(dg)
|
||||
|
||||
segLeg := float64(segSize) / math.Sqrt2
|
||||
segPeakDist := segLeg / math.Sqrt2
|
||||
|
||||
diaLeg := diaGap / math.Sqrt2
|
||||
peakToPeak := diaLeg * 2
|
||||
if segSize == 2 {
|
||||
// Display that has segment size of two looks more balanced with peak
|
||||
// distance of two.
|
||||
peakToPeak = 2
|
||||
}
|
||||
if peakToPeak > 3 && int(peakToPeak)%2 == 0 {
|
||||
// Prefer odd distances to create centered look.
|
||||
peakToPeak++
|
||||
}
|
||||
|
||||
twoSegHypo := 2*segLeg + diaGap
|
||||
twoSegLeg := twoSegHypo / math.Sqrt2
|
||||
edgeSegGap := twoSegLeg - segPeakDist
|
||||
|
||||
spaces := int(numbers.Round(2*edgeSegGap + peakToPeak))
|
||||
shortLen := (bcAr.Dx()-spaces)/2 - 1
|
||||
longLen := (bcAr.Dy()-spaces)/2 - 1
|
||||
|
||||
ptp := int(numbers.Round(peakToPeak))
|
||||
horizLeftX := int(numbers.Round(edgeSegGap))
|
||||
|
||||
// Refer to doc/segment_placement.svg.
|
||||
// Diagram labeled "A mid point".
|
||||
offset := int(numbers.Round(diaLeg - segPeakDist))
|
||||
horizMidX := horizLeftX + shortLen + offset
|
||||
horizRightX := horizLeftX + shortLen + ptp + shortLen + offset
|
||||
|
||||
vertCenY := horizLeftX + longLen + offset
|
||||
vertBotY := horizLeftX + longLen + ptp + longLen + offset
|
||||
|
||||
return &attributes{
|
||||
segSize: segSize,
|
||||
diaGap: diaGap,
|
||||
segPeakDist: segPeakDist,
|
||||
diaLeg: diaLeg,
|
||||
peakToPeak: ptp,
|
||||
shortLen: shortLen,
|
||||
longLen: longLen,
|
||||
|
||||
horizLeftX: horizLeftX,
|
||||
horizMidX: horizMidX,
|
||||
horizRightX: horizRightX,
|
||||
vertCenY: vertCenY,
|
||||
vertBotY: vertBotY,
|
||||
}
|
||||
}
|
||||
|
||||
// hvSegArea returns the area for the specified horizontal or vertical segment.
|
||||
func (a *attributes) hvSegArea(s Segment) image.Rectangle {
|
||||
var (
|
||||
start image.Point
|
||||
length int
|
||||
)
|
||||
|
||||
switch s {
|
||||
case A1:
|
||||
start = image.Point{a.horizLeftX, 0}
|
||||
length = a.shortLen
|
||||
|
||||
case A2:
|
||||
a1 := a.hvSegArea(A1)
|
||||
start = image.Point{a1.Max.X + a.peakToPeak, 0}
|
||||
length = a.shortLen
|
||||
|
||||
case F:
|
||||
start = image.Point{0, a.horizLeftX}
|
||||
length = a.longLen
|
||||
|
||||
case J:
|
||||
start = image.Point{a.horizMidX, a.horizLeftX}
|
||||
length = a.longLen
|
||||
|
||||
case B:
|
||||
start = image.Point{a.horizRightX, a.horizLeftX}
|
||||
length = a.longLen
|
||||
|
||||
case G1:
|
||||
start = image.Point{a.horizLeftX, a.vertCenY}
|
||||
length = a.shortLen
|
||||
|
||||
case G2:
|
||||
g1 := a.hvSegArea(G1)
|
||||
start = image.Point{g1.Max.X + a.peakToPeak, a.vertCenY}
|
||||
length = a.shortLen
|
||||
|
||||
case E:
|
||||
f := a.hvSegArea(F)
|
||||
start = image.Point{0, f.Max.Y + a.peakToPeak}
|
||||
length = a.longLen
|
||||
|
||||
case M:
|
||||
j := a.hvSegArea(J)
|
||||
start = image.Point{a.horizMidX, j.Max.Y + a.peakToPeak}
|
||||
length = a.longLen
|
||||
|
||||
case C:
|
||||
b := a.hvSegArea(B)
|
||||
start = image.Point{a.horizRightX, b.Max.Y + a.peakToPeak}
|
||||
length = a.longLen
|
||||
|
||||
case D1:
|
||||
start = image.Point{a.horizLeftX, a.vertBotY}
|
||||
length = a.shortLen
|
||||
|
||||
case D2:
|
||||
d1 := a.hvSegArea(D1)
|
||||
start = image.Point{d1.Max.X + a.peakToPeak, a.vertBotY}
|
||||
length = a.shortLen
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("cannot determine area for unknown horizontal or vertical segment %v(%d)", s, s))
|
||||
}
|
||||
|
||||
return a.hvArFromStart(start, s, length)
|
||||
}
|
||||
|
||||
// hvArFromStart given start coordinates of a segment, its length and its type,
|
||||
// determines its area.
|
||||
func (a *attributes) hvArFromStart(start image.Point, s Segment, length int) image.Rectangle {
|
||||
st := hvSegType[s]
|
||||
switch st {
|
||||
case segment.Horizontal:
|
||||
return image.Rect(start.X, start.Y, start.X+length, start.Y+a.segSize)
|
||||
case segment.Vertical:
|
||||
return image.Rect(start.X, start.Y, start.X+a.segSize, start.Y+length)
|
||||
default:
|
||||
panic(fmt.Sprintf("cannot create area for segment of unknown type %v(%d)", st, st))
|
||||
}
|
||||
}
|
||||
|
||||
// diaSegArea returns the area for the specified diagonal segment.
|
||||
func (a *attributes) diaSegArea(s Segment) image.Rectangle {
|
||||
switch s {
|
||||
case H:
|
||||
return a.diaBetween(A1, F, J, G1)
|
||||
case K:
|
||||
return a.diaBetween(A2, B, J, G2)
|
||||
case N:
|
||||
return a.diaBetween(G1, M, E, D1)
|
||||
case L:
|
||||
return a.diaBetween(G2, M, C, D2)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("cannot determine area for unknown diagonal segment %v(%d)", s, s))
|
||||
}
|
||||
}
|
||||
|
||||
// diaBetween given four segments (two horizontal and two vertical) returns the
|
||||
// area between them for a diagonal segment.
|
||||
func (a *attributes) diaBetween(top, left, right, bottom Segment) image.Rectangle {
|
||||
topAr := a.hvSegArea(top)
|
||||
leftAr := a.hvSegArea(left)
|
||||
rightAr := a.hvSegArea(right)
|
||||
bottomAr := a.hvSegArea(bottom)
|
||||
|
||||
// hvToDiaGapPerc is the size of gap between horizontal or vertical segment
|
||||
// and the diagonal segment between them in percentage of the diaGap.
|
||||
const hvToDiaGapPerc = 30
|
||||
hvToDiaGap := a.diaGap * hvToDiaGapPerc / 100
|
||||
|
||||
startX := int(numbers.Round(float64(topAr.Min.X) + a.segPeakDist - a.diaLeg + hvToDiaGap))
|
||||
startY := int(numbers.Round(float64(leftAr.Min.Y) + a.segPeakDist - a.diaLeg + hvToDiaGap))
|
||||
endX := int(numbers.Round(float64(bottomAr.Max.X) - a.segPeakDist + a.diaLeg - hvToDiaGap))
|
||||
endY := int(numbers.Round(float64(rightAr.Max.Y) - a.segPeakDist + a.diaLeg - hvToDiaGap))
|
||||
return image.Rect(startX, startY, endX, endY)
|
||||
}
|
BIN
draw/segdisp/sixteen/doc/16-Segment-ASCII-All.jpg
Normal file
BIN
draw/segdisp/sixteen/doc/16-Segment-ASCII-All.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 MiB |
BIN
draw/segdisp/sixteen/doc/segment_placement.graffle
Normal file
BIN
draw/segdisp/sixteen/doc/segment_placement.graffle
Normal file
Binary file not shown.
240
draw/segdisp/sixteen/doc/segment_placement.svg
Normal file
240
draw/segdisp/sixteen/doc/segment_placement.svg
Normal file
@ -0,0 +1,240 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="275 -49 1423 724" width="1423" height="724">
|
||||
<defs>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="black">
|
||||
<g>
|
||||
<path d="M 8 0 L 0 -3 L 0 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_2" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-9 -4 10 8" markerWidth="10" markerHeight="8" color="black">
|
||||
<g>
|
||||
<path d="M -8 0 L 0 3 L 0 -3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<font-face font-family="Helvetica Neue" font-size="18" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_3" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="#0432ff">
|
||||
<g>
|
||||
<path d="M 8 0 L 0 -3 L 0 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_4" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-9 -4 10 8" markerWidth="10" markerHeight="8" color="#0432ff">
|
||||
<g>
|
||||
<path d="M -8 0 L 0 3 L 0 -3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_5" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="#ff9300">
|
||||
<g>
|
||||
<path d="M 8 0 L 0 -3 L 0 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_6" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-9 -4 10 8" markerWidth="10" markerHeight="8" color="#ff9300">
|
||||
<g>
|
||||
<path d="M -8 0 L 0 3 L 0 -3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_7" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="#ff2600">
|
||||
<g>
|
||||
<path d="M 8 0 L 0 -3 L 0 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_8" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-9 -4 10 8" markerWidth="10" markerHeight="8" color="#ff2600">
|
||||
<g>
|
||||
<path d="M -8 0 L 0 3 L 0 -3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<font-face font-family="Helvetica Neue" font-size="24" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue-Bold"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
</defs>
|
||||
<metadata> Produced by OmniGraffle 7.7.1
|
||||
<dc:date>2019-02-01 03:54:59 +0000</dc:date>
|
||||
</metadata>
|
||||
<g id="Canvas_1" fill-opacity="1" stroke-dasharray="none" stroke="none" stroke-opacity="1" fill="none">
|
||||
<title>Canvas 1</title>
|
||||
<rect fill="white" x="275" y="-49" width="1423" height="724"/>
|
||||
<g id="Canvas_1: Layer 1">
|
||||
<title>Layer 1</title>
|
||||
<g id="Graphic_5">
|
||||
<path d="M 827.2404 82.2832 L 827.2404 415.3305 L 660.7168 248.80685 Z" fill="white"/>
|
||||
<path d="M 827.2404 82.2832 L 827.2404 415.3305 L 660.7168 248.80685 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_6">
|
||||
<path d="M 735.5434 507.02755 L 402.4961 507.02755 L 569.01974 340.5039 Z" fill="white"/>
|
||||
<path d="M 735.5434 507.02755 L 402.4961 507.02755 L 569.01974 340.5039 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Line_11">
|
||||
<line x1="653.7083" y1="255.99222" x2="576.0085" y2="333.5117" marker-end="url(#FilledArrow_Marker)" marker-start="url(#FilledArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_12">
|
||||
<text transform="translate(569.88274 291.24906) rotate(-42)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".331" y="17">diaGap</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_13">
|
||||
<path d="M 402.4961 498.1 L 402.4961 82 L 817.1 82.2766" marker-end="url(#FilledArrow_Marker)" marker-start="url(#FilledArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_14">
|
||||
<text transform="translate(530.51974 520.248)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".493" y="17">segWidth</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_15">
|
||||
<text transform="translate(846 238.05485)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".493" y="17">segWidth</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_16">
|
||||
<text transform="translate(445.6801 412.9409) rotate(-45)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".498" y="17">segLeg</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_17">
|
||||
<text transform="translate(694.684 169.1104) rotate(-45)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".498" y="17">segLeg</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_18">
|
||||
<text transform="translate(280 268.748)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".343" y="17">twoSegLeg</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_19">
|
||||
<text transform="translate(563.7168 35.248)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".343" y="17">twoSegLeg</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_20">
|
||||
<line x1="660.7168" y1="248" x2="659.10175" y2="92.07059" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Line_21">
|
||||
<line x1="415.5254" y1="506.0287" x2="825.9707" y2="98.97129" marker-end="url(#FilledArrow_Marker_3)" marker-start="url(#FilledArrow_Marker_4)" stroke="#0432ff" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_22">
|
||||
<text transform="translate(449.7741 481.0203) rotate(-45)" fill="#0432ff">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="#0432ff" x=".345" y="17">twoSegHypo</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_23">
|
||||
<line x1="412.39607" y1="95.22633" x2="649.22936" y2="94.75349" marker-end="url(#FilledArrow_Marker_5)" marker-start="url(#FilledArrow_Marker_6)" stroke="#ff9300" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_24">
|
||||
<text transform="translate(470 106.248)" fill="#ff9300">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="#ff9300" x=".499" y="17">edgeSegGap</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_25">
|
||||
<line x1="669.0326" y1="95.04715" x2="802.1" y2="95.00326" marker-end="url(#FilledArrow_Marker_7)" marker-start="url(#FilledArrow_Marker_8)" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_26">
|
||||
<text transform="translate(688 97.248)" fill="#ff2600">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="#ff2600" x=".163" y="17">segPeakDist</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_44">
|
||||
<path d="M 1530.2556 82.2832 L 1530.2556 415.3305 L 1363.732 248.80685 Z" fill="white"/>
|
||||
<path d="M 1530.2556 82.2832 L 1530.2556 415.3305 L 1363.732 248.80685 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_43">
|
||||
<path d="M 1438.5586 507.02755 L 1105.5113 507.02755 L 1272.035 340.5039 Z" fill="white"/>
|
||||
<path d="M 1438.5586 507.02755 L 1105.5113 507.02755 L 1272.035 340.5039 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Line_42">
|
||||
<line x1="1356.7235" y1="255.99222" x2="1279.0237" y2="333.5117" marker-end="url(#FilledArrow_Marker)" marker-start="url(#FilledArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_39">
|
||||
<text transform="translate(1233.535 520.248)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".493" y="17">segWidth</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_38">
|
||||
<text transform="translate(1549.0152 238.05485)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".493" y="17">segWidth</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_45">
|
||||
<path d="M 1019.0197 415.3305 L 1019.0197 82.2832 L 1185.5434 248.80685 Z" fill="white"/>
|
||||
<path d="M 1019.0197 415.3305 L 1019.0197 82.2832 L 1185.5434 248.80685 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Line_47">
|
||||
<line x1="1192.0877" y1="256.18496" x2="1265.2044" y2="333.31895" marker-end="url(#FilledArrow_Marker)" marker-start="url(#FilledArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_48">
|
||||
<text transform="translate(1300.1147 315.24906) rotate(-42)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".331" y="17">diaGap</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_49">
|
||||
<text transform="translate(1202.2761 274.18212) rotate(49)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".331" y="17">diaGap</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_50">
|
||||
<line x1="1185.2769" y1="249" x2="1353.832" y2="248.52774" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_51">
|
||||
<text transform="translate(1225.1377 213.748)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".324" y="17">peakToPeak</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_52">
|
||||
<line x1="1272.6088" y1="341.07775" x2="1273.2495" y2="248.75352" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_53">
|
||||
<text transform="translate(1285.5152 248.248)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".329" y="17">diaLeg</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_54">
|
||||
<line x1="1272.5152" y1="342" x2="1272.0449" y2="498.8071" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_55">
|
||||
<text transform="translate(1243.5152 425.248)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".163" y="17">segPeakDist</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_56">
|
||||
<text transform="translate(1141.6992 427.1104) rotate(-45)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".498" y="17">segLeg</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_57">
|
||||
<text transform="translate(1153.0152 480.248)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".163" y="17">segPeakDist</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_58">
|
||||
<line x1="819.9915" y1="423.4883" x2="742.2917" y2="501.0078" marker-end="url(#FilledArrow_Marker)" marker-start="url(#FilledArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_59">
|
||||
<text transform="translate(769.3827 488.24906) rotate(-42)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".331" y="17">diaGap</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_60">
|
||||
<path d="M 735.2832 508 L 735.2832 415.3305 L 817.1 415.3305" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4.0,4.0" stroke-width="1"/>
|
||||
</g>
|
||||
<g id="Graphic_61">
|
||||
<text transform="translate(746.5 415.248)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="18" font-weight="400" fill="black" x=".329" y="17">diaLeg</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_62">
|
||||
<text transform="translate(367.3584 -43.348083)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="24" font-weight="700" fill="black" x=".18" y="23">A corner - two segments facing each other.</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_63">
|
||||
<text transform="translate(1002.6377 -43.348083)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="24" font-weight="700" fill="black" x=".476" y="23">A mid point - three segments facing each other.</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 15 KiB |
472
draw/segdisp/sixteen/sixteen.go
Normal file
472
draw/segdisp/sixteen/sixteen.go
Normal file
@ -0,0 +1,472 @@
|
||||
// Copyright 2019 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 sixteen simulates a 16-segment display drawn on a canvas.
|
||||
|
||||
Given a canvas, determines the placement and size of the individual
|
||||
segments and exposes API that can turn individual segments on and off or
|
||||
display ASCII characters.
|
||||
|
||||
The following outlines segments in the display and their names.
|
||||
|
||||
A1 A2
|
||||
------- -------
|
||||
| \ | / |
|
||||
| \ | / |
|
||||
F | H J K | B
|
||||
| \ | / |
|
||||
| \ | / |
|
||||
-G1---- ----G2-
|
||||
| / | \ |
|
||||
| / | \ |
|
||||
E | N M L | C
|
||||
| / | \ |
|
||||
| / | \ |
|
||||
------- -------
|
||||
D1 D2
|
||||
*/
|
||||
package sixteen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"github.com/mum4k/termdash/area"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/braille"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw/segdisp/segment"
|
||||
)
|
||||
|
||||
// Segment represents a single segment in the display.
|
||||
type Segment int
|
||||
|
||||
// String implements fmt.Stringer()
|
||||
func (s Segment) String() string {
|
||||
if n, ok := segmentNames[s]; ok {
|
||||
return n
|
||||
}
|
||||
return "SegmentUnknown"
|
||||
}
|
||||
|
||||
// segmentNames maps Segment values to human readable names.
|
||||
var segmentNames = map[Segment]string{
|
||||
A1: "A1",
|
||||
A2: "A2",
|
||||
B: "B",
|
||||
C: "C",
|
||||
D1: "D1",
|
||||
D2: "D2",
|
||||
E: "E",
|
||||
F: "F",
|
||||
G1: "G1",
|
||||
G2: "G2",
|
||||
H: "H",
|
||||
J: "J",
|
||||
K: "K",
|
||||
L: "L",
|
||||
M: "M",
|
||||
N: "N",
|
||||
}
|
||||
|
||||
const (
|
||||
segmentUnknown Segment = iota
|
||||
|
||||
A1
|
||||
A2
|
||||
B
|
||||
C
|
||||
D1
|
||||
D2
|
||||
E
|
||||
F
|
||||
G1
|
||||
G2
|
||||
H
|
||||
J
|
||||
K
|
||||
L
|
||||
M
|
||||
N
|
||||
|
||||
segmentMax // Used for validation.
|
||||
)
|
||||
|
||||
// characterSegments maps characters that can be displayed on their segments.
|
||||
// See doc/16-Segment-ASCII-All.jpg and:
|
||||
// https://www.partsnotincluded.com/electronics/segmented-led-display-ascii-library
|
||||
var characterSegments = map[rune][]Segment{
|
||||
' ': nil,
|
||||
'!': {B, C},
|
||||
'"': {J, B},
|
||||
'#': {J, B, G1, G2, M, C, D1, D2},
|
||||
'$': {A1, A2, F, J, G1, G2, M, C, D1, D2},
|
||||
'%': {A1, F, J, K, G1, G2, N, M, C, D2},
|
||||
'&': {A1, H, J, G1, E, L, D1, D2},
|
||||
'\'': {J},
|
||||
'(': {K, L},
|
||||
')': {H, N},
|
||||
'*': {H, J, K, G1, G2, N, M, L},
|
||||
'+': {J, G1, G2, M},
|
||||
',': {N},
|
||||
'-': {G1, G2},
|
||||
'/': {N, K},
|
||||
|
||||
'0': {A1, A2, F, K, B, E, N, C, D1, D2},
|
||||
'1': {K, B, C},
|
||||
'2': {A1, A2, B, G1, G2, E, D1, D2},
|
||||
'3': {A1, A2, B, G2, C, D1, D2},
|
||||
'4': {F, B, G1, G2, C},
|
||||
'5': {A1, A2, F, G1, L, D1, D2},
|
||||
'6': {A1, A2, F, G1, G2, E, C, D1, D2},
|
||||
'7': {A1, A2, B, C},
|
||||
'8': {A1, A2, F, B, G1, G2, E, C, D1, D2},
|
||||
'9': {A1, A2, F, B, G1, G2, C, D1, D2},
|
||||
|
||||
':': {J, M},
|
||||
';': {J, N},
|
||||
'<': {K, G1, L},
|
||||
'=': {G1, G2, D1, D2},
|
||||
'>': {H, G2, N},
|
||||
'?': {A1, A2, B, G2, M},
|
||||
'@': {A1, A2, F, J, B, G2, E, D1, D2},
|
||||
|
||||
'A': {A1, A2, F, B, G1, G2, E, C},
|
||||
'B': {A1, A2, J, B, G2, M, C, D1, D2},
|
||||
'C': {A1, A2, F, E, D1, D2},
|
||||
'D': {A1, A2, J, B, M, C, D1, D2},
|
||||
'E': {A1, A2, F, G1, E, D1, D2},
|
||||
'F': {A1, A2, F, G1, E},
|
||||
'G': {A1, A2, F, G2, E, C, D1, D2},
|
||||
'H': {F, B, G1, G2, E, C},
|
||||
'I': {A1, A2, J, M, D1, D2},
|
||||
'J': {B, E, C, D1, D2},
|
||||
'K': {F, K, G1, E, L},
|
||||
'L': {F, E, D1, D2},
|
||||
'M': {F, H, K, B, E, C},
|
||||
'N': {F, H, B, E, L, C},
|
||||
'O': {A1, A2, F, B, E, C, D1, D2},
|
||||
'P': {A1, A2, F, B, G1, G2, E},
|
||||
'Q': {A1, A2, F, B, E, L, C, D1, D2},
|
||||
'R': {A1, A2, F, B, G1, G2, E, L},
|
||||
'S': {A1, A2, F, G1, G2, C, D1, D2},
|
||||
'T': {A1, A2, J, M},
|
||||
'U': {F, B, E, C, D1, D2},
|
||||
'V': {F, K, E, N},
|
||||
'W': {F, E, N, L, C, B},
|
||||
'X': {H, K, N, L},
|
||||
'Y': {F, B, G1, G2, C, D1, D2},
|
||||
'Z': {A1, A2, K, N, D1, D2},
|
||||
|
||||
'[': {A2, J, M, D2},
|
||||
'\\': {H, L},
|
||||
']': {A1, J, M, D1},
|
||||
'^': {N, L},
|
||||
'_': {D1, D2},
|
||||
'`': {H},
|
||||
|
||||
'a': {G1, E, M, D1, D2},
|
||||
'b': {F, G1, E, M, D1},
|
||||
'c': {G1, E, D1},
|
||||
'd': {B, G2, M, C, D2},
|
||||
'e': {G1, E, N, D1},
|
||||
'f': {A2, J, G1, G2, M},
|
||||
'g': {A1, F, J, G1, M, D1},
|
||||
'h': {F, G1, E, M},
|
||||
'i': {M},
|
||||
'j': {J, E, M, D1},
|
||||
'k': {J, K, M, L},
|
||||
'l': {F, E},
|
||||
'm': {G1, G2, E, M, C},
|
||||
'n': {G1, E, M},
|
||||
'o': {G1, E, M, D1},
|
||||
'p': {A1, F, J, G1, E},
|
||||
'q': {A1, F, J, G1, M},
|
||||
'r': {G1, E},
|
||||
's': {A1, F, G1, M, D1},
|
||||
't': {F, G1, E, D1},
|
||||
'u': {E, M, D1},
|
||||
'v': {E, N},
|
||||
'w': {E, N, L, C},
|
||||
'x': {H, K, N, L},
|
||||
'y': {J, B, G2, C, D2},
|
||||
'z': {G1, N, D1},
|
||||
|
||||
'{': {A2, J, G1, M, D2},
|
||||
'|': {J, M},
|
||||
'}': {A1, J, G2, M, D1},
|
||||
'~': {K, G1, G2, N},
|
||||
}
|
||||
|
||||
// SupportsChars asserts whether the display supports all runes in the
|
||||
// provided string.
|
||||
// The display only supports a subset of ASCII characters.
|
||||
// Returns any unsupported runes found in the string in an unspecified order.
|
||||
func SupportsChars(s string) (bool, []rune) {
|
||||
unsupp := map[rune]bool{}
|
||||
for _, r := range s {
|
||||
if _, ok := characterSegments[r]; !ok {
|
||||
unsupp[r] = true
|
||||
}
|
||||
}
|
||||
|
||||
var res []rune
|
||||
for r := range unsupp {
|
||||
res = append(res, r)
|
||||
}
|
||||
return len(res) == 0, res
|
||||
}
|
||||
|
||||
// Sanitize returns a copy of the string, replacing all unsupported characters
|
||||
// with a space character.
|
||||
func Sanitize(s string) string {
|
||||
var b bytes.Buffer
|
||||
for _, r := range s {
|
||||
if _, ok := characterSegments[r]; !ok {
|
||||
b.WriteRune(' ')
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// AllSegments returns all 16 segments in an undefined order.
|
||||
func AllSegments() []Segment {
|
||||
var res []Segment
|
||||
for s := range segmentNames {
|
||||
res = append(res, s)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Option is used to provide options.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
set(*Display)
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*Display)
|
||||
|
||||
// set implements Option.set.
|
||||
func (o option) set(d *Display) {
|
||||
o(d)
|
||||
}
|
||||
|
||||
// CellOpts sets the cell options on the cells that contain the segment display.
|
||||
func CellOpts(cOpts ...cell.Option) Option {
|
||||
return option(func(d *Display) {
|
||||
d.cellOpts = cOpts
|
||||
})
|
||||
}
|
||||
|
||||
// Display represents the segment display.
|
||||
// This object is not thread-safe.
|
||||
type Display struct {
|
||||
// segments maps segments to their current status.
|
||||
segments map[Segment]bool
|
||||
|
||||
cellOpts []cell.Option
|
||||
}
|
||||
|
||||
// New creates a new segment display.
|
||||
// Initially all the segments are off.
|
||||
func New(opts ...Option) *Display {
|
||||
d := &Display{
|
||||
segments: map[Segment]bool{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.set(d)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Clear clears the entire display, turning all segments off.
|
||||
func (d *Display) Clear(opts ...Option) {
|
||||
for _, opt := range opts {
|
||||
opt.set(d)
|
||||
}
|
||||
|
||||
d.segments = map[Segment]bool{}
|
||||
}
|
||||
|
||||
// SetSegment sets the specified segment on.
|
||||
// This method is idempotent.
|
||||
func (d *Display) SetSegment(s Segment) error {
|
||||
if s <= segmentUnknown || s >= segmentMax {
|
||||
return fmt.Errorf("unknown segment %v(%d)", s, s)
|
||||
}
|
||||
d.segments[s] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearSegment sets the specified segment off.
|
||||
// This method is idempotent.
|
||||
func (d *Display) ClearSegment(s Segment) error {
|
||||
if s <= segmentUnknown || s >= segmentMax {
|
||||
return fmt.Errorf("unknown segment %v(%d)", s, s)
|
||||
}
|
||||
d.segments[s] = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleSegment toggles the state of the specified segment, i.e it either sets
|
||||
// or clears it depending on its current state.
|
||||
func (d *Display) ToggleSegment(s Segment) error {
|
||||
if s <= segmentUnknown || s >= segmentMax {
|
||||
return fmt.Errorf("unknown segment %v(%d)", s, s)
|
||||
}
|
||||
if d.segments[s] {
|
||||
d.segments[s] = false
|
||||
} else {
|
||||
d.segments[s] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Character sets all the segments that are needed to display the provided character.
|
||||
// The display only supports a subset of ASCII characters, use SupportsChars()
|
||||
// or Sanitize() to ensure the provided character is supported.
|
||||
// Doesn't clear the display of segments set previously.
|
||||
func (d *Display) SetCharacter(c rune) error {
|
||||
seg, ok := characterSegments[c]
|
||||
if !ok {
|
||||
return fmt.Errorf("display doesn't support character %q rune(%v)", c, c)
|
||||
}
|
||||
|
||||
for _, s := range seg {
|
||||
if err := d.SetSegment(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Minimum valid size of a cell canvas in order to draw the segment display.
|
||||
const (
|
||||
// MinCols is the smallest valid amount of columns in a cell area.
|
||||
MinCols = 6
|
||||
// MinRowPixels is the smallest valid amount of rows in a cell area.
|
||||
MinRows = 5
|
||||
)
|
||||
|
||||
// aspectRatio is the desired aspect ratio of a single segment display.
|
||||
var aspectRatio = image.Point{3, 5}
|
||||
|
||||
// Draw draws the current state of the segment display onto the canvas.
|
||||
// The canvas must be at least MinCols x MinRows cells, or an error will be
|
||||
// returned.
|
||||
// Any options provided to draw overwrite the values provided to New.
|
||||
func (d *Display) Draw(cvs *canvas.Canvas, opts ...Option) error {
|
||||
for _, o := range opts {
|
||||
o.set(d)
|
||||
}
|
||||
|
||||
bc, bcAr, err := toBraille(cvs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attr := newAttributes(bcAr)
|
||||
var sOpts []segment.Option
|
||||
if len(d.cellOpts) > 0 {
|
||||
sOpts = append(sOpts, segment.CellOpts(d.cellOpts...))
|
||||
}
|
||||
for _, segArg := range []struct {
|
||||
s Segment
|
||||
opts []segment.Option
|
||||
}{
|
||||
{A1, nil},
|
||||
{A2, nil},
|
||||
|
||||
{F, nil},
|
||||
{J, []segment.Option{segment.SkipSlopesLTE(2)}},
|
||||
{B, []segment.Option{segment.ReverseSlopes()}},
|
||||
|
||||
{G1, []segment.Option{segment.SkipSlopesLTE(2)}},
|
||||
{G2, []segment.Option{segment.SkipSlopesLTE(2)}},
|
||||
|
||||
{E, nil},
|
||||
{M, []segment.Option{segment.SkipSlopesLTE(2)}},
|
||||
{C, []segment.Option{segment.ReverseSlopes()}},
|
||||
|
||||
{D1, []segment.Option{segment.ReverseSlopes()}},
|
||||
{D2, []segment.Option{segment.ReverseSlopes()}},
|
||||
} {
|
||||
if !d.segments[segArg.s] {
|
||||
continue
|
||||
}
|
||||
sOpts := append(sOpts, segArg.opts...)
|
||||
ar := attr.hvSegArea(segArg.s)
|
||||
if err := segment.HV(bc, ar, hvSegType[segArg.s], sOpts...); err != nil {
|
||||
return fmt.Errorf("failed to draw segment %v, segment.HV => %v", segArg.s, err)
|
||||
}
|
||||
}
|
||||
|
||||
var dsOpts []segment.DiagonalOption
|
||||
if len(d.cellOpts) > 0 {
|
||||
dsOpts = append(dsOpts, segment.DiagonalCellOpts(d.cellOpts...))
|
||||
}
|
||||
for _, seg := range []Segment{H, K, N, L} {
|
||||
if !d.segments[seg] {
|
||||
continue
|
||||
}
|
||||
ar := attr.diaSegArea(seg)
|
||||
if err := segment.Diagonal(bc, ar, attr.segSize, diaSegType[seg], dsOpts...); err != nil {
|
||||
return fmt.Errorf("failed to draw segment %v, segment.Diagonal => %v", seg, err)
|
||||
}
|
||||
}
|
||||
return bc.CopyTo(cvs)
|
||||
}
|
||||
|
||||
// Required, when given an area of cells, returns either an area of the same
|
||||
// size or a smaller area that is required to draw one display.
|
||||
// Returns a smaller area when the provided area didn't have the required
|
||||
// aspect ratio.
|
||||
// Returns an error if the area is too small to draw a segment display, i.e.
|
||||
// smaller than MinCols x MinRows.
|
||||
func Required(cellArea image.Rectangle) (image.Rectangle, error) {
|
||||
if cols, rows := cellArea.Dx(), cellArea.Dy(); cols < MinCols || rows < MinRows {
|
||||
return image.ZR, fmt.Errorf("cell area %v is too small to draw the segment display, has %dx%d cells, need at least %dx%d cells",
|
||||
cellArea, cols, rows, MinCols, MinRows)
|
||||
}
|
||||
|
||||
bcAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Max.X*braille.ColMult, cellArea.Max.Y*braille.RowMult)
|
||||
bcArAdj := area.WithRatio(bcAr, aspectRatio)
|
||||
|
||||
needCols := int(math.Ceil(float64(bcArAdj.Dx()) / braille.ColMult))
|
||||
needRows := int(math.Ceil(float64(bcArAdj.Dy()) / braille.RowMult))
|
||||
needAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Min.X+needCols, cellArea.Min.Y+needRows)
|
||||
return needAr, nil
|
||||
}
|
||||
|
||||
// toBraille converts the canvas into a braille canvas and returns a pixel area
|
||||
// with aspect ratio adjusted for the segment display.
|
||||
func toBraille(cvs *canvas.Canvas) (*braille.Canvas, image.Rectangle, error) {
|
||||
ar, err := Required(cvs.Area())
|
||||
if err != nil {
|
||||
return nil, image.ZR, fmt.Errorf("Required => %v", err)
|
||||
}
|
||||
|
||||
bc, err := braille.New(ar)
|
||||
if err != nil {
|
||||
return nil, image.ZR, fmt.Errorf("braille.New => %v", err)
|
||||
}
|
||||
return bc, area.WithRatio(bc.Area(), aspectRatio), nil
|
||||
}
|
1766
draw/segdisp/sixteen/sixteen_test.go
Normal file
1766
draw/segdisp/sixteen/sixteen_test.go
Normal file
File diff suppressed because it is too large
Load Diff
37
draw/segdisp/sixteen/testsixteen/testsixteen.go
Normal file
37
draw/segdisp/sixteen/testsixteen/testsixteen.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2019 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 testsixteen provides helpers for tests that use the sixteen package.
|
||||
package testsixteen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen"
|
||||
)
|
||||
|
||||
// MustSetCharacter sets the character on the display or panics.
|
||||
func MustSetCharacter(d *sixteen.Display, c rune) {
|
||||
if err := d.SetCharacter(c); err != nil {
|
||||
panic(fmt.Errorf("sixteen.Display.SetCharacter => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// MustDraw draws the display onto the canvas or panics.
|
||||
func MustDraw(d *sixteen.Display, cvs *canvas.Canvas, opts ...sixteen.Option) {
|
||||
if err := d.Draw(cvs, opts...); err != nil {
|
||||
panic(fmt.Errorf("sixteen.Display.Draw => unexpected error: %v", err))
|
||||
}
|
||||
}
|
BIN
images/segmentdisplaydemo.gif
Normal file
BIN
images/segmentdisplaydemo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 MiB |
@ -162,3 +162,11 @@ func RadiansToDegrees(radians float64) int {
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Abs returns the absolute value of x.
|
||||
func Abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
@ -327,3 +327,25 @@ func TestRadiansToDegrees(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbs(t *testing.T) {
|
||||
tests := []struct {
|
||||
input int
|
||||
want int
|
||||
}{
|
||||
{0, 0},
|
||||
{1, 1},
|
||||
{2, 2},
|
||||
{-1, 1},
|
||||
{-2, 2},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(fmt.Sprintf("%d", tc.input), func(t *testing.T) {
|
||||
got := Abs(tc.input)
|
||||
if got != tc.want {
|
||||
t.Errorf("Abs(%d) => %v, want %v", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
112
widgets/segmentdisplay/options.go
Normal file
112
widgets/segmentdisplay/options.go
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright 2019 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 segmentdisplay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
)
|
||||
|
||||
// options.go contains configurable options for SegmentDisplay.
|
||||
|
||||
// Option is used to provide options.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
set(*options)
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*options)
|
||||
|
||||
// set implements Option.set.
|
||||
func (o option) set(opts *options) {
|
||||
o(opts)
|
||||
}
|
||||
|
||||
// options holds the provided options.
|
||||
type options struct {
|
||||
hAlign align.Horizontal
|
||||
vAlign align.Vertical
|
||||
maximizeSegSize bool
|
||||
gapPercent int
|
||||
}
|
||||
|
||||
// validate validates the provided options.
|
||||
func (o *options) validate() error {
|
||||
if min, max := 0, 100; o.gapPercent < min || o.gapPercent > max {
|
||||
return fmt.Errorf("invalid GapPercent %d, must be %d <= value <= %d", o.gapPercent, min, max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newOptions returns options with the default values set.
|
||||
func newOptions() *options {
|
||||
return &options{
|
||||
hAlign: align.HorizontalCenter,
|
||||
vAlign: align.VerticalMiddle,
|
||||
gapPercent: DefaultGapPercent,
|
||||
}
|
||||
}
|
||||
|
||||
// AlignHorizontal sets the horizontal alignment for the individual display
|
||||
// segments. Defaults to alignment in the center.
|
||||
func AlignHorizontal(h align.Horizontal) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.hAlign = h
|
||||
})
|
||||
}
|
||||
|
||||
// AlignVertical sets the vertical alignment for the individual display
|
||||
// segments. Defaults to alignment in the middle
|
||||
func AlignVertical(v align.Vertical) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.vAlign = v
|
||||
})
|
||||
}
|
||||
|
||||
// MaximizeSegmentHeight tells the widget to maximize the height of the
|
||||
// individual display segments.
|
||||
// When this option is set and the user has provided more text than we can fit
|
||||
// on the canvas, the widget will prefer to maximize height of individual
|
||||
// characters which will result in earlier trimming of the text.
|
||||
func MaximizeSegmentHeight() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.maximizeSegSize = true
|
||||
})
|
||||
}
|
||||
|
||||
// MaximizeDisplayedText tells the widget to maximize the amount of characters
|
||||
// that are displayed.
|
||||
// When this option is set and the user has provided more text than we can fit
|
||||
// on the canvas, the widget will prefer to decrease the height of individual
|
||||
// characters and fit more of them on the canvas.
|
||||
// This is the default behavior.
|
||||
func MaximizeDisplayedText() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.maximizeSegSize = false
|
||||
})
|
||||
}
|
||||
|
||||
// DefaultGapPercent is the default value for the GapPercent option.
|
||||
const DefaultGapPercent = 20
|
||||
|
||||
// GapPercent sets the size of the horizontal gap between individual segments
|
||||
// (characters) expressed as a percentage of the segment height.
|
||||
func GapPercent(perc int) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.gapPercent = perc
|
||||
})
|
||||
}
|
115
widgets/segmentdisplay/segment_area.go
Normal file
115
widgets/segmentdisplay/segment_area.go
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright 2019 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 segmentdisplay
|
||||
|
||||
// segment_area.go contains code that determines how many segments we can fit
|
||||
// in the canvas.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen"
|
||||
)
|
||||
|
||||
// segArea contains information about the area that will contain the segments.
|
||||
type segArea struct {
|
||||
// segment is the area for one segment.
|
||||
segment image.Rectangle
|
||||
// canFit is the number of segments we can fit on the canvas.
|
||||
canFit int
|
||||
// gapPixels is the size of gaps between segments in pixels.
|
||||
gapPixels int
|
||||
// gaps is the number of gaps that will be drawn.
|
||||
gaps int
|
||||
}
|
||||
|
||||
// needArea returns the complete area required for all the segments that we can
|
||||
// fit and any gaps.
|
||||
func (sa *segArea) needArea() image.Rectangle {
|
||||
return image.Rect(
|
||||
0,
|
||||
0,
|
||||
sa.segment.Dx()*sa.canFit+sa.gaps*sa.gapPixels,
|
||||
sa.segment.Dy(),
|
||||
)
|
||||
}
|
||||
|
||||
// newSegArea calculates the area for segments given available canvas area,
|
||||
// length of the text to be displayed and the size of gap between segments
|
||||
func newSegArea(cvsAr image.Rectangle, textLen, gapPercent int) (*segArea, error) {
|
||||
segAr, err := sixteen.Required(cvsAr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sixteen.Required => %v", err)
|
||||
}
|
||||
gapPixels := segAr.Dy() * gapPercent / 100
|
||||
|
||||
var (
|
||||
gaps int
|
||||
canFit int
|
||||
taken int
|
||||
)
|
||||
for i := 0; i < textLen; i++ {
|
||||
taken += segAr.Dx()
|
||||
|
||||
if taken > cvsAr.Dx() {
|
||||
break
|
||||
}
|
||||
canFit++
|
||||
|
||||
// Don't insert gaps after the last segment in the text or the last
|
||||
// segment we can fit.
|
||||
if gapPixels == 0 || i == textLen-1 {
|
||||
continue
|
||||
}
|
||||
|
||||
remaining := cvsAr.Dx() - taken
|
||||
// Only insert gaps if we can still fit one more segment with the gap.
|
||||
if remaining >= gapPixels+segAr.Dx() {
|
||||
taken += gapPixels
|
||||
gaps++
|
||||
} else {
|
||||
// Gap is needed but doesn't fit together with the next segment.
|
||||
// So insert neither.
|
||||
break
|
||||
}
|
||||
}
|
||||
return &segArea{
|
||||
segment: segAr,
|
||||
canFit: canFit,
|
||||
gapPixels: gapPixels,
|
||||
gaps: gaps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// maximizeFit finds the largest individual segment size that enables us to fit
|
||||
// the most characters onto a canvas with the provided area. Returns the area
|
||||
// required for a single segment and the number of segments we can fit.
|
||||
func maximizeFit(cvsAr image.Rectangle, textLen, gapPercent int) (*segArea, error) {
|
||||
var bestSegAr *segArea
|
||||
for height := cvsAr.Dy(); height >= sixteen.MinRows; height-- {
|
||||
cvsAr := image.Rect(cvsAr.Min.X, cvsAr.Min.Y, cvsAr.Max.X, cvsAr.Min.Y+height)
|
||||
segAr, err := newSegArea(cvsAr, textLen, gapPercent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if segAr.canFit >= textLen {
|
||||
return segAr, nil
|
||||
}
|
||||
bestSegAr = segAr
|
||||
}
|
||||
return bestSegAr, nil
|
||||
}
|
264
widgets/segmentdisplay/segmentdisplay.go
Normal file
264
widgets/segmentdisplay/segmentdisplay.go
Normal file
@ -0,0 +1,264 @@
|
||||
// Copyright 2019 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 segmentdisplay is a widget that displays text by simulating a
|
||||
// segment display.
|
||||
package segmentdisplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/attrrange"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
// SegmentDisplay displays ASCII content by simulating a segment display.
|
||||
//
|
||||
// Automatically determines the size of individual segments with goal of
|
||||
// maximizing the segment size or with fitting the entire text depending on the
|
||||
// provided options.
|
||||
//
|
||||
// Segment displays support only a subset of ASCII characters, provided options
|
||||
// determine the behavior when an unsupported character is encountered.
|
||||
//
|
||||
// Implements widgetapi.Widget. This object is thread-safe.
|
||||
type SegmentDisplay struct {
|
||||
// buff contains the text to be displayed.
|
||||
buff bytes.Buffer
|
||||
|
||||
// givenWOpts are write options given for the text in buff.
|
||||
givenWOpts []*writeOptions
|
||||
// wOptsTracker tracks the positions in a buff to which the givenWOpts apply.
|
||||
wOptsTracker *attrrange.Tracker
|
||||
|
||||
// mu protects the widget.
|
||||
mu sync.Mutex
|
||||
|
||||
// opts are the provided options.
|
||||
opts *options
|
||||
}
|
||||
|
||||
// New returns a new SegmentDisplay.
|
||||
func New(opts ...Option) (*SegmentDisplay, error) {
|
||||
opt := newOptions()
|
||||
for _, o := range opts {
|
||||
o.set(opt)
|
||||
}
|
||||
if err := opt.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SegmentDisplay{
|
||||
wOptsTracker: attrrange.NewTracker(),
|
||||
opts: opt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TextChunk is a part of or the full text that will be displayed.
|
||||
type TextChunk struct {
|
||||
text string
|
||||
wOpts *writeOptions
|
||||
}
|
||||
|
||||
// NewChunk creates a new text chunk.
|
||||
func NewChunk(text string, wOpts ...WriteOption) *TextChunk {
|
||||
return &TextChunk{
|
||||
text: text,
|
||||
wOpts: newWriteOptions(wOpts...),
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes text for the widget to display. Subsequent calls replace text
|
||||
// written previously. All the provided text chunks are broken into characters
|
||||
// and each character is displayed in one segment.
|
||||
//
|
||||
// The provided write options determine the behavior when text contains
|
||||
// unsupported characters and set cell options for cells that contain
|
||||
// individual display segments.
|
||||
//
|
||||
// Each of the text chunks can have its own options. At least one chunk must be
|
||||
// specified.
|
||||
//
|
||||
// Any provided options override options given to New.
|
||||
func (sd *SegmentDisplay) Write(chunks []*TextChunk, opts ...Option) error {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
|
||||
for _, o := range opts {
|
||||
o.set(sd.opts)
|
||||
}
|
||||
if err := sd.opts.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(chunks) == 0 {
|
||||
return errors.New("at least one text chunk must be specified")
|
||||
}
|
||||
sd.reset()
|
||||
|
||||
for i, tc := range chunks {
|
||||
if tc.text == "" {
|
||||
return fmt.Errorf("text chunk[%d] is empty, all chunks must contains some text", i)
|
||||
}
|
||||
if ok, badRunes := sixteen.SupportsChars(tc.text); !ok && tc.wOpts.errOnUnsupported {
|
||||
return fmt.Errorf("text chunk[%d] contains unsupported characters %v, clean the text or provide the WriteSanitize option", i, badRunes)
|
||||
}
|
||||
text := sixteen.Sanitize(tc.text)
|
||||
|
||||
pos := sd.buff.Len()
|
||||
sd.givenWOpts = append(sd.givenWOpts, tc.wOpts)
|
||||
wOptsIdx := len(sd.givenWOpts) - 1
|
||||
if err := sd.wOptsTracker.Add(pos, pos+len(text), wOptsIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
sd.buff.WriteString(text)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset resets the widget back to empty content.
|
||||
func (sd *SegmentDisplay) Reset() {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
sd.reset()
|
||||
}
|
||||
|
||||
// reset is the implementation of Reset.
|
||||
// Caller must hold sd.mu.
|
||||
func (sd *SegmentDisplay) reset() {
|
||||
sd.buff.Reset()
|
||||
sd.givenWOpts = nil
|
||||
sd.wOptsTracker = attrrange.NewTracker()
|
||||
}
|
||||
|
||||
// preprocess determines the size of individual segments maximizing their
|
||||
// height or the amount of displayed characters based on the specified options.
|
||||
// Returns the area required for a single segment, the text that we can fit and
|
||||
// size of gaps between segments in cells.
|
||||
func (sd *SegmentDisplay) preprocess(cvsAr image.Rectangle) (*segArea, error) {
|
||||
textLen := sd.buff.Len() // We're guaranteed by Write to only have ASCII characters.
|
||||
segAr, err := newSegArea(cvsAr, textLen, sd.opts.gapPercent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
need := sd.buff.Len()
|
||||
if need <= segAr.canFit || sd.opts.maximizeSegSize {
|
||||
return segAr, nil
|
||||
}
|
||||
|
||||
bestAr, err := maximizeFit(cvsAr, textLen, sd.opts.gapPercent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bestAr, nil
|
||||
}
|
||||
|
||||
// Draw draws the SegmentDisplay widget onto the canvas.
|
||||
// Implements widgetapi.Widget.Draw.
|
||||
func (sd *SegmentDisplay) Draw(cvs *canvas.Canvas) error {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
|
||||
if sd.buff.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
segAr, err := sd.preprocess(cvs.Area())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
text := sd.buff.String()
|
||||
aligned, err := align.Rectangle(cvs.Area(), segAr.needArea(), sd.opts.hAlign, sd.opts.vAlign)
|
||||
if err != nil {
|
||||
return fmt.Errorf("align.Rectangle => %v", err)
|
||||
}
|
||||
|
||||
optRange, err := sd.wOptsTracker.ForPosition(0) // Text options for the current byte.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gaps := segAr.gaps
|
||||
startX := aligned.Min.X
|
||||
for i, c := range text {
|
||||
if i >= segAr.canFit {
|
||||
break
|
||||
}
|
||||
|
||||
disp := sixteen.New()
|
||||
if err := disp.SetCharacter(c); err != nil {
|
||||
return fmt.Errorf("disp.SetCharacter => %v", err)
|
||||
}
|
||||
|
||||
endX := startX + segAr.segment.Dx()
|
||||
ar := image.Rect(startX, aligned.Min.Y, endX, aligned.Max.Y)
|
||||
startX = endX
|
||||
if gaps > 0 {
|
||||
startX += segAr.gapPixels
|
||||
gaps--
|
||||
}
|
||||
|
||||
dCvs, err := canvas.New(ar)
|
||||
if err != nil {
|
||||
return fmt.Errorf("canvas.New => %v", err)
|
||||
}
|
||||
|
||||
if i >= optRange.High { // Get the next write options.
|
||||
or, err := sd.wOptsTracker.ForPosition(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
optRange = or
|
||||
}
|
||||
wOpts := sd.givenWOpts[optRange.AttrIdx]
|
||||
|
||||
if err := disp.Draw(dCvs, sixteen.CellOpts(wOpts.cellOpts...)); err != nil {
|
||||
return fmt.Errorf("disp.Draw => %v", err)
|
||||
}
|
||||
|
||||
if err := dCvs.CopyTo(cvs); err != nil {
|
||||
return fmt.Errorf("dCvs.CopyTo => %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keyboard input isn't supported on the SegmentDisplay widget.
|
||||
func (*SegmentDisplay) Keyboard(k *terminalapi.Keyboard) error {
|
||||
return errors.New("the SegmentDisplay widget doesn't support keyboard events")
|
||||
}
|
||||
|
||||
// Mouse input isn't supported on the SegmentDisplay widget.
|
||||
func (*SegmentDisplay) Mouse(m *terminalapi.Mouse) error {
|
||||
return errors.New("the SegmentDisplay widget doesn't support mouse events")
|
||||
}
|
||||
|
||||
// Options implements widgetapi.Widget.Options.
|
||||
func (sd *SegmentDisplay) Options() widgetapi.Options {
|
||||
return widgetapi.Options{
|
||||
// The smallest supported size of a display segment.
|
||||
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
||||
WantKeyboard: false,
|
||||
WantMouse: false,
|
||||
}
|
||||
}
|
831
widgets/segmentdisplay/segmentdisplay_test.go
Normal file
831
widgets/segmentdisplay/segmentdisplay_test.go
Normal file
@ -0,0 +1,831 @@
|
||||
// Copyright 2019 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 segmentdisplay
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/testcanvas"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen"
|
||||
"github.com/mum4k/termdash/draw/segdisp/sixteen/testsixteen"
|
||||
"github.com/mum4k/termdash/terminal/faketerm"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
// mustDrawChar draws the provided character in the area of the canvas or panics.
|
||||
func mustDrawChar(cvs *canvas.Canvas, char rune, ar image.Rectangle, opts ...sixteen.Option) {
|
||||
d := sixteen.New()
|
||||
testsixteen.MustSetCharacter(d, char)
|
||||
c := testcanvas.MustNew(ar)
|
||||
testsixteen.MustDraw(d, c, opts...)
|
||||
testcanvas.MustCopyTo(c, cvs)
|
||||
}
|
||||
|
||||
func TestSegmentDisplay(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
opts []Option
|
||||
update func(*SegmentDisplay) error // update gets called before drawing of the widget.
|
||||
canvas image.Rectangle
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantNewErr bool
|
||||
wantUpdateErr bool // whether to expect an error on a call to the update function
|
||||
wantDrawErr bool
|
||||
}{
|
||||
{
|
||||
desc: "New fails on invalid GapPercent (too low)",
|
||||
opts: []Option{
|
||||
GapPercent(-1),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
wantNewErr: true,
|
||||
},
|
||||
{
|
||||
desc: "New fails on invalid GapPercent (too high)",
|
||||
opts: []Option{
|
||||
GapPercent(101),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
wantNewErr: true,
|
||||
},
|
||||
{
|
||||
desc: "write fails on invalid GapPercent (too low)",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(
|
||||
[]*TextChunk{NewChunk("1")},
|
||||
GapPercent(-1),
|
||||
)
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "write fails on invalid GapPercent (too high)",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(
|
||||
[]*TextChunk{NewChunk("1")},
|
||||
GapPercent(101),
|
||||
)
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on area too small for a segment",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols-1, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("1")})
|
||||
},
|
||||
wantDrawErr: true,
|
||||
},
|
||||
{
|
||||
desc: "write fails without chunks",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(nil)
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "write fails with an empty chunk",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("")})
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "write fails on unsupported characters when requested",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk(".", WriteErrOnUnsupported())})
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "draws empty without text",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, all fit exactly",
|
||||
opts: []Option{
|
||||
GapPercent(0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("123")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
|
||||
{'2', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows)},
|
||||
{'3', image.Rect(sixteen.MinCols*2, 0, sixteen.MinCols*3, sixteen.MinRows)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "write sanitizes text by default",
|
||||
opts: []Option{
|
||||
GapPercent(0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk(".1")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "write sanitizes text with option",
|
||||
opts: []Option{
|
||||
GapPercent(0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk(".1", WriteSanitize())})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment vertical middle by default",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("1")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "subsequent calls to write overwrite previous text",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
|
||||
return err
|
||||
}
|
||||
return sd.Write([]*TextChunk{NewChunk("4")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '4', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets cell options per text chunk",
|
||||
opts: []Option{
|
||||
GapPercent(0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(
|
||||
[]*TextChunk{
|
||||
NewChunk("1", WriteCellOpts(
|
||||
cell.FgColor(cell.ColorRed),
|
||||
cell.BgColor(cell.ColorBlue),
|
||||
)),
|
||||
NewChunk("2", WriteCellOpts(
|
||||
cell.FgColor(cell.ColorGreen),
|
||||
cell.BgColor(cell.ColorYellow),
|
||||
)),
|
||||
})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(
|
||||
cvs, '1',
|
||||
image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
sixteen.CellOpts(
|
||||
cell.FgColor(cell.ColorRed),
|
||||
cell.BgColor(cell.ColorBlue),
|
||||
),
|
||||
)
|
||||
mustDrawChar(
|
||||
cvs, '2',
|
||||
image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
sixteen.CellOpts(
|
||||
cell.FgColor(cell.ColorGreen),
|
||||
cell.BgColor(cell.ColorYellow),
|
||||
),
|
||||
)
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "reset resets the text content",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
|
||||
return err
|
||||
}
|
||||
sd.Reset()
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "reset resets provided cell options",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
if err := sd.Write(
|
||||
[]*TextChunk{
|
||||
NewChunk("1", WriteCellOpts(
|
||||
cell.FgColor(cell.ColorRed),
|
||||
cell.BgColor(cell.ColorBlue),
|
||||
)),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
sd.Reset()
|
||||
return sd.Write([]*TextChunk{NewChunk("1")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment vertical middle with option",
|
||||
opts: []Option{
|
||||
AlignVertical(align.VerticalMiddle),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("1")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 1, sixteen.MinCols, sixteen.MinRows+1))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment vertical top with option",
|
||||
opts: []Option{
|
||||
AlignVertical(align.VerticalTop),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("1")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "options given to Write override those given to New so aligns top",
|
||||
opts: []Option{
|
||||
AlignVertical(align.VerticalBottom),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write(
|
||||
[]*TextChunk{NewChunk("1")},
|
||||
AlignVertical(align.VerticalTop),
|
||||
)
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment vertical bottom with option",
|
||||
opts: []Option{
|
||||
AlignVertical(align.VerticalBottom),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("1")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '1', image.Rect(0, 2, sixteen.MinCols, sixteen.MinRows+2))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment horizontal center by default",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("8")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '8', image.Rect(1, 0, sixteen.MinCols+1, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment horizontal center with option",
|
||||
opts: []Option{
|
||||
AlignHorizontal(align.HorizontalCenter),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("8")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '8', image.Rect(1, 0, sixteen.MinCols+1, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment horizontal left with option",
|
||||
opts: []Option{
|
||||
AlignHorizontal(align.HorizontalLeft),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("8")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '8', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "aligns segment horizontal right with option",
|
||||
opts: []Option{
|
||||
AlignHorizontal(align.HorizontalRight),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("8")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
mustDrawChar(cvs, '8', image.Rect(2, 0, sixteen.MinCols+2, sixteen.MinRows))
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, not enough space, maximizes segment height with option",
|
||||
opts: []Option{
|
||||
MaximizeSegmentHeight(),
|
||||
GapPercent(0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("123")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
|
||||
{'2', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, not enough space, maximizes displayed text by default and fits all",
|
||||
opts: []Option{
|
||||
GapPercent(0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("123")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 7, 6, 12)},
|
||||
{'2', image.Rect(6, 7, 12, 12)},
|
||||
{'3', image.Rect(12, 7, 18, 12)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, not enough space, maximizes displayed text but cannot fit all",
|
||||
opts: []Option{
|
||||
GapPercent(0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("1234")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 7, 6, 12)},
|
||||
{'2', image.Rect(6, 7, 12, 12)},
|
||||
{'3', image.Rect(12, 7, 18, 12)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments, not enough space, maximizes displayed text with option",
|
||||
opts: []Option{
|
||||
MaximizeDisplayedText(),
|
||||
GapPercent(0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows*4),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("123")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 7, 6, 12)},
|
||||
{'2', image.Rect(6, 7, 12, 12)},
|
||||
{'3', image.Rect(12, 7, 18, 12)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments with a gap by default",
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("123")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
|
||||
{'2', image.Rect(sixteen.MinCols+1, 0, sixteen.MinCols*2+1, sixteen.MinRows)},
|
||||
{'3', image.Rect(sixteen.MinCols*2+2, 0, sixteen.MinCols*3+2, sixteen.MinRows)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments with a gap, exact fit",
|
||||
opts: []Option{
|
||||
GapPercent(20),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("123")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
|
||||
{'2', image.Rect(sixteen.MinCols+1, 0, sixteen.MinCols*2+1, sixteen.MinRows)},
|
||||
{'3', image.Rect(sixteen.MinCols*2+2, 0, sixteen.MinCols*3+2, sixteen.MinRows)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments with a larger gap",
|
||||
opts: []Option{
|
||||
GapPercent(40),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("123")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(3, 0, 9, 5)},
|
||||
{'2', image.Rect(11, 0, 17, 5)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments with a gap, not all fit, maximizes displayed text",
|
||||
opts: []Option{
|
||||
GapPercent(20),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*3+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("8888")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'8', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
|
||||
{'8', image.Rect(sixteen.MinCols+1, 0, sixteen.MinCols*2+1, sixteen.MinRows)},
|
||||
{'8', image.Rect(sixteen.MinCols*2+2, 0, sixteen.MinCols*3+2, sixteen.MinRows)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments with a gap, not all fit, last segment would fit without a gap",
|
||||
opts: []Option{
|
||||
GapPercent(20),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*4+2, sixteen.MinRows),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("8888")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'8', image.Rect(3, 0, 9, 5)},
|
||||
{'8', image.Rect(10, 0, 16, 5)},
|
||||
{'8', image.Rect(17, 0, 23, 5)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws multiple segments with a gap, not enough space, maximizes segment height with option",
|
||||
opts: []Option{
|
||||
MaximizeSegmentHeight(),
|
||||
GapPercent(20),
|
||||
},
|
||||
canvas: image.Rect(0, 0, sixteen.MinCols*5, sixteen.MinRows*2),
|
||||
update: func(sd *SegmentDisplay) error {
|
||||
return sd.Write([]*TextChunk{NewChunk("123")})
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
for _, tc := range []struct {
|
||||
char rune
|
||||
area image.Rectangle
|
||||
}{
|
||||
{'1', image.Rect(2, 0, 14, 10)},
|
||||
{'2', image.Rect(16, 0, 28, 10)},
|
||||
} {
|
||||
mustDrawChar(cvs, tc.char, tc.area)
|
||||
}
|
||||
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sd, err := New(tc.opts...)
|
||||
if (err != nil) != tc.wantNewErr {
|
||||
t.Errorf("New => unexpected error: %v, wantNewErr: %v", err, tc.wantNewErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c, err := canvas.New(tc.canvas)
|
||||
if err != nil {
|
||||
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if tc.update != nil {
|
||||
err = tc.update(sd)
|
||||
if (err != nil) != tc.wantUpdateErr {
|
||||
t.Errorf("update => unexpected error: %v, wantUpdateErr: %v", err, tc.wantUpdateErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = sd.Draw(c)
|
||||
if (err != nil) != tc.wantDrawErr {
|
||||
t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
var want *faketerm.Terminal
|
||||
if tc.want != nil {
|
||||
want = tc.want(c.Size())
|
||||
} else {
|
||||
want = faketerm.MustNew(c.Size())
|
||||
}
|
||||
|
||||
if diff := faketerm.Diff(want, got); diff != "" {
|
||||
t.Errorf("Draw => %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyboard(t *testing.T) {
|
||||
sd, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
if err := sd.Keyboard(&terminalapi.Keyboard{}); err == nil {
|
||||
t.Errorf("Keyboard => got nil err, wanted one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMouse(t *testing.T) {
|
||||
sd, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
if err := sd.Mouse(&terminalapi.Mouse{}); err == nil {
|
||||
t.Errorf("Mouse => got nil err, wanted one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
sd, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
got := sd.Options()
|
||||
want := widgetapi.Options{
|
||||
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
||||
WantKeyboard: false,
|
||||
WantMouse: false,
|
||||
}
|
||||
if diff := pretty.Compare(want, got); diff != "" {
|
||||
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
}
|
162
widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
Normal file
162
widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
Normal file
@ -0,0 +1,162 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Binary segmentdisplaydemo shows the functionality of a segment display.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mum4k/termdash"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/container"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
"github.com/mum4k/termdash/terminal/termbox"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
"github.com/mum4k/termdash/widgets/segmentdisplay"
|
||||
)
|
||||
|
||||
// clock displays the current time on the segment display.
|
||||
// Exists when the context expires.
|
||||
func clock(ctx context.Context, sd *segmentdisplay.SegmentDisplay) {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
now := time.Now()
|
||||
nowStr := now.Format("15 04")
|
||||
parts := strings.Split(nowStr, " ")
|
||||
|
||||
spacer := " "
|
||||
if now.Second()%2 == 0 {
|
||||
spacer = "_"
|
||||
}
|
||||
chunks := []*segmentdisplay.TextChunk{
|
||||
segmentdisplay.NewChunk(parts[0], segmentdisplay.WriteCellOpts(cell.FgColor(cell.ColorBlue))),
|
||||
segmentdisplay.NewChunk(spacer),
|
||||
segmentdisplay.NewChunk(parts[1], segmentdisplay.WriteCellOpts(cell.FgColor(cell.ColorRed))),
|
||||
}
|
||||
if err := sd.Write(chunks); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rotate returns a new slice with inputs rotated by step.
|
||||
// I.e. for a step of one:
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
// And so on.
|
||||
func rotate(inputs []rune, step int) []rune {
|
||||
return append(inputs[step:], inputs[:step]...)
|
||||
}
|
||||
|
||||
// rollText rolls a text across the segment display.
|
||||
// Exists when the context expires.
|
||||
func rollText(ctx context.Context, sd *segmentdisplay.SegmentDisplay) {
|
||||
const text = "Termdash"
|
||||
colors := map[rune]cell.Color{
|
||||
'T': cell.ColorBlue,
|
||||
'e': cell.ColorRed,
|
||||
'r': cell.ColorYellow,
|
||||
'm': cell.ColorBlue,
|
||||
'd': cell.ColorGreen,
|
||||
'a': cell.ColorRed,
|
||||
's': cell.ColorGreen,
|
||||
'h': cell.ColorRed,
|
||||
}
|
||||
|
||||
var state []rune
|
||||
for i := 0; i < len(text); i++ {
|
||||
state = append(state, ' ')
|
||||
}
|
||||
state = append(state, []rune(text)...)
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
var chunks []*segmentdisplay.TextChunk
|
||||
for i := 0; i < len(text); i++ {
|
||||
chunks = append(chunks, segmentdisplay.NewChunk(
|
||||
string(state[i]),
|
||||
segmentdisplay.WriteCellOpts(cell.FgColor(colors[state[i]])),
|
||||
))
|
||||
}
|
||||
if err := sd.Write(chunks); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
state = rotate(state, 1)
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
t, err := termbox.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
clockSD, err := segmentdisplay.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go clock(ctx, clockSD)
|
||||
|
||||
rollingSD, err := segmentdisplay.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go rollText(ctx, rollingSD)
|
||||
|
||||
c, err := container.New(
|
||||
t,
|
||||
container.Border(draw.LineStyleLight),
|
||||
container.BorderTitle("PRESS Q TO QUIT"),
|
||||
container.SplitHorizontal(
|
||||
container.Top(
|
||||
container.PlaceWidget(rollingSD),
|
||||
),
|
||||
container.Bottom(
|
||||
container.PlaceWidget(clockSD),
|
||||
),
|
||||
container.SplitPercent(40),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
quitter := func(k *terminalapi.Keyboard) {
|
||||
if k.Key == 'q' || k.Key == 'Q' {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(1*time.Second)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
73
widgets/segmentdisplay/write_options.go
Normal file
73
widgets/segmentdisplay/write_options.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2019 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 segmentdisplay
|
||||
|
||||
// write_options.go contains options used when writing content to the widget.
|
||||
|
||||
import "github.com/mum4k/termdash/cell"
|
||||
|
||||
// WriteOption is used to provide options to Write().
|
||||
type WriteOption interface {
|
||||
// set sets the provided option.
|
||||
set(*writeOptions)
|
||||
}
|
||||
|
||||
// writeOptions stores the provided options.
|
||||
type writeOptions struct {
|
||||
cellOpts []cell.Option
|
||||
errOnUnsupported bool
|
||||
}
|
||||
|
||||
// newWriteOptions returns new writeOptions instance.
|
||||
func newWriteOptions(wOpts ...WriteOption) *writeOptions {
|
||||
wo := &writeOptions{}
|
||||
for _, o := range wOpts {
|
||||
o.set(wo)
|
||||
}
|
||||
return wo
|
||||
}
|
||||
|
||||
// writeOption implements WriteOption.
|
||||
type writeOption func(*writeOptions)
|
||||
|
||||
// set implements WriteOption.set.
|
||||
func (wo writeOption) set(wOpts *writeOptions) {
|
||||
wo(wOpts)
|
||||
}
|
||||
|
||||
// WriteCellOpts sets options on the cells that contain the text.
|
||||
func WriteCellOpts(opts ...cell.Option) WriteOption {
|
||||
return writeOption(func(wOpts *writeOptions) {
|
||||
wOpts.cellOpts = opts
|
||||
})
|
||||
}
|
||||
|
||||
// WriteSanitize instructs Write to sanitize the text, replacing all characters
|
||||
// the display doesn't support with a space ' ' character.
|
||||
// This is the default behavior.
|
||||
func WriteSanitize(opts ...cell.Option) WriteOption {
|
||||
return writeOption(func(wOpts *writeOptions) {
|
||||
wOpts.errOnUnsupported = false
|
||||
})
|
||||
}
|
||||
|
||||
// WriteErrOnUnsupported instructs Write to return an error when the text
|
||||
// contains a character the display doesn't support.
|
||||
// The default behavior is to sanitize the text, see WriteSanitize().
|
||||
func WriteErrOnUnsupported(opts ...cell.Option) WriteOption {
|
||||
return writeOption(func(wOpts *writeOptions) {
|
||||
wOpts.errOnUnsupported = true
|
||||
})
|
||||
}
|
@ -23,6 +23,7 @@ import (
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/mum4k/termdash/attrrange"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
@ -42,7 +43,9 @@ type Text struct {
|
||||
// buff contains the text to be displayed in the widget.
|
||||
buff bytes.Buffer
|
||||
// givenWOpts are write options given for the text.
|
||||
givenWOpts givenWOpts
|
||||
givenWOpts []*writeOptions
|
||||
// wOptsTracker tracks the positions in a buff to which the givenWOpts apply.
|
||||
wOptsTracker *attrrange.Tracker
|
||||
|
||||
// scroll tracks scrolling the position.
|
||||
scroll *scrollTracker
|
||||
@ -69,9 +72,9 @@ type Text struct {
|
||||
func New(opts ...Option) *Text {
|
||||
opt := newOptions(opts...)
|
||||
return &Text{
|
||||
givenWOpts: newGivenWOpts(),
|
||||
scroll: newScrollTracker(opt),
|
||||
opts: opt,
|
||||
wOptsTracker: attrrange.NewTracker(),
|
||||
scroll: newScrollTracker(opt),
|
||||
opts: opt,
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +84,8 @@ func (t *Text) Reset() {
|
||||
defer t.mu.Unlock()
|
||||
|
||||
t.buff.Reset()
|
||||
t.givenWOpts = newGivenWOpts()
|
||||
t.givenWOpts = nil
|
||||
t.wOptsTracker = attrrange.NewTracker()
|
||||
t.scroll = newScrollTracker(t.opts)
|
||||
t.lastWidth = 0
|
||||
t.contentChanged = true
|
||||
@ -103,7 +107,11 @@ func (t *Text) Write(text string, wOpts ...WriteOption) error {
|
||||
}
|
||||
|
||||
pos := t.buff.Len()
|
||||
t.givenWOpts[pos] = newOptsRange(pos, pos+len(text), newWriteOptions(wOpts...))
|
||||
t.givenWOpts = append(t.givenWOpts, newWriteOptions(wOpts...))
|
||||
wOptsIdx := len(t.givenWOpts) - 1
|
||||
if err := t.wOptsTracker.Add(pos, pos+len(text), wOptsIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := t.buff.WriteString(text); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -157,7 +165,10 @@ func (t *Text) draw(text string, cvs *canvas.Canvas) error {
|
||||
var cur image.Point // Tracks the current drawing position on the canvas.
|
||||
height := cvs.Area().Dy()
|
||||
fromLine := t.scroll.firstLine(len(t.lines), height)
|
||||
optRange := t.givenWOpts.forPosition(0) // Text options for the current byte.
|
||||
optRange, err := t.wOptsTracker.ForPosition(0) // Text options for the current byte.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
startPos := t.lines[fromLine]
|
||||
for i, r := range text {
|
||||
if i < startPos {
|
||||
@ -202,10 +213,15 @@ func (t *Text) draw(text string, cvs *canvas.Canvas) error {
|
||||
continue // Don't print the newline runes, just interpret them above.
|
||||
}
|
||||
|
||||
if i >= optRange.high { // Get the next write options.
|
||||
optRange = t.givenWOpts.forPosition(i)
|
||||
if i >= optRange.High { // Get the next write options.
|
||||
or, err := t.wOptsTracker.ForPosition(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
optRange = or
|
||||
}
|
||||
cells, err := cvs.SetCell(cur, r, optRange.opts.cellOpts)
|
||||
wOpts := t.givenWOpts[optRange.AttrIdx]
|
||||
cells, err := cvs.SetCell(cur, r, wOpts.cellOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -17,8 +17,6 @@ package text
|
||||
// write_options.go contains options used when writing content to the Text widget.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/mum4k/termdash/cell"
|
||||
)
|
||||
|
||||
@ -58,60 +56,3 @@ func WriteCellOpts(opts ...cell.Option) WriteOption {
|
||||
wOpts.cellOpts = cell.NewOptions(opts...)
|
||||
})
|
||||
}
|
||||
|
||||
// optsRange are write options that apply to a range of bytes in the text.
|
||||
type optsRange struct {
|
||||
// low is the first byte where these options apply.
|
||||
low int
|
||||
|
||||
// high is the end of the range. The opts apply to all bytes in range low
|
||||
// <= b < high.
|
||||
high int
|
||||
|
||||
// opts are the options provided at a call to Write().
|
||||
opts *writeOptions
|
||||
}
|
||||
|
||||
// newOptsRange returns a new optsRange.
|
||||
func newOptsRange(low, high int, opts *writeOptions) *optsRange {
|
||||
return &optsRange{
|
||||
low: low,
|
||||
high: high,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
// givenWOpts stores the write options provided on all the calls to Write().
|
||||
// The options are keyed by their low indices.
|
||||
type givenWOpts map[int]*optsRange
|
||||
|
||||
// newGivenWOpts returns a new givenWOpts instance.
|
||||
func newGivenWOpts() givenWOpts {
|
||||
return givenWOpts{}
|
||||
}
|
||||
|
||||
// forPosition returns write options that apply to character at the specified
|
||||
// byte position.
|
||||
func (g givenWOpts) forPosition(pos int) *optsRange {
|
||||
if or, ok := g[pos]; ok {
|
||||
return or
|
||||
}
|
||||
|
||||
var keys []int
|
||||
for k := range g {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Ints(keys)
|
||||
|
||||
res := newOptsRange(0, 0, newWriteOptions())
|
||||
for _, k := range keys {
|
||||
or := g[k]
|
||||
if or.low > pos {
|
||||
break
|
||||
}
|
||||
if or.high > pos {
|
||||
res = or
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
@ -1,214 +0,0 @@
|
||||
// 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 text
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
)
|
||||
|
||||
func TestGivenWOpts(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
given givenWOpts
|
||||
pos int
|
||||
want *optsRange
|
||||
}{
|
||||
{
|
||||
desc: "no write options given results in defaults",
|
||||
given: nil,
|
||||
pos: 1,
|
||||
want: &optsRange{
|
||||
low: 0,
|
||||
high: 0,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls before them",
|
||||
given: givenWOpts{
|
||||
2: &optsRange{
|
||||
low: 2,
|
||||
high: 5,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorBlue,
|
||||
},
|
||||
},
|
||||
},
|
||||
5: &optsRange{
|
||||
low: 5,
|
||||
high: 10,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorRed,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pos: 1,
|
||||
want: &optsRange{
|
||||
low: 0,
|
||||
high: 0,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls on the lower",
|
||||
given: givenWOpts{
|
||||
2: &optsRange{
|
||||
low: 2,
|
||||
high: 5,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorBlue,
|
||||
},
|
||||
},
|
||||
},
|
||||
5: &optsRange{
|
||||
low: 5,
|
||||
high: 10,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorRed,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pos: 2,
|
||||
want: &optsRange{
|
||||
low: 2,
|
||||
high: 5,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorBlue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls between them",
|
||||
given: givenWOpts{
|
||||
2: &optsRange{
|
||||
low: 2,
|
||||
high: 5,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorBlue,
|
||||
},
|
||||
},
|
||||
},
|
||||
5: &optsRange{
|
||||
low: 5,
|
||||
high: 10,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorRed,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pos: 4,
|
||||
want: &optsRange{
|
||||
low: 2,
|
||||
high: 5,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorBlue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls on the higher",
|
||||
given: givenWOpts{
|
||||
2: &optsRange{
|
||||
low: 2,
|
||||
high: 5,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorBlue,
|
||||
},
|
||||
},
|
||||
},
|
||||
5: &optsRange{
|
||||
low: 5,
|
||||
high: 10,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorRed,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pos: 5,
|
||||
want: &optsRange{
|
||||
low: 5,
|
||||
high: 10,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorRed,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple given options, position falls after them",
|
||||
given: givenWOpts{
|
||||
2: &optsRange{
|
||||
low: 2,
|
||||
high: 5,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorBlue,
|
||||
},
|
||||
},
|
||||
},
|
||||
5: &optsRange{
|
||||
low: 5,
|
||||
high: 10,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{
|
||||
FgColor: cell.ColorRed,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pos: 10,
|
||||
want: &optsRange{
|
||||
low: 0,
|
||||
high: 0,
|
||||
opts: &writeOptions{
|
||||
cellOpts: &cell.Options{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := tc.given.forPosition(tc.pos)
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("forPosition => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user