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

Merge pull request #101 from mum4k/segment-display

Segment display
This commit is contained in:
Jakub Sobon 2019-02-06 23:46:50 -05:00 committed by GitHub
commit e44d70b568
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 6994 additions and 306 deletions

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

View 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

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

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

View File

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

View File

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

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

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

View 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,
}
}

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

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

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

View File

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

View File

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

View File

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