Projects STRLCPY termdash Commits 990e13d2
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
Showing first 21 files as there are too many
  • ■ ■ ■ ■ ■ ■
    CHANGELOG.md
     1 +# Changelog
     2 + 
     3 +All notable changes to this project are documented here.
     4 + 
     5 +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
     6 +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
     7 + 
     8 +## [Unreleased]
     9 + 
     10 +## [0.6.0] - 07-Feb-2019
     11 + 
     12 +### Added
     13 + 
     14 +- The SegmentDisplay widget.
     15 +- A CHANGELOG.
     16 +- New line styles for borders.
     17 + 
     18 +### Changed
     19 + 
     20 +- Better recordings of the individual demos.
     21 + 
     22 +### Fixed
     23 + 
     24 +- The LineChart now has an option to change the behavior of the Y axis from
     25 + zero anchored to adaptive.
     26 +- Lint errors reported on the Go report card.
     27 +- Widgets now correctly handle a race when new user data are supplied between
     28 + calls to their Options() and Draw() methods.
     29 + 
     30 +## [0.5.0] - 21-Jan-2019
     31 + 
     32 +### Added
     33 + 
     34 +- Draw primitives for drawing circles.
     35 +- The Donut widget.
     36 + 
     37 +### Fixed
     38 + 
     39 +- Bugfixes in the braille canvas.
     40 +- Lint errors reported on the Go report card.
     41 +- Flaky behavior in termdash_test.
     42 + 
     43 +## [0.4.0] - 15-Jan-2019
     44 + 
     45 +### Added
     46 + 
     47 +- 256 color support.
     48 +- Variable size container splits.
     49 +- A more complete demo of the functionality.
     50 + 
     51 +### Changed
     52 + 
     53 +- Updated documentation and README.
     54 + 
     55 +## [0.3.0] - 13-Jan-2019
     56 + 
     57 +### Added
     58 + 
     59 +- Primitives for drawing lines.
     60 +- Implementation of a Braille canvas.
     61 +- The LineChart widget.
     62 + 
     63 +## [0.2.0] - 02-Jul-2018
     64 + 
     65 +### Added
     66 + 
     67 +- The SparkLine widget.
     68 +- The BarChart widget.
     69 +- Manually triggered redraw.
     70 +- Travis now checks for presence of licence headers.
     71 + 
     72 +### Fixed
     73 + 
     74 +- Fixing races in termdash_test.
     75 + 
     76 +## 0.1.0 - 13-Jun-2018
     77 + 
     78 +### Added
     79 + 
     80 +- Documentation of the project and its goals.
     81 +- Drawing infrastructure.
     82 +- Testing infrastructure.
     83 +- The Gauge widget.
     84 +- The Text widget.
     85 + 
     86 +[Unreleased]: https://github.com/mum4k/termdash/compare/v0.6.0...devel
     87 +[0.6.0]: https://github.com/mum4k/termdash/compare/v0.5.0...v0.6.0
     88 +[0.5.0]: https://github.com/mum4k/termdash/compare/v0.4.0...v0.5.0
     89 +[0.4.0]: https://github.com/mum4k/termdash/compare/v0.3.0...v0.4.0
     90 +[0.3.0]: https://github.com/mum4k/termdash/compare/v0.2.0...v0.3.0
     91 +[0.2.0]: https://github.com/mum4k/termdash/compare/v0.1.0...v0.2.0
     92 + 
  • ■ ■ ■ ■ ■
    README.md
    skipped 2 lines
    3 3  [![Coverage Status](https://coveralls.io/repos/github/mum4k/termdash/badge.svg?branch=master)](https://coveralls.io/github/mum4k/termdash?branch=master)
    4 4  [![Go Report Card](https://goreportcard.com/badge/github.com/mum4k/termdash)](https://goreportcard.com/report/github.com/mum4k/termdash)
    5 5  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE)
     6 +[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
    6 7   
    7 8  # termdash
    8 9   
    9  -[<img src="./images/termdashdemo.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
     10 +[<img src="./images/termdashdemo_0_6_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
    10 11   
    11 12  This project implements a cross-platform customizable terminal based dashboard.
    12 13  The feature set is inspired by the
    skipped 18 lines
    31 32  - Drawing primitives (Go functions) for widget development with character and
    32 33   sub-character resolution.
    33 34   
     35 +See the [changelog](CHANGELOG.md) for more details.
     36 + 
    34 37  # Installation
    35 38   
    36 39  To install this library, run the following:
    37 40   
    38 41  ```
    39 42  go get -u github.com/mum4k/termdash
    40  - 
    41 43  ```
    42 44   
    43 45  # Usage
    skipped 58 lines
    102 104  go run github.com/mum4k/termdash/widgets/sparkline/sparklinedemo/sparklinedemo.go
    103 105  ```
    104 106   
    105  -[<img src="./images/sparklinedemo.gif" alt="sparklinedemo" type="image/gif">](widgets/sparkline/sparklinedemo/sparklinedemo.go)
     107 +[<img src="./images/sparklinedemo.gif" alt="sparklinedemo" type="image/gif" width="50%">](widgets/sparkline/sparklinedemo/sparklinedemo.go)
    106 108   
    107 109  ### The BarChart
    108 110   
    skipped 4 lines
    113 115  go run github.com/mum4k/termdash/widgets/barchart/barchartdemo/barchartdemo.go
    114 116  ```
    115 117   
    116  -[<img src="./images/barchartdemo.gif" alt="barchartdemo" type="image/gif">](widgets/barchart/barchartdemo/barchartdemo.go)
     118 +[<img src="./images/barchartdemo.gif" alt="barchartdemo" type="image/gif" width="50%">](widgets/barchart/barchartdemo/barchartdemo.go)
    117 119   
    118 120  ### The LineChart
    119 121   
    skipped 4 lines
    124 126  go run github.com/mum4k/termdash/widgets/linechart/linechartdemo/linechartdemo.go
    125 127  ```
    126 128   
    127  -[<img src="./images/linechartdemo.gif" alt="linechartdemo" type="image/gif">](widgets/linechart/linechartdemo/linechartdemo.go)
     129 +[<img src="./images/linechartdemo.gif" alt="linechartdemo" type="image/gif" width="70%">](widgets/linechart/linechartdemo/linechartdemo.go)
     130 + 
     131 +### The SegmentDisplay
     132 + 
     133 +Displays text by simulating a 16-segment display. Run the
     134 +[linechartdemo](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go).
     135 + 
     136 +```go
     137 +go run github.com/mum4k/termdash/widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
     138 +```
     139 + 
     140 +[<img src="./images/segmentdisplaydemo.gif" alt="segmentdisplaydemo" type="image/gif">](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go)
    128 141   
    129 142  # Contributing
    130 143   
    skipped 20 lines
  • ■ ■ ■ ■
    align/align.go
    skipped 120 lines
    121 121   ), nil
    122 122  }
    123 123   
    124  -// Rectangle aligns the rectangle within the provided area returning the
     124 +// Rectangle aligns the area within the rectangle returning the
    125 125  // aligned area. The area must fall within the rectangle.
    126 126  func Rectangle(rect image.Rectangle, ar image.Rectangle, h Horizontal, v Vertical) (image.Rectangle, error) {
    127 127   if !ar.In(rect) {
    skipped 47 lines
  • ■ ■ ■ ■ ■ ■
    area/area.go
    skipped 17 lines
    18 18  import (
    19 19   "fmt"
    20 20   "image"
     21 + 
     22 + "github.com/mum4k/termdash/numbers"
    21 23  )
    22 24   
    23 25  // Size returns the size of the provided area.
    skipped 52 lines
    76 78   return left, right, nil
    77 79  }
    78 80   
    79  -// abs returns the absolute value of x.
    80  -func abs(x int) int {
    81  - if x < 0 {
    82  - return -x
    83  - }
    84  - return x
    85  -}
    86  - 
    87 81  // ExcludeBorder returns a new area created by subtracting a border around the
    88 82  // provided area. Return the zero area if there isn't enough space to exclude
    89 83  // the border.
    skipped 5 lines
    95 89   return image.ZR
    96 90   }
    97 91   return image.Rect(
    98  - abs(area.Min.X+1),
    99  - abs(area.Min.Y+1),
    100  - abs(area.Max.X-1),
    101  - abs(area.Max.Y-1),
     92 + numbers.Abs(area.Min.X+1),
     93 + numbers.Abs(area.Min.Y+1),
     94 + numbers.Abs(area.Max.X-1),
     95 + numbers.Abs(area.Max.Y-1),
    102 96   )
    103 97  }
    104 98   
    skipped 57 lines
  • ■ ■ ■ ■ ■ ■
    attrrange/attrrange.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +// Package attrrange simplifies tracking of attributes that apply to a range of
     16 +// items.
     17 +// Refer to the examples if the test file for details on usage.
     18 +package attrrange
     19 + 
     20 +import (
     21 + "fmt"
     22 + "sort"
     23 +)
     24 + 
     25 +// AttrRange is a range of items that share the same attributes.
     26 +type AttrRange struct {
     27 + // Low is the first position where these attributes apply.
     28 + Low int
     29 + 
     30 + // High is the end of the range. The attributes apply to all items in range
     31 + // Low <= b < high.
     32 + High int
     33 + 
     34 + // AttrIdx is the index of the attributes that apply to this range.
     35 + AttrIdx int
     36 +}
     37 + 
     38 +// newAttrRange returns a new AttrRange instance.
     39 +func newAttrRange(low, high, attrIdx int) *AttrRange {
     40 + return &AttrRange{
     41 + Low: low,
     42 + High: high,
     43 + AttrIdx: attrIdx,
     44 + }
     45 +}
     46 + 
     47 +// Tracker tracks attributes that apply to a range of items.
     48 +// This object is not thread safe.
     49 +type Tracker struct {
     50 + // ranges maps low indices of ranges to the attribute ranges.
     51 + ranges map[int]*AttrRange
     52 +}
     53 + 
     54 +// NewTracker returns a new tracker of ranges that share the same attributes.
     55 +func NewTracker() *Tracker {
     56 + return &Tracker{
     57 + ranges: map[int]*AttrRange{},
     58 + }
     59 +}
     60 + 
     61 +// Add adds a new range of items that share attributes with the specified
     62 +// index.
     63 +// The low position of the range must not overlap with low position of any
     64 +// existing range.
     65 +func (t *Tracker) Add(low, high, attrIdx int) error {
     66 + ar := newAttrRange(low, high, attrIdx)
     67 + if ar, ok := t.ranges[low]; ok {
     68 + return fmt.Errorf("already have range starting on low:%d, existing:%+v", low, ar)
     69 + }
     70 + t.ranges[low] = ar
     71 + return nil
     72 +}
     73 + 
     74 +// ForPosition returns attribute index that apply to the specified position.
     75 +// Returns ErrNotFound when the requested position wasn't found in any of the
     76 +// known ranges.
     77 +func (t *Tracker) ForPosition(pos int) (*AttrRange, error) {
     78 + if ar, ok := t.ranges[pos]; ok {
     79 + return ar, nil
     80 + }
     81 + 
     82 + var keys []int
     83 + for k := range t.ranges {
     84 + keys = append(keys, k)
     85 + }
     86 + sort.Ints(keys)
     87 + 
     88 + var res *AttrRange
     89 + for _, k := range keys {
     90 + ar := t.ranges[k]
     91 + if ar.Low > pos {
     92 + break
     93 + }
     94 + if ar.High > pos {
     95 + res = ar
     96 + }
     97 + }
     98 + 
     99 + if res == nil {
     100 + return nil, fmt.Errorf("did not find attribute range for position %d", pos)
     101 + }
     102 + return res, nil
     103 +}
     104 + 
  • ■ ■ ■ ■ ■ ■
    attrrange/attrrange_test.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +package attrrange
     16 + 
     17 +import (
     18 + "log"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 + "github.com/mum4k/termdash/cell"
     23 +)
     24 + 
     25 +func Example() {
     26 + // Caller has a slice of some attributes, like a cell color that applies
     27 + // to a portion of text.
     28 + attrs := []cell.Color{cell.ColorRed, cell.ColorBlue}
     29 + redIdx := 0
     30 + blueIdx := 1
     31 + 
     32 + // This is the text the colors apply to.
     33 + const text = "HelloWorld"
     34 + 
     35 + // Assuming that we want the word "Hello" in red and the word "World" in
     36 + // green, we can set our ranges as follows:
     37 + tr := NewTracker()
     38 + if err := tr.Add(0, len("Hello"), redIdx); err != nil {
     39 + panic(err)
     40 + }
     41 + if err := tr.Add(len("Hello")+1, len(text), blueIdx); err != nil {
     42 + panic(err)
     43 + }
     44 + 
     45 + // Now to get the index into attrs (i.e. the color) for a particular
     46 + // character, we can do:
     47 + for i, c := range text {
     48 + ar, err := tr.ForPosition(i)
     49 + if err != nil {
     50 + panic(err)
     51 + }
     52 + 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)
     53 + }
     54 +}
     55 + 
     56 +func TestForPosition(t *testing.T) {
     57 + tests := []struct {
     58 + desc string
     59 + // if not nil, called before calling ForPosition.
     60 + // Can add ranges.
     61 + update func(*Tracker) error
     62 + pos int
     63 + want *AttrRange
     64 + wantErr bool
     65 + wantUpdateErr bool
     66 + }{
     67 + {
     68 + desc: "fails when no ranges given",
     69 + pos: 0,
     70 + wantErr: true,
     71 + },
     72 + {
     73 + desc: "fails to add a duplicate",
     74 + update: func(tr *Tracker) error {
     75 + if err := tr.Add(2, 5, 40); err != nil {
     76 + return err
     77 + }
     78 + return tr.Add(2, 3, 41)
     79 + },
     80 + wantUpdateErr: true,
     81 + },
     82 + {
     83 + desc: "fails when multiple given ranges, position falls before them",
     84 + update: func(tr *Tracker) error {
     85 + if err := tr.Add(2, 5, 40); err != nil {
     86 + return err
     87 + }
     88 + return tr.Add(5, 10, 41)
     89 + },
     90 + pos: 1,
     91 + wantErr: true,
     92 + },
     93 + {
     94 + desc: "multiple given options, position falls on the lower",
     95 + update: func(tr *Tracker) error {
     96 + if err := tr.Add(2, 5, 40); err != nil {
     97 + return err
     98 + }
     99 + return tr.Add(5, 10, 41)
     100 + },
     101 + pos: 2,
     102 + want: newAttrRange(2, 5, 40),
     103 + },
     104 + {
     105 + desc: "multiple given options, position falls between them",
     106 + update: func(tr *Tracker) error {
     107 + if err := tr.Add(2, 5, 40); err != nil {
     108 + return err
     109 + }
     110 + return tr.Add(5, 10, 41)
     111 + },
     112 + pos: 4,
     113 + want: newAttrRange(2, 5, 40),
     114 + },
     115 + {
     116 + desc: "multiple given options, position falls on the higher",
     117 + update: func(tr *Tracker) error {
     118 + if err := tr.Add(2, 5, 40); err != nil {
     119 + return err
     120 + }
     121 + return tr.Add(5, 10, 41)
     122 + },
     123 + pos: 5,
     124 + want: newAttrRange(5, 10, 41),
     125 + },
     126 + {
     127 + desc: "multiple given options, position falls after them",
     128 + update: func(tr *Tracker) error {
     129 + if err := tr.Add(2, 5, 40); err != nil {
     130 + return err
     131 + }
     132 + return tr.Add(5, 10, 41)
     133 + },
     134 + pos: 10,
     135 + wantErr: true,
     136 + },
     137 + }
     138 + 
     139 + for _, tc := range tests {
     140 + t.Run(tc.desc, func(t *testing.T) {
     141 + tr := NewTracker()
     142 + if tc.update != nil {
     143 + err := tc.update(tr)
     144 + if (err != nil) != tc.wantUpdateErr {
     145 + t.Errorf("tc.update => unexpected error:%v, wantUpdateErr:%v", err, tc.wantUpdateErr)
     146 + }
     147 + if err != nil {
     148 + return
     149 + }
     150 + }
     151 + 
     152 + got, err := tr.ForPosition(tc.pos)
     153 + if (err != nil) != tc.wantErr {
     154 + t.Errorf("ForPosition => unexpected error:%v, wantErr:%v", err, tc.wantErr)
     155 + }
     156 + if err != nil {
     157 + return
     158 + }
     159 + 
     160 + if diff := pretty.Compare(tc.want, got); diff != "" {
     161 + t.Errorf("ForPosition => unexpected diff (-want, +got):\n%s", diff)
     162 + }
     163 + })
     164 + }
     165 +}
     166 + 
  • ■ ■ ■ ■ ■ ■
    canvas/testcanvas/testcanvas.go
    skipped 50 lines
    51 51   return cells
    52 52  }
    53 53   
     54 +// MustCopyTo copies the content of the source canvas onto the destination
     55 +// canvas or panics.
     56 +func MustCopyTo(src, dst *canvas.Canvas) {
     57 + if err := src.CopyTo(dst); err != nil {
     58 + panic(fmt.Sprintf("canvas.CopyTo => unexpected error: %v", err))
     59 + }
     60 +}
     61 + 
  • ■ ■ ■ ■ ■
    container/draw.go
    skipped 134 lines
    135 135   if err != nil {
    136 136   return err
    137 137   }
    138  - 
    139  - if err := draw.Text(cvs, "⇄", image.Point{0, 0}); err != nil {
     138 + if err := draw.ResizeNeeded(cvs); err != nil {
    140 139   return err
    141 140   }
    142 141   return cvs.Apply(c.term)
    skipped 18 lines
  • ■ ■ ■ ■
    container/options.go
    skipped 204 lines
    205 205   
    206 206  // AlignHorizontal sets the horizontal alignment for the widget placed in the
    207 207  // container. Has no effect if the container contains no widget.
    208  -// Defaults alignment in the center.
     208 +// Defaults to alignment in the center.
    209 209  func AlignHorizontal(h align.Horizontal) Option {
    210 210   return option(func(c *Container) error {
    211 211   c.opts.hAlign = h
    skipped 195 lines
  • ■ ■ ■ ■ ■
    doc/widget_development.md
    skipped 9 lines
    10 10  ## Thread safety
    11 11   
    12 12  All widget implementations must be thread safe, since the infrastructure calls
    13  -the widget's **Draw()** method concurrently with the user of the widget setting
    14  -the displayed values.
     13 +the widget's **Options** and **Draw()** method concurrently with the user of
     14 +the widget setting the displayed values.
    15 15   
    16 16  ## Drawing the widget's content
    17 17   
    skipped 20 lines
    38 38  If the current size of the terminal and the configured container splits result
    39 39  in a canvas smaller than the **MinimumSize**, the infrastructure won't call the
    40 40  widget's **Draw()** method. The widgets can use this to prevent impossible
    41  -scenarios where an error would have to be returned.
     41 +scenarios where an error would have to be returned. Note that if the values
     42 +returned on a call to the **Options** method aren't static, but depend on the
     43 +user data provided to the widget, the widget **must** protect against the
     44 +scenario where the infrastructure provides a canvas that doesn't match the
     45 +returned options. This is because the infrastructure cannot guarantee the user
     46 +won't change the data between calls to **Options** and **Draw**.
     47 + 
     48 +A widget can draw a character indicating that a resize is needed in such cases:
     49 + 
     50 +```go
     51 +func (w *Widget) Draw(cvs *canvas.Canvas) error {
     52 + min := w.minSize() // Output depends on the current state.
     53 + needAr, err := area.FromSize(min)
     54 + if err != nil {
     55 + return err
     56 + }
     57 + if !needAr.In(cvs.Area()) {
     58 + return draw.ResizeNeeded(cvs)
     59 + }
     60 + 
     61 + // Draw the widget.
     62 + return nil
     63 +}
     64 +```
    42 65   
    43 66  If the container configuration results in a canvas larger than **MaximumSize**
    44 67  the canvas will be limited to the specified size. Widgets can either specify a
    45 68  limit for both the maximum width and height or limit just one of them.
     69 + 
    46 70   
    47 71  ## Unit tests
    48 72   
    skipped 73 lines
  • ■ ■ ■ ■ ■ ■
    draw/border_test.go
    skipped 92 lines
    93 93   },
    94 94   },
    95 95   {
     96 + desc: "draws double border around the canvas",
     97 + canvas: image.Rect(0, 0, 4, 4),
     98 + border: image.Rect(0, 0, 4, 4),
     99 + opts: []BorderOption{
     100 + BorderLineStyle(LineStyleDouble),
     101 + },
     102 + want: func(size image.Point) *faketerm.Terminal {
     103 + ft := faketerm.MustNew(size)
     104 + c := testcanvas.MustNew(ft.Area())
     105 + 
     106 + testcanvas.MustSetCell(c, image.Point{0, 0}, lineStyleChars[LineStyleDouble][topLeftCorner])
     107 + testcanvas.MustSetCell(c, image.Point{0, 1}, lineStyleChars[LineStyleDouble][vLine])
     108 + testcanvas.MustSetCell(c, image.Point{0, 2}, lineStyleChars[LineStyleDouble][vLine])
     109 + testcanvas.MustSetCell(c, image.Point{0, 3}, lineStyleChars[LineStyleDouble][bottomLeftCorner])
     110 + 
     111 + testcanvas.MustSetCell(c, image.Point{1, 0}, lineStyleChars[LineStyleDouble][hLine])
     112 + testcanvas.MustSetCell(c, image.Point{1, 3}, lineStyleChars[LineStyleDouble][hLine])
     113 + 
     114 + testcanvas.MustSetCell(c, image.Point{2, 0}, lineStyleChars[LineStyleDouble][hLine])
     115 + testcanvas.MustSetCell(c, image.Point{2, 3}, lineStyleChars[LineStyleDouble][hLine])
     116 + 
     117 + testcanvas.MustSetCell(c, image.Point{3, 0}, lineStyleChars[LineStyleDouble][topRightCorner])
     118 + testcanvas.MustSetCell(c, image.Point{3, 1}, lineStyleChars[LineStyleDouble][vLine])
     119 + testcanvas.MustSetCell(c, image.Point{3, 2}, lineStyleChars[LineStyleDouble][vLine])
     120 + testcanvas.MustSetCell(c, image.Point{3, 3}, lineStyleChars[LineStyleDouble][bottomRightCorner])
     121 + 
     122 + testcanvas.MustApply(c, ft)
     123 + return ft
     124 + },
     125 + },
     126 + {
     127 + desc: "draws round border around the canvas",
     128 + canvas: image.Rect(0, 0, 4, 4),
     129 + border: image.Rect(0, 0, 4, 4),
     130 + opts: []BorderOption{
     131 + BorderLineStyle(LineStyleRound),
     132 + },
     133 + want: func(size image.Point) *faketerm.Terminal {
     134 + ft := faketerm.MustNew(size)
     135 + c := testcanvas.MustNew(ft.Area())
     136 + 
     137 + testcanvas.MustSetCell(c, image.Point{0, 0}, lineStyleChars[LineStyleRound][topLeftCorner])
     138 + testcanvas.MustSetCell(c, image.Point{0, 1}, lineStyleChars[LineStyleRound][vLine])
     139 + testcanvas.MustSetCell(c, image.Point{0, 2}, lineStyleChars[LineStyleRound][vLine])
     140 + testcanvas.MustSetCell(c, image.Point{0, 3}, lineStyleChars[LineStyleRound][bottomLeftCorner])
     141 + 
     142 + testcanvas.MustSetCell(c, image.Point{1, 0}, lineStyleChars[LineStyleRound][hLine])
     143 + testcanvas.MustSetCell(c, image.Point{1, 3}, lineStyleChars[LineStyleRound][hLine])
     144 + 
     145 + testcanvas.MustSetCell(c, image.Point{2, 0}, lineStyleChars[LineStyleRound][hLine])
     146 + testcanvas.MustSetCell(c, image.Point{2, 3}, lineStyleChars[LineStyleRound][hLine])
     147 + 
     148 + testcanvas.MustSetCell(c, image.Point{3, 0}, lineStyleChars[LineStyleRound][topRightCorner])
     149 + testcanvas.MustSetCell(c, image.Point{3, 1}, lineStyleChars[LineStyleRound][vLine])
     150 + testcanvas.MustSetCell(c, image.Point{3, 2}, lineStyleChars[LineStyleRound][vLine])
     151 + testcanvas.MustSetCell(c, image.Point{3, 3}, lineStyleChars[LineStyleRound][bottomRightCorner])
     152 + 
     153 + testcanvas.MustApply(c, ft)
     154 + return ft
     155 + },
     156 + },
     157 + {
    96 158   desc: "draws border in the canvas",
    97 159   canvas: image.Rect(0, 0, 4, 4),
    98 160   border: image.Rect(1, 1, 3, 3),
    skipped 343 lines
  • ■ ■ ■ ■ ■
    draw/braille_line.go
    skipped 21 lines
    22 22   
    23 23   "github.com/mum4k/termdash/canvas/braille"
    24 24   "github.com/mum4k/termdash/cell"
     25 + "github.com/mum4k/termdash/numbers"
    25 26  )
    26 27   
    27 28  // braillePixelChange represents an action on a pixel on the braille canvas.
    skipped 105 lines
    133 134   // Implements Bresenham's line algorithm.
    134 135   // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
    135 136   
    136  - vertProj := abs(end.Y - start.Y)
    137  - horizProj := abs(end.X - start.X)
     137 + vertProj := numbers.Abs(end.Y - start.Y)
     138 + horizProj := numbers.Abs(end.X - start.X)
    138 139   if vertProj < horizProj {
    139 140   if start.X > end.X {
    140 141   return lineLow(end.X, end.Y, start.X, start.Y)
    skipped 61 lines
    202 203   return res
    203 204  }
    204 205   
    205  -// abs returns the absolute value of x.
    206  -func abs(x int) int {
    207  - if x < 0 {
    208  - return -x
    209  - }
    210  - return x
    211  -}
    212  - 
  • ■ ■ ■ ■ ■ ■
    draw/line_style.go
    skipped 38 lines
    39 39   vAndRight: '├',
    40 40   vAndH: '┼',
    41 41   },
     42 + LineStyleDouble: {
     43 + hLine: '═',
     44 + vLine: '║',
     45 + topLeftCorner: '╔',
     46 + topRightCorner: '╗',
     47 + bottomLeftCorner: '╚',
     48 + bottomRightCorner: '╝',
     49 + hAndUp: '╩',
     50 + hAndDown: '╦',
     51 + vAndLeft: '╣',
     52 + vAndRight: '╠',
     53 + vAndH: '╬',
     54 + },
     55 + LineStyleRound: {
     56 + hLine: '─',
     57 + vLine: '│',
     58 + topLeftCorner: '╭',
     59 + topRightCorner: '╮',
     60 + bottomLeftCorner: '╰',
     61 + bottomRightCorner: '╯',
     62 + hAndUp: '┴',
     63 + hAndDown: '┬',
     64 + vAndLeft: '┤',
     65 + vAndRight: '├',
     66 + vAndH: '┼',
     67 + },
    42 68  }
    43 69   
    44 70  // init verifies that all line parts are half-width runes (occupy only one
    skipped 30 lines
    75 101   
    76 102  // lineStyleNames maps LineStyle values to human readable names.
    77 103  var lineStyleNames = map[LineStyle]string{
    78  - LineStyleLight: "LineStyleLight",
     104 + LineStyleLight: "LineStyleLight",
     105 + LineStyleDouble: "LineStyleDouble",
     106 + LineStyleRound: "LineStyleRound",
    79 107  }
    80 108   
    81 109  // Supported line styles.
    82 110  const (
    83 111   LineStyleNone LineStyle = iota
    84 112   LineStyleLight
     113 + LineStyleDouble
     114 + LineStyleRound
    85 115  )
    86 116   
    87 117  // linePart identifies individual line parts.
    skipped 38 lines
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/segment/segment.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +// Package segment provides functions that draw a single segment.
     16 +package segment
     17 + 
     18 +import (
     19 + "fmt"
     20 + "image"
     21 + 
     22 + "github.com/mum4k/termdash/canvas/braille"
     23 + "github.com/mum4k/termdash/cell"
     24 + "github.com/mum4k/termdash/draw"
     25 +)
     26 + 
     27 +// Type identifies the type of the segment that is drawn.
     28 +type Type int
     29 + 
     30 +// String implements fmt.Stringer()
     31 +func (st Type) String() string {
     32 + if n, ok := segmentTypeNames[st]; ok {
     33 + return n
     34 + }
     35 + return "TypeUnknown"
     36 +}
     37 + 
     38 +// segmentTypeNames maps Type values to human readable names.
     39 +var segmentTypeNames = map[Type]string{
     40 + Horizontal: "Horizontal",
     41 + Vertical: "Vertical",
     42 +}
     43 + 
     44 +const (
     45 + segmentTypeUnknown Type = iota
     46 + 
     47 + // Horizontal is a horizontal segment.
     48 + Horizontal
     49 + // Vertical is a vertical segment.
     50 + Vertical
     51 + 
     52 + segmentTypeMax // Used for validation.
     53 +)
     54 + 
     55 +// Option is used to provide options.
     56 +type Option interface {
     57 + // set sets the provided option.
     58 + set(*options)
     59 +}
     60 + 
     61 +// options stores the provided options.
     62 +type options struct {
     63 + cellOpts []cell.Option
     64 + skipSlopesLTE int
     65 + reverseSlopes bool
     66 +}
     67 + 
     68 +// option implements Option.
     69 +type option func(*options)
     70 + 
     71 +// set implements Option.set.
     72 +func (o option) set(opts *options) {
     73 + o(opts)
     74 +}
     75 + 
     76 +// CellOpts sets options on the cells that contain the segment.
     77 +// Cell options on a braille canvas can only be set on the entire cell, not per
     78 +// pixel.
     79 +func CellOpts(cOpts ...cell.Option) Option {
     80 + return option(func(opts *options) {
     81 + opts.cellOpts = cOpts
     82 + })
     83 +}
     84 + 
     85 +// SkipSlopesLTE if provided instructs HV to not create slopes at the ends of a
     86 +// segment if the height of the horizontal or the width of the vertical segment
     87 +// is less or equal to the provided value.
     88 +func SkipSlopesLTE(v int) Option {
     89 + return option(func(opts *options) {
     90 + opts.skipSlopesLTE = v
     91 + })
     92 +}
     93 + 
     94 +// ReverseSlopes if provided reverses the order in which slopes are drawn.
     95 +// This only has a visible effect when the horizontal segment has height of two
     96 +// or the vertical segment has width of two.
     97 +// Without this option segments with height / width of two look like this:
     98 +// - |
     99 +// --- ||
     100 +// |
     101 +//
     102 +// With this option:
     103 +// --- |
     104 +// - ||
     105 +// |
     106 +func ReverseSlopes() Option {
     107 + return option(func(opts *options) {
     108 + opts.reverseSlopes = true
     109 + })
     110 +}
     111 + 
     112 +// validArea validates the provided area.
     113 +func validArea(ar image.Rectangle) error {
     114 + if ar.Min.X < 0 || ar.Min.Y < 0 {
     115 + return fmt.Errorf("the start coordinates cannot be negative, got: %v", ar)
     116 + }
     117 + if ar.Max.X < 0 || ar.Max.Y < 0 {
     118 + return fmt.Errorf("the end coordinates cannot be negative, got: %v", ar)
     119 + }
     120 + if ar.Dx() < 1 || ar.Dy() < 1 {
     121 + 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)
     122 + }
     123 + return nil
     124 +}
     125 + 
     126 +// HV draws a horizontal or a vertical display segment, filling the provided area.
     127 +// The segment will have slopes on both of its ends.
     128 +func HV(bc *braille.Canvas, ar image.Rectangle, st Type, opts ...Option) error {
     129 + if err := validArea(ar); err != nil {
     130 + return err
     131 + }
     132 + 
     133 + opt := &options{}
     134 + for _, o := range opts {
     135 + o.set(opt)
     136 + }
     137 + 
     138 + var nextLine nextHVLineFn
     139 + var lines int
     140 + switch st {
     141 + case Horizontal:
     142 + lines = ar.Dy()
     143 + nextLine = nextHorizLine
     144 + 
     145 + case Vertical:
     146 + lines = ar.Dx()
     147 + nextLine = nextVertLine
     148 + 
     149 + default:
     150 + return fmt.Errorf("unsupported segment type %v(%d)", st, st)
     151 + }
     152 + 
     153 + for i := 0; i < lines; i++ {
     154 + start, end := nextLine(i, ar, opt)
     155 + if err := draw.BrailleLine(bc, start, end, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
     156 + return err
     157 + }
     158 + 
     159 + }
     160 + return nil
     161 +}
     162 + 
     163 +// nextHVLineFn is a function that determines the start and end points of a line
     164 +// number num in a horizontal or a vertical segment.
     165 +type nextHVLineFn func(num int, ar image.Rectangle, opt *options) (image.Point, image.Point)
     166 + 
     167 +// nextHorizLine determines the start and end point of individual lines in a
     168 +// horizontal segment.
     169 +func nextHorizLine(num int, ar image.Rectangle, opt *options) (image.Point, image.Point) {
     170 + // Start and end points of the full row without adjustments for slopes.
     171 + start := image.Point{ar.Min.X, ar.Min.Y + num}
     172 + end := image.Point{ar.Max.X - 1, ar.Min.Y + num}
     173 + 
     174 + height := ar.Dy()
     175 + width := ar.Dx()
     176 + if height <= opt.skipSlopesLTE || height < 2 || width < 3 {
     177 + // No slopes under these dimensions as we don't have the resolution.
     178 + return start, end
     179 + }
     180 + 
     181 + // Don't adjust rows that fall exactly in the middle of the segment height.
     182 + // E.g when height divides oddly, we want the middle row to take the full
     183 + // width:
     184 + // --
     185 + // ----
     186 + // --
     187 + //
     188 + // And when the height divides oddly, we want the two middle rows to take
     189 + // the full width:
     190 + // --
     191 + // ----
     192 + // ----
     193 + // --
     194 + // We only do this for segments that are at least three rows tall.
     195 + // For smaller segments we still want this behavior:
     196 + // --
     197 + // ----
     198 + halfHeight := height / 2
     199 + if height > 2 {
     200 + if num == halfHeight || (height%2 == 0 && num == halfHeight-1) {
     201 + return start, end
     202 + }
     203 + }
     204 + if height == 2 && opt.reverseSlopes {
     205 + return adjustHoriz(start, end, width, num)
     206 + }
     207 + 
     208 + if num < halfHeight {
     209 + adjust := halfHeight - num
     210 + if height%2 == 0 && height > 2 {
     211 + // On evenly divided height, we need one less adjustment on every
     212 + // row above the half, since two rows are taking the full width
     213 + // as shown above.
     214 + adjust--
     215 + }
     216 + return adjustHoriz(start, end, width, adjust)
     217 + }
     218 + adjust := num - halfHeight
     219 + return adjustHoriz(start, end, width, adjust)
     220 +}
     221 + 
     222 +// nextVertLine determines the start and end point of individual lines in a
     223 +// vertical segment.
     224 +func nextVertLine(num int, ar image.Rectangle, opt *options) (image.Point, image.Point) {
     225 + // Start and end points of the full column without adjustments for slopes.
     226 + start := image.Point{ar.Min.X + num, ar.Min.Y}
     227 + end := image.Point{ar.Min.X + num, ar.Max.Y - 1}
     228 + 
     229 + height := ar.Dy()
     230 + width := ar.Dx()
     231 + if width <= opt.skipSlopesLTE || height < 3 || width < 2 {
     232 + // No slopes under these dimensions as we don't have the resolution.
     233 + return start, end
     234 + }
     235 + 
     236 + // Don't adjust lines that fall exactly in the middle of the segment height.
     237 + // E.g when width divides oddly, we want the middle line to take the full
     238 + // height:
     239 + // |
     240 + // |||
     241 + // |||
     242 + // |
     243 + //
     244 + // And when the width divides oddly, we want the two middle columns to take
     245 + // the full height:
     246 + // ||
     247 + // ||||
     248 + // ||||
     249 + // ||
     250 + //
     251 + // We only do this for segments that are at least three columns wide.
     252 + // For smaller segments we still want this behavior:
     253 + // |
     254 + // ||
     255 + // ||
     256 + // |
     257 + halfWidth := width / 2
     258 + if width > 2 {
     259 + if num == halfWidth || (width%2 == 0 && num == halfWidth-1) {
     260 + return start, end
     261 + }
     262 + }
     263 + if width == 2 && opt.reverseSlopes {
     264 + return adjustVert(start, end, width, num)
     265 + }
     266 + 
     267 + if num < halfWidth {
     268 + adjust := halfWidth - num
     269 + if width%2 == 0 && width > 2 {
     270 + // On evenly divided width, we need one less adjustment on every
     271 + // column above the half, since two lines are taking the full
     272 + // height as shown above.
     273 + adjust--
     274 + }
     275 + return adjustVert(start, end, height, adjust)
     276 + }
     277 + adjust := num - halfWidth
     278 + return adjustVert(start, end, height, adjust)
     279 +}
     280 + 
     281 +// adjustHoriz given start and end points that identify a horizontal line,
     282 +// returns points that are adjusted towards each other on the line by the
     283 +// specified amount.
     284 +// I.e. the start is moved to the right and the end is moved to the left.
     285 +// The points won't be allowed to cross each other.
     286 +// The segWidth is the full width of the segment we are drawing.
     287 +func adjustHoriz(start, end image.Point, segWidth int, adjust int) (image.Point, image.Point) {
     288 + ns := start.Add(image.Point{adjust, 0})
     289 + ne := end.Sub(image.Point{adjust, 0})
     290 + 
     291 + if ns.X <= ne.X {
     292 + return ns, ne
     293 + }
     294 + 
     295 + halfWidth := segWidth / 2
     296 + if segWidth%2 == 0 {
     297 + // The width of the segment divides evenly, place start and end next to each other.
     298 + // E.g: 0 1 2 3 4 5
     299 + // - - ns ne - -
     300 + ns = image.Point{halfWidth - 1, start.Y}
     301 + ne = image.Point{halfWidth, end.Y}
     302 + } else {
     303 + // The width of the segment divides oddly, place both start and end on the mid point.
     304 + // E.g: 0 1 2 3 4
     305 + // - - nsne - -
     306 + ns = image.Point{halfWidth, start.Y}
     307 + ne = ns
     308 + }
     309 + return ns, ne
     310 +}
     311 + 
     312 +// adjustVert given start and end points that identify a vertical line,
     313 +// returns points that are adjusted towards each other on the line by the
     314 +// specified amount.
     315 +// I.e. the start is moved down and the end is moved up.
     316 +// The points won't be allowed to cross each other.
     317 +// The segHeight is the full height of the segment we are drawing.
     318 +func adjustVert(start, end image.Point, segHeight int, adjust int) (image.Point, image.Point) {
     319 + adjStart, adjEnd := adjustHoriz(swapCoord(start), swapCoord(end), segHeight, adjust)
     320 + return swapCoord(adjStart), swapCoord(adjEnd)
     321 +}
     322 + 
     323 +// swapCoord returns a point with its X and Y coordinates swapped.
     324 +func swapCoord(p image.Point) image.Point {
     325 + return image.Point{p.Y, p.X}
     326 +}
     327 + 
     328 +// DiagonalType determines the type of diagonal segment.
     329 +type DiagonalType int
     330 + 
     331 +// String implements fmt.Stringer()
     332 +func (dt DiagonalType) String() string {
     333 + if n, ok := diagonalTypeNames[dt]; ok {
     334 + return n
     335 + }
     336 + return "DiagonalTypeUnknown"
     337 +}
     338 + 
     339 +// diagonalTypeNames maps DiagonalType values to human readable names.
     340 +var diagonalTypeNames = map[DiagonalType]string{
     341 + LeftToRight: "LeftToRight",
     342 + RightToLeft: "RightToLeft",
     343 +}
     344 + 
     345 +const (
     346 + diagonalTypeUnknown DiagonalType = iota
     347 + // LeftToRight is a diagonal segment from top left to bottom right.
     348 + LeftToRight
     349 + // RightToLeft is a diagonal segment from top right to bottom left.
     350 + RightToLeft
     351 + 
     352 + diagonalTypeMax // Used for validation.
     353 +)
     354 + 
     355 +// nextDiagLineFn is a function that determines the start and end points of a line
     356 +// number num in a diagonal segment.
     357 +// Points start and end define the first diagonal exactly in the middle.
     358 +// Points prevStart and prevEnd define line num-1.
     359 +type nextDiagLineFn func(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point)
     360 + 
     361 +// DiagonalOption is used to provide options.
     362 +type DiagonalOption interface {
     363 + // set sets the provided option.
     364 + set(*diagonalOptions)
     365 +}
     366 + 
     367 +// diagonalOptions stores the provided diagonal options.
     368 +type diagonalOptions struct {
     369 + cellOpts []cell.Option
     370 +}
     371 + 
     372 +// diagonalOption implements DiagonalOption.
     373 +type diagonalOption func(*diagonalOptions)
     374 + 
     375 +// set implements DiagonalOption.set.
     376 +func (o diagonalOption) set(opts *diagonalOptions) {
     377 + o(opts)
     378 +}
     379 + 
     380 +// DiagonalCellOpts sets options on the cells that contain the diagonal
     381 +// segment.
     382 +// Cell options on a braille canvas can only be set on the entire cell, not per
     383 +// pixel.
     384 +func DiagonalCellOpts(cOpts ...cell.Option) DiagonalOption {
     385 + return diagonalOption(func(opts *diagonalOptions) {
     386 + opts.cellOpts = cOpts
     387 + })
     388 +}
     389 + 
     390 +// Diagonal draws a diagonal segment of the specified width filling the area.
     391 +func Diagonal(bc *braille.Canvas, ar image.Rectangle, width int, dt DiagonalType, opts ...DiagonalOption) error {
     392 + if err := validArea(ar); err != nil {
     393 + return err
     394 + }
     395 + if min := 1; width < min {
     396 + return fmt.Errorf("invalid width %d, must be width >= %d", width, min)
     397 + }
     398 + opt := &diagonalOptions{}
     399 + for _, o := range opts {
     400 + o.set(opt)
     401 + }
     402 + 
     403 + var start, end image.Point
     404 + var nextFn nextDiagLineFn
     405 + switch dt {
     406 + case LeftToRight:
     407 + start = ar.Min
     408 + end = image.Point{ar.Max.X - 1, ar.Max.Y - 1}
     409 + nextFn = nextLRLine
     410 + 
     411 + case RightToLeft:
     412 + start = image.Point{ar.Max.X - 1, ar.Min.Y}
     413 + end = image.Point{ar.Min.X, ar.Max.Y - 1}
     414 + nextFn = nextRLLine
     415 + 
     416 + default:
     417 + return fmt.Errorf("unsupported diagonal type %v(%d)", dt, dt)
     418 + }
     419 + 
     420 + if err := draw.BrailleLine(bc, start, end, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
     421 + return err
     422 + }
     423 + 
     424 + ns := start
     425 + ne := end
     426 + for i := 1; i < width; i++ {
     427 + ns, ne = nextFn(i, start, end, ns, ne)
     428 + 
     429 + if !ns.In(ar) || !ne.In(ar) {
     430 + 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)
     431 + }
     432 + 
     433 + if err := draw.BrailleLine(bc, ns, ne, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
     434 + return err
     435 + }
     436 + }
     437 + return nil
     438 +}
     439 + 
     440 +// nextLRLine is a function that determines the start and end points of the
     441 +// next line of a left-to-right diagonal segment.
     442 +func nextLRLine(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point) {
     443 + dist := num / 2
     444 + if num%2 != 0 {
     445 + // Every odd line is placed above the mid diagonal.
     446 + ns := image.Point{start.X + dist + 1, start.Y}
     447 + ne := image.Point{end.X, end.Y - dist - 1}
     448 + return ns, ne
     449 + }
     450 + 
     451 + // Every even line is placed under the mid diagonal.
     452 + ns := image.Point{start.X, start.Y + dist}
     453 + ne := image.Point{end.X - dist, end.Y}
     454 + return ns, ne
     455 +}
     456 + 
     457 +// nextRLLine is a function that determines the start and end points of the
     458 +// next line of a right-to-left diagonal segment.
     459 +func nextRLLine(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point) {
     460 + dist := num / 2
     461 + if num%2 != 0 {
     462 + // Every odd line is placed above the mid diagonal.
     463 + ns := image.Point{start.X - dist - 1, start.Y}
     464 + ne := image.Point{end.X, end.Y - dist - 1}
     465 + return ns, ne
     466 + }
     467 + 
     468 + // Every even line is placed under the mid diagonal.
     469 + ns := image.Point{start.X, start.Y + dist}
     470 + ne := image.Point{end.X + dist, end.Y}
     471 + return ns, ne
     472 +}
     473 + 
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/segment/segment_test.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +package segment
     16 + 
     17 +import (
     18 + "fmt"
     19 + "image"
     20 + "testing"
     21 + 
     22 + "github.com/mum4k/termdash/area"
     23 + "github.com/mum4k/termdash/canvas/braille"
     24 + "github.com/mum4k/termdash/canvas/braille/testbraille"
     25 + "github.com/mum4k/termdash/cell"
     26 + "github.com/mum4k/termdash/draw"
     27 + "github.com/mum4k/termdash/draw/testdraw"
     28 + "github.com/mum4k/termdash/terminal/faketerm"
     29 +)
     30 + 
     31 +func TestHV(t *testing.T) {
     32 + tests := []struct {
     33 + desc string
     34 + opts []Option
     35 + cellCanvas image.Rectangle // Canvas in cells that will be converted to braille canvas for drawing.
     36 + ar image.Rectangle
     37 + st Type
     38 + want func(size image.Point) *faketerm.Terminal
     39 + wantErr bool
     40 + }{
     41 + {
     42 + desc: "fails on area with negative Min.X",
     43 + cellCanvas: image.Rect(0, 0, 1, 1),
     44 + ar: image.Rect(-1, 0, 1, 1),
     45 + st: Horizontal,
     46 + wantErr: true,
     47 + },
     48 + {
     49 + desc: "fails on area with negative Min.Y",
     50 + cellCanvas: image.Rect(0, 0, 1, 1),
     51 + ar: image.Rect(0, -1, 1, 1),
     52 + st: Horizontal,
     53 + wantErr: true,
     54 + },
     55 + {
     56 + desc: "fails on area with negative Max.X",
     57 + cellCanvas: image.Rect(0, 0, 1, 1),
     58 + ar: image.Rectangle{image.Point{0, 0}, image.Point{-1, 1}},
     59 + st: Horizontal,
     60 + wantErr: true,
     61 + },
     62 + {
     63 + desc: "fails on area with negative Max.Y",
     64 + cellCanvas: image.Rect(0, 0, 1, 1),
     65 + ar: image.Rectangle{image.Point{0, 0}, image.Point{1, -1}},
     66 + st: Horizontal,
     67 + wantErr: true,
     68 + },
     69 + {
     70 + desc: "fails on area with zero Dx()",
     71 + cellCanvas: image.Rect(0, 0, 1, 1),
     72 + ar: image.Rect(0, 0, 0, 1),
     73 + st: Horizontal,
     74 + wantErr: true,
     75 + },
     76 + {
     77 + desc: "fails on area with zero Dy()",
     78 + cellCanvas: image.Rect(0, 0, 1, 1),
     79 + ar: image.Rect(0, 0, 1, 0),
     80 + st: Horizontal,
     81 + wantErr: true,
     82 + },
     83 + {
     84 + desc: "fails on unsupported segment type (too small)",
     85 + cellCanvas: image.Rect(0, 0, 1, 1),
     86 + ar: image.Rect(0, 0, 2, 2),
     87 + st: Type(0),
     88 + wantErr: true,
     89 + },
     90 + {
     91 + desc: "fails on unsupported segment type (too large)",
     92 + cellCanvas: image.Rect(0, 0, 1, 1),
     93 + ar: image.Rect(0, 0, 2, 2),
     94 + st: Type(int(Vertical) + 1),
     95 + wantErr: true,
     96 + },
     97 + {
     98 + desc: "fails on area larger than the canvas",
     99 + cellCanvas: image.Rect(0, 0, 1, 1),
     100 + ar: image.Rect(0, 0, 3, 1),
     101 + st: Horizontal,
     102 + wantErr: true,
     103 + },
     104 + {
     105 + desc: "sets cell options",
     106 + opts: []Option{
     107 + CellOpts(
     108 + cell.FgColor(cell.ColorRed),
     109 + cell.BgColor(cell.ColorGreen),
     110 + ),
     111 + },
     112 + cellCanvas: image.Rect(0, 0, 1, 1),
     113 + ar: image.Rect(0, 0, 1, 1),
     114 + st: Horizontal,
     115 + want: func(size image.Point) *faketerm.Terminal {
     116 + ft := faketerm.MustNew(size)
     117 + bc := testbraille.MustNew(ft.Area())
     118 + 
     119 + testbraille.MustSetPixel(bc, image.Point{0, 0}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorGreen))
     120 + testbraille.MustApply(bc, ft)
     121 + return ft
     122 + },
     123 + },
     124 + {
     125 + desc: "horizontal, segment 1x1",
     126 + cellCanvas: image.Rect(0, 0, 1, 1),
     127 + ar: image.Rect(0, 0, 1, 1),
     128 + st: Horizontal,
     129 + want: func(size image.Point) *faketerm.Terminal {
     130 + ft := faketerm.MustNew(size)
     131 + bc := testbraille.MustNew(ft.Area())
     132 + 
     133 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     134 + testbraille.MustApply(bc, ft)
     135 + return ft
     136 + },
     137 + },
     138 + {
     139 + desc: "horizontal, segment 1x2",
     140 + cellCanvas: image.Rect(0, 0, 1, 1),
     141 + ar: image.Rect(0, 0, 1, 2),
     142 + st: Horizontal,
     143 + want: func(size image.Point) *faketerm.Terminal {
     144 + ft := faketerm.MustNew(size)
     145 + bc := testbraille.MustNew(ft.Area())
     146 + 
     147 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     148 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{0, 1})
     149 + testbraille.MustApply(bc, ft)
     150 + return ft
     151 + },
     152 + },
     153 + {
     154 + desc: "horizontal, segment 1x3",
     155 + cellCanvas: image.Rect(0, 0, 1, 1),
     156 + ar: image.Rect(0, 0, 1, 3),
     157 + st: Horizontal,
     158 + want: func(size image.Point) *faketerm.Terminal {
     159 + ft := faketerm.MustNew(size)
     160 + bc := testbraille.MustNew(ft.Area())
     161 + 
     162 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     163 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{0, 1})
     164 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{0, 2})
     165 + testbraille.MustApply(bc, ft)
     166 + return ft
     167 + },
     168 + },
     169 + {
     170 + desc: "horizontal, segment 1x4",
     171 + cellCanvas: image.Rect(0, 0, 1, 1),
     172 + ar: image.Rect(0, 0, 1, 4),
     173 + st: Horizontal,
     174 + want: func(size image.Point) *faketerm.Terminal {
     175 + ft := faketerm.MustNew(size)
     176 + bc := testbraille.MustNew(ft.Area())
     177 + 
     178 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     179 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{0, 1})
     180 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{0, 2})
     181 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{0, 3})
     182 + testbraille.MustApply(bc, ft)
     183 + return ft
     184 + },
     185 + },
     186 + {
     187 + desc: "horizontal, segment 1x5",
     188 + cellCanvas: image.Rect(0, 0, 1, 2),
     189 + ar: image.Rect(0, 0, 1, 5),
     190 + st: Horizontal,
     191 + want: func(size image.Point) *faketerm.Terminal {
     192 + ft := faketerm.MustNew(size)
     193 + bc := testbraille.MustNew(ft.Area())
     194 + 
     195 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     196 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{0, 1})
     197 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{0, 2})
     198 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{0, 3})
     199 + testdraw.MustBrailleLine(bc, image.Point{0, 4}, image.Point{0, 4})
     200 + testbraille.MustApply(bc, ft)
     201 + return ft
     202 + },
     203 + },
     204 + {
     205 + desc: "horizontal, segment 2x1",
     206 + cellCanvas: image.Rect(0, 0, 1, 1),
     207 + ar: image.Rect(0, 0, 2, 1),
     208 + st: Horizontal,
     209 + want: func(size image.Point) *faketerm.Terminal {
     210 + ft := faketerm.MustNew(size)
     211 + bc := testbraille.MustNew(ft.Area())
     212 + 
     213 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{1, 0})
     214 + testbraille.MustApply(bc, ft)
     215 + return ft
     216 + },
     217 + },
     218 + {
     219 + desc: "horizontal, segment 2x2",
     220 + cellCanvas: image.Rect(0, 0, 1, 1),
     221 + ar: image.Rect(0, 0, 2, 2),
     222 + st: Horizontal,
     223 + want: func(size image.Point) *faketerm.Terminal {
     224 + ft := faketerm.MustNew(size)
     225 + bc := testbraille.MustNew(ft.Area())
     226 + 
     227 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{1, 0})
     228 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     229 + testbraille.MustApply(bc, ft)
     230 + return ft
     231 + },
     232 + },
     233 + {
     234 + desc: "horizontal, segment 2x3",
     235 + cellCanvas: image.Rect(0, 0, 1, 1),
     236 + ar: image.Rect(0, 0, 2, 3),
     237 + st: Horizontal,
     238 + want: func(size image.Point) *faketerm.Terminal {
     239 + ft := faketerm.MustNew(size)
     240 + bc := testbraille.MustNew(ft.Area())
     241 + 
     242 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{1, 0})
     243 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     244 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{1, 2})
     245 + testbraille.MustApply(bc, ft)
     246 + return ft
     247 + },
     248 + },
     249 + {
     250 + desc: "horizontal, segment 2x4",
     251 + cellCanvas: image.Rect(0, 0, 1, 1),
     252 + ar: image.Rect(0, 0, 2, 4),
     253 + st: Horizontal,
     254 + want: func(size image.Point) *faketerm.Terminal {
     255 + ft := faketerm.MustNew(size)
     256 + bc := testbraille.MustNew(ft.Area())
     257 + 
     258 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{1, 0})
     259 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     260 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{1, 2})
     261 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{1, 3})
     262 + testbraille.MustApply(bc, ft)
     263 + return ft
     264 + },
     265 + },
     266 + {
     267 + desc: "horizontal, segment 2x5",
     268 + cellCanvas: image.Rect(0, 0, 1, 2),
     269 + ar: image.Rect(0, 0, 2, 5),
     270 + st: Horizontal,
     271 + want: func(size image.Point) *faketerm.Terminal {
     272 + ft := faketerm.MustNew(size)
     273 + bc := testbraille.MustNew(ft.Area())
     274 + 
     275 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{1, 0})
     276 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     277 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{1, 2})
     278 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{1, 3})
     279 + testdraw.MustBrailleLine(bc, image.Point{0, 4}, image.Point{1, 4})
     280 + testbraille.MustApply(bc, ft)
     281 + return ft
     282 + },
     283 + },
     284 + {
     285 + desc: "horizontal, segment 3x1",
     286 + cellCanvas: image.Rect(0, 0, 2, 1),
     287 + ar: image.Rect(0, 0, 3, 1),
     288 + st: Horizontal,
     289 + want: func(size image.Point) *faketerm.Terminal {
     290 + ft := faketerm.MustNew(size)
     291 + bc := testbraille.MustNew(ft.Area())
     292 + 
     293 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{2, 0})
     294 + testbraille.MustApply(bc, ft)
     295 + return ft
     296 + },
     297 + },
     298 + {
     299 + desc: "horizontal, segment 3x2",
     300 + cellCanvas: image.Rect(0, 0, 2, 1),
     301 + ar: image.Rect(0, 0, 3, 2),
     302 + st: Horizontal,
     303 + want: func(size image.Point) *faketerm.Terminal {
     304 + ft := faketerm.MustNew(size)
     305 + bc := testbraille.MustNew(ft.Area())
     306 + 
     307 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     308 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     309 + testbraille.MustApply(bc, ft)
     310 + return ft
     311 + },
     312 + },
     313 + {
     314 + desc: "horizontal, segment 3x2, reverse slopes",
     315 + opts: []Option{
     316 + ReverseSlopes(),
     317 + },
     318 + cellCanvas: image.Rect(0, 0, 2, 1),
     319 + ar: image.Rect(0, 0, 3, 2),
     320 + st: Horizontal,
     321 + want: func(size image.Point) *faketerm.Terminal {
     322 + ft := faketerm.MustNew(size)
     323 + bc := testbraille.MustNew(ft.Area())
     324 + 
     325 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{2, 0})
     326 + testdraw.MustBrailleLine(bc, image.Point{1, 1}, image.Point{1, 1})
     327 + testbraille.MustApply(bc, ft)
     328 + return ft
     329 + },
     330 + },
     331 + {
     332 + desc: "horizontal, segment 3x3",
     333 + cellCanvas: image.Rect(0, 0, 2, 1),
     334 + ar: image.Rect(0, 0, 3, 3),
     335 + st: Horizontal,
     336 + want: func(size image.Point) *faketerm.Terminal {
     337 + ft := faketerm.MustNew(size)
     338 + bc := testbraille.MustNew(ft.Area())
     339 + 
     340 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     341 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     342 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{1, 2})
     343 + testbraille.MustApply(bc, ft)
     344 + return ft
     345 + },
     346 + },
     347 + {
     348 + desc: "horizontal, segment 3x3, reverse slopes has no effect on larger height",
     349 + opts: []Option{
     350 + ReverseSlopes(),
     351 + },
     352 + cellCanvas: image.Rect(0, 0, 2, 1),
     353 + ar: image.Rect(0, 0, 3, 3),
     354 + st: Horizontal,
     355 + want: func(size image.Point) *faketerm.Terminal {
     356 + ft := faketerm.MustNew(size)
     357 + bc := testbraille.MustNew(ft.Area())
     358 + 
     359 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     360 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     361 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{1, 2})
     362 + testbraille.MustApply(bc, ft)
     363 + return ft
     364 + },
     365 + },
     366 + {
     367 + desc: "horizontal, segment 3x3, skip slopes",
     368 + opts: []Option{
     369 + SkipSlopesLTE(3),
     370 + },
     371 + cellCanvas: image.Rect(0, 0, 2, 1),
     372 + ar: image.Rect(0, 0, 3, 3),
     373 + st: Horizontal,
     374 + want: func(size image.Point) *faketerm.Terminal {
     375 + ft := faketerm.MustNew(size)
     376 + bc := testbraille.MustNew(ft.Area())
     377 + 
     378 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{2, 0})
     379 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     380 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{2, 2})
     381 + testbraille.MustApply(bc, ft)
     382 + return ft
     383 + },
     384 + },
     385 + {
     386 + desc: "horizontal, segment 3x3, doesn't skip slopes because is taller",
     387 + opts: []Option{
     388 + SkipSlopesLTE(2),
     389 + },
     390 + cellCanvas: image.Rect(0, 0, 2, 1),
     391 + ar: image.Rect(0, 0, 3, 3),
     392 + st: Horizontal,
     393 + want: func(size image.Point) *faketerm.Terminal {
     394 + ft := faketerm.MustNew(size)
     395 + bc := testbraille.MustNew(ft.Area())
     396 + 
     397 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     398 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     399 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{1, 2})
     400 + testbraille.MustApply(bc, ft)
     401 + return ft
     402 + },
     403 + },
     404 + {
     405 + desc: "horizontal, segment 3x4",
     406 + cellCanvas: image.Rect(0, 0, 2, 1),
     407 + ar: image.Rect(0, 0, 3, 4),
     408 + st: Horizontal,
     409 + want: func(size image.Point) *faketerm.Terminal {
     410 + ft := faketerm.MustNew(size)
     411 + bc := testbraille.MustNew(ft.Area())
     412 + 
     413 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     414 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     415 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{2, 2})
     416 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{1, 3})
     417 + testbraille.MustApply(bc, ft)
     418 + return ft
     419 + },
     420 + },
     421 + {
     422 + desc: "horizontal, segment 3x5",
     423 + cellCanvas: image.Rect(0, 0, 2, 2),
     424 + ar: image.Rect(0, 0, 3, 5),
     425 + st: Horizontal,
     426 + want: func(size image.Point) *faketerm.Terminal {
     427 + ft := faketerm.MustNew(size)
     428 + bc := testbraille.MustNew(ft.Area())
     429 + 
     430 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     431 + testdraw.MustBrailleLine(bc, image.Point{1, 1}, image.Point{1, 1})
     432 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{2, 2})
     433 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{1, 3})
     434 + testdraw.MustBrailleLine(bc, image.Point{1, 4}, image.Point{1, 4})
     435 + testbraille.MustApply(bc, ft)
     436 + return ft
     437 + },
     438 + },
     439 + {
     440 + desc: "horizontal, segment 4x1",
     441 + cellCanvas: image.Rect(0, 0, 2, 1),
     442 + ar: image.Rect(0, 0, 4, 1),
     443 + st: Horizontal,
     444 + want: func(size image.Point) *faketerm.Terminal {
     445 + ft := faketerm.MustNew(size)
     446 + bc := testbraille.MustNew(ft.Area())
     447 + 
     448 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 0})
     449 + testbraille.MustApply(bc, ft)
     450 + return ft
     451 + },
     452 + },
     453 + {
     454 + desc: "horizontal, segment 4x2",
     455 + cellCanvas: image.Rect(0, 0, 2, 1),
     456 + ar: image.Rect(0, 0, 4, 2),
     457 + st: Horizontal,
     458 + want: func(size image.Point) *faketerm.Terminal {
     459 + ft := faketerm.MustNew(size)
     460 + bc := testbraille.MustNew(ft.Area())
     461 + 
     462 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{2, 0})
     463 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{3, 1})
     464 + testbraille.MustApply(bc, ft)
     465 + return ft
     466 + },
     467 + },
     468 + {
     469 + desc: "horizontal, segment 4x3",
     470 + cellCanvas: image.Rect(0, 0, 2, 1),
     471 + ar: image.Rect(0, 0, 4, 3),
     472 + st: Horizontal,
     473 + want: func(size image.Point) *faketerm.Terminal {
     474 + ft := faketerm.MustNew(size)
     475 + bc := testbraille.MustNew(ft.Area())
     476 + 
     477 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{2, 0})
     478 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{3, 1})
     479 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{2, 2})
     480 + testbraille.MustApply(bc, ft)
     481 + return ft
     482 + },
     483 + },
     484 + {
     485 + desc: "horizontal, segment 4x4",
     486 + cellCanvas: image.Rect(0, 0, 2, 1),
     487 + ar: image.Rect(0, 0, 4, 4),
     488 + st: Horizontal,
     489 + want: func(size image.Point) *faketerm.Terminal {
     490 + ft := faketerm.MustNew(size)
     491 + bc := testbraille.MustNew(ft.Area())
     492 + 
     493 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{2, 0})
     494 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{3, 1})
     495 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{3, 2})
     496 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{2, 3})
     497 + testbraille.MustApply(bc, ft)
     498 + return ft
     499 + },
     500 + },
     501 + {
     502 + desc: "horizontal, segment 4x5",
     503 + cellCanvas: image.Rect(0, 0, 2, 2),
     504 + ar: image.Rect(0, 0, 4, 5),
     505 + st: Horizontal,
     506 + want: func(size image.Point) *faketerm.Terminal {
     507 + ft := faketerm.MustNew(size)
     508 + bc := testbraille.MustNew(ft.Area())
     509 + 
     510 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{2, 0})
     511 + testdraw.MustBrailleLine(bc, image.Point{1, 1}, image.Point{2, 1})
     512 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{3, 2})
     513 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{2, 3})
     514 + testdraw.MustBrailleLine(bc, image.Point{1, 4}, image.Point{2, 4})
     515 + testbraille.MustApply(bc, ft)
     516 + return ft
     517 + },
     518 + },
     519 + {
     520 + desc: "horizontal, segment 5x1",
     521 + cellCanvas: image.Rect(0, 0, 3, 1),
     522 + ar: image.Rect(0, 0, 5, 1),
     523 + st: Horizontal,
     524 + want: func(size image.Point) *faketerm.Terminal {
     525 + ft := faketerm.MustNew(size)
     526 + bc := testbraille.MustNew(ft.Area())
     527 + 
     528 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{4, 0})
     529 + testbraille.MustApply(bc, ft)
     530 + return ft
     531 + },
     532 + },
     533 + {
     534 + desc: "horizontal, segment 5x2",
     535 + cellCanvas: image.Rect(0, 0, 3, 1),
     536 + ar: image.Rect(0, 0, 5, 2),
     537 + st: Horizontal,
     538 + want: func(size image.Point) *faketerm.Terminal {
     539 + ft := faketerm.MustNew(size)
     540 + bc := testbraille.MustNew(ft.Area())
     541 + 
     542 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 0})
     543 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{4, 1})
     544 + testbraille.MustApply(bc, ft)
     545 + return ft
     546 + },
     547 + },
     548 + {
     549 + desc: "horizontal, segment 5x3",
     550 + cellCanvas: image.Rect(0, 0, 3, 1),
     551 + ar: image.Rect(0, 0, 5, 3),
     552 + st: Horizontal,
     553 + want: func(size image.Point) *faketerm.Terminal {
     554 + ft := faketerm.MustNew(size)
     555 + bc := testbraille.MustNew(ft.Area())
     556 + 
     557 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 0})
     558 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{4, 1})
     559 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{3, 2})
     560 + testbraille.MustApply(bc, ft)
     561 + return ft
     562 + },
     563 + },
     564 + {
     565 + desc: "horizontal, segment 5x4",
     566 + cellCanvas: image.Rect(0, 0, 3, 1),
     567 + ar: image.Rect(0, 0, 5, 4),
     568 + st: Horizontal,
     569 + want: func(size image.Point) *faketerm.Terminal {
     570 + ft := faketerm.MustNew(size)
     571 + bc := testbraille.MustNew(ft.Area())
     572 + 
     573 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 0})
     574 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{4, 1})
     575 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{4, 2})
     576 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{3, 3})
     577 + testbraille.MustApply(bc, ft)
     578 + return ft
     579 + },
     580 + },
     581 + {
     582 + desc: "horizontal, segment 5x5",
     583 + cellCanvas: image.Rect(0, 0, 3, 2),
     584 + ar: image.Rect(0, 0, 5, 5),
     585 + st: Horizontal,
     586 + want: func(size image.Point) *faketerm.Terminal {
     587 + ft := faketerm.MustNew(size)
     588 + bc := testbraille.MustNew(ft.Area())
     589 + 
     590 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{2, 0})
     591 + testdraw.MustBrailleLine(bc, image.Point{1, 1}, image.Point{3, 1})
     592 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{4, 2})
     593 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{3, 3})
     594 + testdraw.MustBrailleLine(bc, image.Point{2, 4}, image.Point{2, 4})
     595 + testbraille.MustApply(bc, ft)
     596 + return ft
     597 + },
     598 + },
     599 + {
     600 + desc: "vertical, segment 1x1",
     601 + cellCanvas: image.Rect(0, 0, 1, 1),
     602 + ar: image.Rect(0, 0, 1, 1),
     603 + st: Vertical,
     604 + want: func(size image.Point) *faketerm.Terminal {
     605 + ft := faketerm.MustNew(size)
     606 + bc := testbraille.MustNew(ft.Area())
     607 + 
     608 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     609 + testbraille.MustApply(bc, ft)
     610 + return ft
     611 + },
     612 + },
     613 + {
     614 + desc: "vertical, segment 1x2",
     615 + cellCanvas: image.Rect(0, 0, 1, 1),
     616 + ar: image.Rect(0, 0, 1, 2),
     617 + st: Vertical,
     618 + want: func(size image.Point) *faketerm.Terminal {
     619 + ft := faketerm.MustNew(size)
     620 + bc := testbraille.MustNew(ft.Area())
     621 + 
     622 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     623 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{0, 1})
     624 + testbraille.MustApply(bc, ft)
     625 + return ft
     626 + },
     627 + },
     628 + {
     629 + desc: "vertical, segment 1x3",
     630 + cellCanvas: image.Rect(0, 0, 1, 1),
     631 + ar: image.Rect(0, 0, 1, 3),
     632 + st: Vertical,
     633 + want: func(size image.Point) *faketerm.Terminal {
     634 + ft := faketerm.MustNew(size)
     635 + bc := testbraille.MustNew(ft.Area())
     636 + 
     637 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     638 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{0, 1})
     639 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{0, 2})
     640 + testbraille.MustApply(bc, ft)
     641 + return ft
     642 + },
     643 + },
     644 + {
     645 + desc: "vertical, segment 1x4",
     646 + cellCanvas: image.Rect(0, 0, 1, 1),
     647 + ar: image.Rect(0, 0, 1, 4),
     648 + st: Vertical,
     649 + want: func(size image.Point) *faketerm.Terminal {
     650 + ft := faketerm.MustNew(size)
     651 + bc := testbraille.MustNew(ft.Area())
     652 + 
     653 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     654 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{0, 1})
     655 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{0, 2})
     656 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{0, 3})
     657 + testbraille.MustApply(bc, ft)
     658 + return ft
     659 + },
     660 + },
     661 + {
     662 + desc: "vertical, segment 1x5",
     663 + cellCanvas: image.Rect(0, 0, 1, 2),
     664 + ar: image.Rect(0, 0, 1, 5),
     665 + st: Vertical,
     666 + want: func(size image.Point) *faketerm.Terminal {
     667 + ft := faketerm.MustNew(size)
     668 + bc := testbraille.MustNew(ft.Area())
     669 + 
     670 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     671 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{0, 1})
     672 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{0, 2})
     673 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{0, 3})
     674 + testdraw.MustBrailleLine(bc, image.Point{0, 4}, image.Point{0, 4})
     675 + testbraille.MustApply(bc, ft)
     676 + return ft
     677 + },
     678 + },
     679 + {
     680 + desc: "vertical, segment 2x1",
     681 + cellCanvas: image.Rect(0, 0, 1, 1),
     682 + ar: image.Rect(0, 0, 2, 1),
     683 + st: Vertical,
     684 + want: func(size image.Point) *faketerm.Terminal {
     685 + ft := faketerm.MustNew(size)
     686 + bc := testbraille.MustNew(ft.Area())
     687 + 
     688 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{1, 0})
     689 + testbraille.MustApply(bc, ft)
     690 + return ft
     691 + },
     692 + },
     693 + {
     694 + desc: "vertical, segment 2x2",
     695 + cellCanvas: image.Rect(0, 0, 1, 1),
     696 + ar: image.Rect(0, 0, 2, 2),
     697 + st: Vertical,
     698 + want: func(size image.Point) *faketerm.Terminal {
     699 + ft := faketerm.MustNew(size)
     700 + bc := testbraille.MustNew(ft.Area())
     701 + 
     702 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{1, 0})
     703 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     704 + testbraille.MustApply(bc, ft)
     705 + return ft
     706 + },
     707 + },
     708 + {
     709 + desc: "vertical, segment 2x3",
     710 + cellCanvas: image.Rect(0, 0, 1, 1),
     711 + ar: image.Rect(0, 0, 2, 3),
     712 + st: Vertical,
     713 + want: func(size image.Point) *faketerm.Terminal {
     714 + ft := faketerm.MustNew(size)
     715 + bc := testbraille.MustNew(ft.Area())
     716 + 
     717 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     718 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     719 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{1, 2})
     720 + testbraille.MustApply(bc, ft)
     721 + return ft
     722 + },
     723 + },
     724 + {
     725 + desc: "vertical, segment 2x3, reverse slopes",
     726 + opts: []Option{
     727 + ReverseSlopes(),
     728 + },
     729 + cellCanvas: image.Rect(0, 0, 1, 1),
     730 + ar: image.Rect(0, 0, 2, 3),
     731 + st: Vertical,
     732 + want: func(size image.Point) *faketerm.Terminal {
     733 + ft := faketerm.MustNew(size)
     734 + bc := testbraille.MustNew(ft.Area())
     735 + 
     736 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     737 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     738 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{0, 2})
     739 + testbraille.MustApply(bc, ft)
     740 + return ft
     741 + },
     742 + },
     743 + {
     744 + desc: "vertical, segment 2x4",
     745 + cellCanvas: image.Rect(0, 0, 1, 1),
     746 + ar: image.Rect(0, 0, 2, 4),
     747 + st: Vertical,
     748 + want: func(size image.Point) *faketerm.Terminal {
     749 + ft := faketerm.MustNew(size)
     750 + bc := testbraille.MustNew(ft.Area())
     751 + 
     752 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     753 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     754 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{1, 2})
     755 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{1, 3})
     756 + testbraille.MustApply(bc, ft)
     757 + return ft
     758 + },
     759 + },
     760 + {
     761 + desc: "vertical, segment 2x5",
     762 + cellCanvas: image.Rect(0, 0, 1, 2),
     763 + ar: image.Rect(0, 0, 2, 5),
     764 + st: Vertical,
     765 + want: func(size image.Point) *faketerm.Terminal {
     766 + ft := faketerm.MustNew(size)
     767 + bc := testbraille.MustNew(ft.Area())
     768 + 
     769 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     770 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{1, 1})
     771 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{1, 2})
     772 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{1, 3})
     773 + testdraw.MustBrailleLine(bc, image.Point{1, 4}, image.Point{1, 4})
     774 + testbraille.MustApply(bc, ft)
     775 + return ft
     776 + },
     777 + },
     778 + {
     779 + desc: "vertical, segment 3x1",
     780 + cellCanvas: image.Rect(0, 0, 2, 1),
     781 + ar: image.Rect(0, 0, 3, 1),
     782 + st: Vertical,
     783 + want: func(size image.Point) *faketerm.Terminal {
     784 + ft := faketerm.MustNew(size)
     785 + bc := testbraille.MustNew(ft.Area())
     786 + 
     787 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{2, 0})
     788 + testbraille.MustApply(bc, ft)
     789 + return ft
     790 + },
     791 + },
     792 + {
     793 + desc: "vertical, segment 3x2",
     794 + cellCanvas: image.Rect(0, 0, 2, 1),
     795 + ar: image.Rect(0, 0, 3, 2),
     796 + st: Vertical,
     797 + want: func(size image.Point) *faketerm.Terminal {
     798 + ft := faketerm.MustNew(size)
     799 + bc := testbraille.MustNew(ft.Area())
     800 + 
     801 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{2, 0})
     802 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     803 + testbraille.MustApply(bc, ft)
     804 + return ft
     805 + },
     806 + },
     807 + {
     808 + desc: "vertical, segment 3x3",
     809 + cellCanvas: image.Rect(0, 0, 2, 1),
     810 + ar: image.Rect(0, 0, 3, 3),
     811 + st: Vertical,
     812 + want: func(size image.Point) *faketerm.Terminal {
     813 + ft := faketerm.MustNew(size)
     814 + bc := testbraille.MustNew(ft.Area())
     815 + 
     816 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     817 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     818 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{1, 2})
     819 + testbraille.MustApply(bc, ft)
     820 + return ft
     821 + },
     822 + },
     823 + {
     824 + desc: "vertical, segment 3x3, reverse slopes has no effect on larger width",
     825 + opts: []Option{
     826 + ReverseSlopes(),
     827 + },
     828 + cellCanvas: image.Rect(0, 0, 2, 1),
     829 + ar: image.Rect(0, 0, 3, 3),
     830 + st: Vertical,
     831 + want: func(size image.Point) *faketerm.Terminal {
     832 + ft := faketerm.MustNew(size)
     833 + bc := testbraille.MustNew(ft.Area())
     834 + 
     835 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     836 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     837 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{1, 2})
     838 + testbraille.MustApply(bc, ft)
     839 + return ft
     840 + },
     841 + },
     842 + {
     843 + desc: "vertical, segment 3x3, skips slopes",
     844 + opts: []Option{
     845 + SkipSlopesLTE(3),
     846 + },
     847 + cellCanvas: image.Rect(0, 0, 2, 1),
     848 + ar: image.Rect(0, 0, 3, 3),
     849 + st: Vertical,
     850 + want: func(size image.Point) *faketerm.Terminal {
     851 + ft := faketerm.MustNew(size)
     852 + bc := testbraille.MustNew(ft.Area())
     853 + 
     854 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{2, 0})
     855 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     856 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{2, 2})
     857 + testbraille.MustApply(bc, ft)
     858 + return ft
     859 + },
     860 + },
     861 + {
     862 + desc: "vertical, segment 3x3, doesn't skips slopes because is wider",
     863 + opts: []Option{
     864 + SkipSlopesLTE(2),
     865 + },
     866 + cellCanvas: image.Rect(0, 0, 2, 1),
     867 + ar: image.Rect(0, 0, 3, 3),
     868 + st: Vertical,
     869 + want: func(size image.Point) *faketerm.Terminal {
     870 + ft := faketerm.MustNew(size)
     871 + bc := testbraille.MustNew(ft.Area())
     872 + 
     873 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     874 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     875 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{1, 2})
     876 + testbraille.MustApply(bc, ft)
     877 + return ft
     878 + },
     879 + },
     880 + {
     881 + desc: "vertical, segment 3x4",
     882 + cellCanvas: image.Rect(0, 0, 2, 1),
     883 + ar: image.Rect(0, 0, 3, 4),
     884 + st: Vertical,
     885 + want: func(size image.Point) *faketerm.Terminal {
     886 + ft := faketerm.MustNew(size)
     887 + bc := testbraille.MustNew(ft.Area())
     888 + 
     889 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     890 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     891 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{2, 2})
     892 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{1, 3})
     893 + testbraille.MustApply(bc, ft)
     894 + return ft
     895 + },
     896 + },
     897 + {
     898 + desc: "vertical, segment 3x5",
     899 + cellCanvas: image.Rect(0, 0, 2, 2),
     900 + ar: image.Rect(0, 0, 3, 5),
     901 + st: Vertical,
     902 + want: func(size image.Point) *faketerm.Terminal {
     903 + ft := faketerm.MustNew(size)
     904 + bc := testbraille.MustNew(ft.Area())
     905 + 
     906 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{1, 0})
     907 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 1})
     908 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{2, 2})
     909 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{2, 3})
     910 + testdraw.MustBrailleLine(bc, image.Point{1, 4}, image.Point{1, 4})
     911 + testbraille.MustApply(bc, ft)
     912 + return ft
     913 + },
     914 + },
     915 + {
     916 + desc: "vertical, segment 4x1",
     917 + cellCanvas: image.Rect(0, 0, 2, 1),
     918 + ar: image.Rect(0, 0, 4, 1),
     919 + st: Vertical,
     920 + want: func(size image.Point) *faketerm.Terminal {
     921 + ft := faketerm.MustNew(size)
     922 + bc := testbraille.MustNew(ft.Area())
     923 + 
     924 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 0})
     925 + testbraille.MustApply(bc, ft)
     926 + return ft
     927 + },
     928 + },
     929 + {
     930 + desc: "vertical, segment 4x2",
     931 + cellCanvas: image.Rect(0, 0, 2, 1),
     932 + ar: image.Rect(0, 0, 4, 2),
     933 + st: Vertical,
     934 + want: func(size image.Point) *faketerm.Terminal {
     935 + ft := faketerm.MustNew(size)
     936 + bc := testbraille.MustNew(ft.Area())
     937 + 
     938 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 0})
     939 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{3, 1})
     940 + testbraille.MustApply(bc, ft)
     941 + return ft
     942 + },
     943 + },
     944 + {
     945 + desc: "vertical, segment 4x3",
     946 + cellCanvas: image.Rect(0, 0, 2, 1),
     947 + ar: image.Rect(0, 0, 4, 3),
     948 + st: Vertical,
     949 + want: func(size image.Point) *faketerm.Terminal {
     950 + ft := faketerm.MustNew(size)
     951 + bc := testbraille.MustNew(ft.Area())
     952 + 
     953 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{2, 0})
     954 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{3, 1})
     955 + testdraw.MustBrailleLine(bc, image.Point{1, 2}, image.Point{2, 2})
     956 + testbraille.MustApply(bc, ft)
     957 + return ft
     958 + },
     959 + },
     960 + {
     961 + desc: "vertical, segment 4x4",
     962 + cellCanvas: image.Rect(0, 0, 2, 1),
     963 + ar: image.Rect(0, 0, 4, 4),
     964 + st: Vertical,
     965 + want: func(size image.Point) *faketerm.Terminal {
     966 + ft := faketerm.MustNew(size)
     967 + bc := testbraille.MustNew(ft.Area())
     968 + 
     969 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{2, 0})
     970 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{3, 1})
     971 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{3, 2})
     972 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{2, 3})
     973 + testbraille.MustApply(bc, ft)
     974 + return ft
     975 + },
     976 + },
     977 + {
     978 + desc: "vertical, segment 4x5",
     979 + cellCanvas: image.Rect(0, 0, 2, 2),
     980 + ar: image.Rect(0, 0, 4, 5),
     981 + st: Vertical,
     982 + want: func(size image.Point) *faketerm.Terminal {
     983 + ft := faketerm.MustNew(size)
     984 + bc := testbraille.MustNew(ft.Area())
     985 + 
     986 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{2, 0})
     987 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{3, 1})
     988 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{3, 2})
     989 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{3, 3})
     990 + testdraw.MustBrailleLine(bc, image.Point{1, 4}, image.Point{2, 4})
     991 + testbraille.MustApply(bc, ft)
     992 + return ft
     993 + },
     994 + },
     995 + {
     996 + desc: "vertical, segment 5x1",
     997 + cellCanvas: image.Rect(0, 0, 3, 1),
     998 + ar: image.Rect(0, 0, 5, 1),
     999 + st: Vertical,
     1000 + want: func(size image.Point) *faketerm.Terminal {
     1001 + ft := faketerm.MustNew(size)
     1002 + bc := testbraille.MustNew(ft.Area())
     1003 + 
     1004 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{4, 0})
     1005 + testbraille.MustApply(bc, ft)
     1006 + return ft
     1007 + },
     1008 + },
     1009 + {
     1010 + desc: "vertical, segment 5x2",
     1011 + cellCanvas: image.Rect(0, 0, 3, 1),
     1012 + ar: image.Rect(0, 0, 5, 2),
     1013 + st: Vertical,
     1014 + want: func(size image.Point) *faketerm.Terminal {
     1015 + ft := faketerm.MustNew(size)
     1016 + bc := testbraille.MustNew(ft.Area())
     1017 + 
     1018 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{4, 0})
     1019 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{4, 1})
     1020 + testbraille.MustApply(bc, ft)
     1021 + return ft
     1022 + },
     1023 + },
     1024 + {
     1025 + desc: "vertical, segment 5x3",
     1026 + cellCanvas: image.Rect(0, 0, 3, 1),
     1027 + ar: image.Rect(0, 0, 5, 3),
     1028 + st: Vertical,
     1029 + want: func(size image.Point) *faketerm.Terminal {
     1030 + ft := faketerm.MustNew(size)
     1031 + bc := testbraille.MustNew(ft.Area())
     1032 + 
     1033 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{2, 0})
     1034 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{4, 1})
     1035 + testdraw.MustBrailleLine(bc, image.Point{2, 2}, image.Point{2, 2})
     1036 + testbraille.MustApply(bc, ft)
     1037 + return ft
     1038 + },
     1039 + },
     1040 + {
     1041 + desc: "vertical, segment 5x4",
     1042 + cellCanvas: image.Rect(0, 0, 3, 1),
     1043 + ar: image.Rect(0, 0, 5, 4),
     1044 + st: Vertical,
     1045 + want: func(size image.Point) *faketerm.Terminal {
     1046 + ft := faketerm.MustNew(size)
     1047 + bc := testbraille.MustNew(ft.Area())
     1048 + 
     1049 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{2, 0})
     1050 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{4, 1})
     1051 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{4, 2})
     1052 + testdraw.MustBrailleLine(bc, image.Point{2, 3}, image.Point{2, 3})
     1053 + testbraille.MustApply(bc, ft)
     1054 + return ft
     1055 + },
     1056 + },
     1057 + {
     1058 + desc: "vertical, segment 5x5",
     1059 + cellCanvas: image.Rect(0, 0, 3, 2),
     1060 + ar: image.Rect(0, 0, 5, 5),
     1061 + st: Vertical,
     1062 + want: func(size image.Point) *faketerm.Terminal {
     1063 + ft := faketerm.MustNew(size)
     1064 + bc := testbraille.MustNew(ft.Area())
     1065 + 
     1066 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{2, 0})
     1067 + testdraw.MustBrailleLine(bc, image.Point{1, 1}, image.Point{3, 1})
     1068 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{4, 2})
     1069 + testdraw.MustBrailleLine(bc, image.Point{1, 3}, image.Point{3, 3})
     1070 + testdraw.MustBrailleLine(bc, image.Point{2, 4}, image.Point{2, 4})
     1071 + testbraille.MustApply(bc, ft)
     1072 + return ft
     1073 + },
     1074 + },
     1075 + }
     1076 + 
     1077 + for _, tc := range tests {
     1078 + t.Run(fmt.Sprintf("%s st:%v", tc.desc, tc.st), func(t *testing.T) {
     1079 + bc, err := braille.New(tc.cellCanvas)
     1080 + if err != nil {
     1081 + t.Fatalf("braille.New => unexpected error: %v", err)
     1082 + }
     1083 + 
     1084 + err = HV(bc, tc.ar, tc.st, tc.opts...)
     1085 + if (err != nil) != tc.wantErr {
     1086 + t.Errorf("HV => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     1087 + }
     1088 + if err != nil {
     1089 + return
     1090 + }
     1091 + 
     1092 + size := area.Size(tc.cellCanvas)
     1093 + want := faketerm.MustNew(size)
     1094 + if tc.want != nil {
     1095 + want = tc.want(size)
     1096 + }
     1097 + 
     1098 + got, err := faketerm.New(size)
     1099 + if err != nil {
     1100 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     1101 + }
     1102 + if err := bc.Apply(got); err != nil {
     1103 + t.Fatalf("bc.Apply => unexpected error: %v", err)
     1104 + }
     1105 + if diff := faketerm.Diff(want, got); diff != "" {
     1106 + t.Fatalf("HV => %v", diff)
     1107 + }
     1108 + })
     1109 + }
     1110 +}
     1111 + 
     1112 +// hvSegment is one horizontal or vertical segment.
     1113 +type hvSegment struct {
     1114 + ar image.Rectangle
     1115 + st Type
     1116 +}
     1117 + 
     1118 +// diagSegment is one diagonal segment.
     1119 +type diagSegment struct {
     1120 + ar image.Rectangle
     1121 + width int
     1122 + dt DiagonalType
     1123 +}
     1124 + 
     1125 +func TestAdjustHoriz(t *testing.T) {
     1126 + tests := []struct {
     1127 + desc string
     1128 + start image.Point
     1129 + end image.Point
     1130 + segWidth int
     1131 + adjust int
     1132 + wantStart image.Point
     1133 + wantEnd image.Point
     1134 + }{
     1135 + {
     1136 + desc: "no change for zero adjustment",
     1137 + },
     1138 + {
     1139 + desc: "safe adjustments, points don't cross",
     1140 + start: image.Point{0, 0},
     1141 + end: image.Point{5, 0},
     1142 + segWidth: 6,
     1143 + adjust: 1,
     1144 + wantStart: image.Point{1, 0},
     1145 + wantEnd: image.Point{4, 0},
     1146 + },
     1147 + {
     1148 + desc: "safe adjustments, points land on each other",
     1149 + start: image.Point{0, 0},
     1150 + end: image.Point{4, 0},
     1151 + segWidth: 5,
     1152 + adjust: 2,
     1153 + wantStart: image.Point{2, 0},
     1154 + wantEnd: image.Point{2, 0},
     1155 + },
     1156 + 
     1157 + {
     1158 + desc: "points cross, width divides evenly",
     1159 + start: image.Point{0, 0},
     1160 + end: image.Point{5, 0},
     1161 + segWidth: 6,
     1162 + adjust: 3,
     1163 + wantStart: image.Point{2, 0},
     1164 + wantEnd: image.Point{3, 0},
     1165 + },
     1166 + {
     1167 + desc: "points cross, width divides oddly",
     1168 + start: image.Point{0, 0},
     1169 + end: image.Point{6, 0},
     1170 + segWidth: 7,
     1171 + adjust: 4,
     1172 + wantStart: image.Point{3, 0},
     1173 + wantEnd: image.Point{3, 0},
     1174 + },
     1175 + }
     1176 + 
     1177 + for _, tc := range tests {
     1178 + t.Run(tc.desc, func(t *testing.T) {
     1179 + gotStart, gotEnd := adjustHoriz(tc.start, tc.end, tc.segWidth, tc.adjust)
     1180 + if !gotStart.Eq(tc.wantStart) || !gotEnd.Eq(tc.wantEnd) {
     1181 + t.Errorf("adjustHoriz(%v, %v, %v, %v) => %v, %v, want %v, %v", tc.start, tc.end, tc.segWidth, tc.adjust, gotStart, gotEnd, tc.wantStart, tc.wantEnd)
     1182 + }
     1183 + 
     1184 + })
     1185 + }
     1186 +}
     1187 + 
     1188 +func TestAdjustVert(t *testing.T) {
     1189 + tests := []struct {
     1190 + desc string
     1191 + start image.Point
     1192 + end image.Point
     1193 + segHeight int
     1194 + adjust int
     1195 + wantStart image.Point
     1196 + wantEnd image.Point
     1197 + }{
     1198 + {
     1199 + desc: "no change for zero adjustment",
     1200 + },
     1201 + {
     1202 + desc: "safe adjustments, points don't cross",
     1203 + start: image.Point{0, 0},
     1204 + end: image.Point{0, 5},
     1205 + segHeight: 6,
     1206 + adjust: 1,
     1207 + wantStart: image.Point{0, 1},
     1208 + wantEnd: image.Point{0, 4},
     1209 + },
     1210 + {
     1211 + desc: "safe adjustments, points land on each other",
     1212 + start: image.Point{0, 0},
     1213 + end: image.Point{0, 4},
     1214 + segHeight: 5,
     1215 + adjust: 2,
     1216 + wantStart: image.Point{0, 2},
     1217 + wantEnd: image.Point{0, 2},
     1218 + },
     1219 + 
     1220 + {
     1221 + desc: "points cross, width divides evenly",
     1222 + start: image.Point{0, 0},
     1223 + end: image.Point{0, 5},
     1224 + segHeight: 6,
     1225 + adjust: 3,
     1226 + wantStart: image.Point{0, 2},
     1227 + wantEnd: image.Point{0, 3},
     1228 + },
     1229 + {
     1230 + desc: "points cross, width divides oddly",
     1231 + start: image.Point{0, 0},
     1232 + end: image.Point{0, 6},
     1233 + segHeight: 7,
     1234 + adjust: 4,
     1235 + wantStart: image.Point{0, 3},
     1236 + wantEnd: image.Point{0, 3},
     1237 + },
     1238 + }
     1239 + 
     1240 + for _, tc := range tests {
     1241 + t.Run(tc.desc, func(t *testing.T) {
     1242 + gotStart, gotEnd := adjustVert(tc.start, tc.end, tc.segHeight, tc.adjust)
     1243 + if !gotStart.Eq(tc.wantStart) || !gotEnd.Eq(tc.wantEnd) {
     1244 + t.Errorf("adjustVert(%v, %v, %v, %v) => %v, %v, want %v, %v", tc.start, tc.end, tc.segHeight, tc.adjust, gotStart, gotEnd, tc.wantStart, tc.wantEnd)
     1245 + }
     1246 + 
     1247 + })
     1248 + }
     1249 +}
     1250 + 
     1251 +func TestDiagonal(t *testing.T) {
     1252 + tests := []struct {
     1253 + desc string
     1254 + opts []DiagonalOption
     1255 + cellCanvas image.Rectangle // Canvas in cells that will be converted to braille canvas for drawing.
     1256 + ar image.Rectangle
     1257 + width int
     1258 + dt DiagonalType
     1259 + want func(size image.Point) *faketerm.Terminal
     1260 + wantErr bool
     1261 + }{
     1262 + {
     1263 + desc: "fails on area with negative Min.X",
     1264 + cellCanvas: image.Rect(0, 0, 1, 1),
     1265 + ar: image.Rect(-1, 0, 1, 1),
     1266 + width: 1,
     1267 + dt: LeftToRight,
     1268 + wantErr: true,
     1269 + },
     1270 + {
     1271 + desc: "fails on area with negative Min.Y",
     1272 + cellCanvas: image.Rect(0, 0, 1, 1),
     1273 + ar: image.Rect(0, -1, 1, 1),
     1274 + width: 1,
     1275 + dt: LeftToRight,
     1276 + wantErr: true,
     1277 + },
     1278 + {
     1279 + desc: "fails on area with negative Max.X",
     1280 + cellCanvas: image.Rect(0, 0, 1, 1),
     1281 + ar: image.Rectangle{image.Point{0, 0}, image.Point{-1, 1}},
     1282 + width: 1,
     1283 + dt: LeftToRight,
     1284 + wantErr: true,
     1285 + },
     1286 + {
     1287 + desc: "fails on area with negative Max.Y",
     1288 + cellCanvas: image.Rect(0, 0, 1, 1),
     1289 + ar: image.Rectangle{image.Point{0, 0}, image.Point{1, -1}},
     1290 + width: 1,
     1291 + dt: LeftToRight,
     1292 + wantErr: true,
     1293 + },
     1294 + {
     1295 + desc: "fails on area with zero Dx()",
     1296 + cellCanvas: image.Rect(0, 0, 1, 1),
     1297 + ar: image.Rect(0, 0, 0, 1),
     1298 + width: 1,
     1299 + dt: LeftToRight,
     1300 + wantErr: true,
     1301 + },
     1302 + {
     1303 + desc: "fails on area with zero Dy()",
     1304 + cellCanvas: image.Rect(0, 0, 1, 1),
     1305 + ar: image.Rect(0, 0, 1, 0),
     1306 + width: 1,
     1307 + dt: LeftToRight,
     1308 + wantErr: true,
     1309 + },
     1310 + {
     1311 + desc: "fails on unsupported diagonal type (too small)",
     1312 + cellCanvas: image.Rect(0, 0, 1, 1),
     1313 + ar: image.Rect(0, 0, 2, 2),
     1314 + width: 1,
     1315 + dt: DiagonalType(0),
     1316 + wantErr: true,
     1317 + },
     1318 + {
     1319 + desc: "fails on unsupported diagonal type (too large)",
     1320 + cellCanvas: image.Rect(0, 0, 1, 1),
     1321 + ar: image.Rect(0, 0, 2, 2),
     1322 + width: 1,
     1323 + dt: DiagonalType(int(RightToLeft) + 1),
     1324 + wantErr: true,
     1325 + },
     1326 + {
     1327 + desc: "fails on area larger than the canvas",
     1328 + cellCanvas: image.Rect(0, 0, 1, 1),
     1329 + ar: image.Rect(0, 0, 3, 1),
     1330 + width: 1,
     1331 + dt: LeftToRight,
     1332 + wantErr: true,
     1333 + },
     1334 + {
     1335 + desc: "fails on zero width",
     1336 + cellCanvas: image.Rect(0, 0, 1, 1),
     1337 + ar: image.Rect(0, 0, 3, 1),
     1338 + width: 0,
     1339 + dt: LeftToRight,
     1340 + wantErr: true,
     1341 + },
     1342 + {
     1343 + desc: "left to right, area 4x4, width 1",
     1344 + cellCanvas: image.Rect(0, 0, 2, 1),
     1345 + ar: image.Rect(0, 0, 4, 4),
     1346 + dt: LeftToRight,
     1347 + width: 1,
     1348 + want: func(size image.Point) *faketerm.Terminal {
     1349 + ft := faketerm.MustNew(size)
     1350 + bc := testbraille.MustNew(ft.Area())
     1351 + 
     1352 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 3})
     1353 + 
     1354 + testbraille.MustApply(bc, ft)
     1355 + return ft
     1356 + },
     1357 + },
     1358 + {
     1359 + desc: "right to left, area 4x4, width 1",
     1360 + cellCanvas: image.Rect(0, 0, 2, 1),
     1361 + ar: image.Rect(0, 0, 4, 4),
     1362 + dt: RightToLeft,
     1363 + width: 1,
     1364 + want: func(size image.Point) *faketerm.Terminal {
     1365 + ft := faketerm.MustNew(size)
     1366 + bc := testbraille.MustNew(ft.Area())
     1367 + 
     1368 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{0, 3})
     1369 + 
     1370 + testbraille.MustApply(bc, ft)
     1371 + return ft
     1372 + },
     1373 + },
     1374 + {
     1375 + desc: "left to right, area 4x4, width 2",
     1376 + cellCanvas: image.Rect(0, 0, 2, 1),
     1377 + ar: image.Rect(0, 0, 4, 4),
     1378 + dt: LeftToRight,
     1379 + width: 2,
     1380 + want: func(size image.Point) *faketerm.Terminal {
     1381 + ft := faketerm.MustNew(size)
     1382 + bc := testbraille.MustNew(ft.Area())
     1383 + 
     1384 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 2})
     1385 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 3})
     1386 + 
     1387 + testbraille.MustApply(bc, ft)
     1388 + return ft
     1389 + },
     1390 + },
     1391 + {
     1392 + desc: "right to left, area 4x4, width 2",
     1393 + cellCanvas: image.Rect(0, 0, 2, 1),
     1394 + ar: image.Rect(0, 0, 4, 4),
     1395 + dt: RightToLeft,
     1396 + width: 2,
     1397 + want: func(size image.Point) *faketerm.Terminal {
     1398 + ft := faketerm.MustNew(size)
     1399 + bc := testbraille.MustNew(ft.Area())
     1400 + 
     1401 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{0, 2})
     1402 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{0, 3})
     1403 + 
     1404 + testbraille.MustApply(bc, ft)
     1405 + return ft
     1406 + },
     1407 + },
     1408 + {
     1409 + desc: "left to right, area 4x4, width 3",
     1410 + cellCanvas: image.Rect(0, 0, 2, 1),
     1411 + ar: image.Rect(0, 0, 4, 4),
     1412 + dt: LeftToRight,
     1413 + width: 3,
     1414 + want: func(size image.Point) *faketerm.Terminal {
     1415 + ft := faketerm.MustNew(size)
     1416 + bc := testbraille.MustNew(ft.Area())
     1417 + 
     1418 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 2})
     1419 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 3})
     1420 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 3})
     1421 + 
     1422 + testbraille.MustApply(bc, ft)
     1423 + return ft
     1424 + },
     1425 + },
     1426 + {
     1427 + desc: "right to left, area 4x4, width 3",
     1428 + cellCanvas: image.Rect(0, 0, 2, 1),
     1429 + ar: image.Rect(0, 0, 4, 4),
     1430 + dt: RightToLeft,
     1431 + width: 3,
     1432 + want: func(size image.Point) *faketerm.Terminal {
     1433 + ft := faketerm.MustNew(size)
     1434 + bc := testbraille.MustNew(ft.Area())
     1435 + 
     1436 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{0, 2})
     1437 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{0, 3})
     1438 + testdraw.MustBrailleLine(bc, image.Point{3, 1}, image.Point{1, 3})
     1439 + 
     1440 + testbraille.MustApply(bc, ft)
     1441 + return ft
     1442 + },
     1443 + },
     1444 + {
     1445 + desc: "left to right, area 8x4, width 3",
     1446 + cellCanvas: image.Rect(0, 0, 4, 1),
     1447 + ar: image.Rect(0, 0, 8, 4),
     1448 + dt: LeftToRight,
     1449 + width: 3,
     1450 + want: func(size image.Point) *faketerm.Terminal {
     1451 + ft := faketerm.MustNew(size)
     1452 + bc := testbraille.MustNew(ft.Area())
     1453 + 
     1454 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{7, 3})
     1455 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{7, 2})
     1456 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{6, 3})
     1457 + 
     1458 + testbraille.MustApply(bc, ft)
     1459 + return ft
     1460 + },
     1461 + },
     1462 + {
     1463 + desc: "right to left, area 8x4, width 3",
     1464 + cellCanvas: image.Rect(0, 0, 4, 1),
     1465 + ar: image.Rect(0, 0, 8, 4),
     1466 + dt: RightToLeft,
     1467 + width: 3,
     1468 + want: func(size image.Point) *faketerm.Terminal {
     1469 + ft := faketerm.MustNew(size)
     1470 + bc := testbraille.MustNew(ft.Area())
     1471 + 
     1472 + testdraw.MustBrailleLine(bc, image.Point{7, 0}, image.Point{0, 3})
     1473 + testdraw.MustBrailleLine(bc, image.Point{6, 0}, image.Point{0, 2})
     1474 + testdraw.MustBrailleLine(bc, image.Point{7, 1}, image.Point{1, 3})
     1475 + 
     1476 + testbraille.MustApply(bc, ft)
     1477 + return ft
     1478 + },
     1479 + },
     1480 + {
     1481 + desc: "left to right, area 4x8, width 3",
     1482 + cellCanvas: image.Rect(0, 0, 2, 2),
     1483 + ar: image.Rect(0, 0, 4, 8),
     1484 + dt: LeftToRight,
     1485 + width: 3,
     1486 + want: func(size image.Point) *faketerm.Terminal {
     1487 + ft := faketerm.MustNew(size)
     1488 + bc := testbraille.MustNew(ft.Area())
     1489 + 
     1490 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 7})
     1491 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 6})
     1492 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 7})
     1493 + 
     1494 + testbraille.MustApply(bc, ft)
     1495 + return ft
     1496 + },
     1497 + },
     1498 + {
     1499 + desc: "right to left, area 4x8, width 3",
     1500 + cellCanvas: image.Rect(0, 0, 2, 2),
     1501 + ar: image.Rect(0, 0, 4, 8),
     1502 + dt: RightToLeft,
     1503 + width: 3,
     1504 + want: func(size image.Point) *faketerm.Terminal {
     1505 + ft := faketerm.MustNew(size)
     1506 + bc := testbraille.MustNew(ft.Area())
     1507 + 
     1508 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{0, 7})
     1509 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{0, 6})
     1510 + testdraw.MustBrailleLine(bc, image.Point{3, 1}, image.Point{1, 7})
     1511 + 
     1512 + testbraille.MustApply(bc, ft)
     1513 + return ft
     1514 + },
     1515 + },
     1516 + {
     1517 + desc: "left to right, area 4x4, width 4",
     1518 + cellCanvas: image.Rect(0, 0, 2, 1),
     1519 + ar: image.Rect(0, 0, 4, 4),
     1520 + dt: LeftToRight,
     1521 + width: 4,
     1522 + want: func(size image.Point) *faketerm.Terminal {
     1523 + ft := faketerm.MustNew(size)
     1524 + bc := testbraille.MustNew(ft.Area())
     1525 + 
     1526 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{3, 1})
     1527 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 2})
     1528 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 3})
     1529 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 3})
     1530 + 
     1531 + testbraille.MustApply(bc, ft)
     1532 + return ft
     1533 + },
     1534 + },
     1535 + {
     1536 + desc: "right to left, area 4x4, width 4",
     1537 + cellCanvas: image.Rect(0, 0, 2, 1),
     1538 + ar: image.Rect(0, 0, 4, 4),
     1539 + dt: RightToLeft,
     1540 + width: 4,
     1541 + want: func(size image.Point) *faketerm.Terminal {
     1542 + ft := faketerm.MustNew(size)
     1543 + bc := testbraille.MustNew(ft.Area())
     1544 + 
     1545 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{0, 1})
     1546 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{0, 2})
     1547 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{0, 3})
     1548 + testdraw.MustBrailleLine(bc, image.Point{3, 1}, image.Point{1, 3})
     1549 + 
     1550 + testbraille.MustApply(bc, ft)
     1551 + return ft
     1552 + },
     1553 + },
     1554 + {
     1555 + desc: "left to right, area 4x4, width 5",
     1556 + cellCanvas: image.Rect(0, 0, 2, 1),
     1557 + ar: image.Rect(0, 0, 4, 4),
     1558 + dt: LeftToRight,
     1559 + width: 5,
     1560 + want: func(size image.Point) *faketerm.Terminal {
     1561 + ft := faketerm.MustNew(size)
     1562 + bc := testbraille.MustNew(ft.Area())
     1563 + 
     1564 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{3, 1})
     1565 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 2})
     1566 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 3})
     1567 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 3})
     1568 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{1, 3})
     1569 + 
     1570 + testbraille.MustApply(bc, ft)
     1571 + return ft
     1572 + },
     1573 + },
     1574 + {
     1575 + desc: "right to left, area 4x4, width 5",
     1576 + cellCanvas: image.Rect(0, 0, 2, 1),
     1577 + ar: image.Rect(0, 0, 4, 4),
     1578 + dt: RightToLeft,
     1579 + width: 5,
     1580 + want: func(size image.Point) *faketerm.Terminal {
     1581 + ft := faketerm.MustNew(size)
     1582 + bc := testbraille.MustNew(ft.Area())
     1583 + 
     1584 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{0, 1})
     1585 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{0, 2})
     1586 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{0, 3})
     1587 + testdraw.MustBrailleLine(bc, image.Point{3, 1}, image.Point{1, 3})
     1588 + testdraw.MustBrailleLine(bc, image.Point{3, 2}, image.Point{2, 3})
     1589 + 
     1590 + testbraille.MustApply(bc, ft)
     1591 + return ft
     1592 + },
     1593 + },
     1594 + {
     1595 + desc: "left to right, area 4x4, width 6",
     1596 + cellCanvas: image.Rect(0, 0, 2, 1),
     1597 + ar: image.Rect(0, 0, 4, 4),
     1598 + dt: LeftToRight,
     1599 + width: 6,
     1600 + want: func(size image.Point) *faketerm.Terminal {
     1601 + ft := faketerm.MustNew(size)
     1602 + bc := testbraille.MustNew(ft.Area())
     1603 + 
     1604 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{3, 0})
     1605 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{3, 1})
     1606 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 2})
     1607 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 3})
     1608 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 3})
     1609 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{1, 3})
     1610 + 
     1611 + testbraille.MustApply(bc, ft)
     1612 + return ft
     1613 + },
     1614 + },
     1615 + {
     1616 + desc: "right to left, area 4x4, width 6",
     1617 + cellCanvas: image.Rect(0, 0, 2, 1),
     1618 + ar: image.Rect(0, 0, 4, 4),
     1619 + dt: RightToLeft,
     1620 + width: 6,
     1621 + want: func(size image.Point) *faketerm.Terminal {
     1622 + ft := faketerm.MustNew(size)
     1623 + bc := testbraille.MustNew(ft.Area())
     1624 + 
     1625 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     1626 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{0, 1})
     1627 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{0, 2})
     1628 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{0, 3})
     1629 + testdraw.MustBrailleLine(bc, image.Point{3, 1}, image.Point{1, 3})
     1630 + testdraw.MustBrailleLine(bc, image.Point{3, 2}, image.Point{2, 3})
     1631 + 
     1632 + testbraille.MustApply(bc, ft)
     1633 + return ft
     1634 + },
     1635 + },
     1636 + {
     1637 + desc: "left to right, area 4x4, width 7",
     1638 + cellCanvas: image.Rect(0, 0, 2, 1),
     1639 + ar: image.Rect(0, 0, 4, 4),
     1640 + dt: LeftToRight,
     1641 + width: 7,
     1642 + want: func(size image.Point) *faketerm.Terminal {
     1643 + ft := faketerm.MustNew(size)
     1644 + bc := testbraille.MustNew(ft.Area())
     1645 + 
     1646 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{3, 0})
     1647 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{3, 1})
     1648 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 2})
     1649 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 3})
     1650 + testdraw.MustBrailleLine(bc, image.Point{0, 1}, image.Point{2, 3})
     1651 + testdraw.MustBrailleLine(bc, image.Point{0, 2}, image.Point{1, 3})
     1652 + testdraw.MustBrailleLine(bc, image.Point{0, 3}, image.Point{3, 3})
     1653 + 
     1654 + testbraille.MustApply(bc, ft)
     1655 + return ft
     1656 + },
     1657 + },
     1658 + {
     1659 + desc: "right to left, area 4x4, width 7",
     1660 + cellCanvas: image.Rect(0, 0, 2, 1),
     1661 + ar: image.Rect(0, 0, 4, 4),
     1662 + dt: RightToLeft,
     1663 + width: 7,
     1664 + want: func(size image.Point) *faketerm.Terminal {
     1665 + ft := faketerm.MustNew(size)
     1666 + bc := testbraille.MustNew(ft.Area())
     1667 + 
     1668 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{0, 0})
     1669 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{0, 1})
     1670 + testdraw.MustBrailleLine(bc, image.Point{2, 0}, image.Point{0, 2})
     1671 + testdraw.MustBrailleLine(bc, image.Point{3, 0}, image.Point{0, 3})
     1672 + testdraw.MustBrailleLine(bc, image.Point{3, 1}, image.Point{1, 3})
     1673 + testdraw.MustBrailleLine(bc, image.Point{3, 2}, image.Point{2, 3})
     1674 + testdraw.MustBrailleLine(bc, image.Point{3, 3}, image.Point{3, 3})
     1675 + 
     1676 + testbraille.MustApply(bc, ft)
     1677 + return ft
     1678 + },
     1679 + },
     1680 + {
     1681 + desc: "left to right, fails when width is larger than area",
     1682 + cellCanvas: image.Rect(0, 0, 2, 1),
     1683 + ar: image.Rect(0, 0, 4, 4),
     1684 + dt: LeftToRight,
     1685 + width: 8,
     1686 + wantErr: true,
     1687 + },
     1688 + {
     1689 + desc: "right to left, fails when width is larger than area",
     1690 + cellCanvas: image.Rect(0, 0, 2, 1),
     1691 + ar: image.Rect(0, 0, 4, 4),
     1692 + dt: RightToLeft,
     1693 + width: 8,
     1694 + wantErr: true,
     1695 + },
     1696 + {
     1697 + desc: "sets cell options",
     1698 + opts: []DiagonalOption{
     1699 + DiagonalCellOpts(
     1700 + cell.FgColor(cell.ColorRed),
     1701 + cell.BgColor(cell.ColorGreen),
     1702 + ),
     1703 + },
     1704 + cellCanvas: image.Rect(0, 0, 2, 1),
     1705 + ar: image.Rect(0, 0, 4, 4),
     1706 + dt: LeftToRight,
     1707 + width: 2,
     1708 + want: func(size image.Point) *faketerm.Terminal {
     1709 + ft := faketerm.MustNew(size)
     1710 + bc := testbraille.MustNew(ft.Area())
     1711 + 
     1712 + opts := []draw.BrailleLineOption{
     1713 + draw.BrailleLineCellOpts(
     1714 + cell.FgColor(cell.ColorRed),
     1715 + cell.BgColor(cell.ColorGreen),
     1716 + ),
     1717 + }
     1718 + testdraw.MustBrailleLine(bc, image.Point{1, 0}, image.Point{3, 2}, opts...)
     1719 + testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{3, 3}, opts...)
     1720 + 
     1721 + testbraille.MustApply(bc, ft)
     1722 + return ft
     1723 + },
     1724 + },
     1725 + }
     1726 + 
     1727 + for _, tc := range tests {
     1728 + t.Run(fmt.Sprintf("%s dt:%v", tc.desc, tc.dt), func(t *testing.T) {
     1729 + bc, err := braille.New(tc.cellCanvas)
     1730 + if err != nil {
     1731 + t.Fatalf("braille.New => unexpected error: %v", err)
     1732 + }
     1733 + 
     1734 + err = Diagonal(bc, tc.ar, tc.width, tc.dt, tc.opts...)
     1735 + if (err != nil) != tc.wantErr {
     1736 + t.Errorf("Diagonal => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     1737 + }
     1738 + if err != nil {
     1739 + return
     1740 + }
     1741 + 
     1742 + size := area.Size(tc.cellCanvas)
     1743 + want := faketerm.MustNew(size)
     1744 + if tc.want != nil {
     1745 + want = tc.want(size)
     1746 + }
     1747 + 
     1748 + got, err := faketerm.New(size)
     1749 + if err != nil {
     1750 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     1751 + }
     1752 + if err := bc.Apply(got); err != nil {
     1753 + t.Fatalf("bc.Apply => unexpected error: %v", err)
     1754 + }
     1755 + if diff := faketerm.Diff(want, got); diff != "" {
     1756 + t.Fatalf("Diagonal => %v", diff)
     1757 + }
     1758 + })
     1759 + }
     1760 +}
     1761 + 
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/segment/testsegment/testsegment.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +// Package testsegment provides helpers for tests that use the segment package.
     16 +package testsegment
     17 + 
     18 +import (
     19 + "fmt"
     20 + "image"
     21 + 
     22 + "github.com/mum4k/termdash/canvas/braille"
     23 + "github.com/mum4k/termdash/draw/segdisp/segment"
     24 +)
     25 + 
     26 +// MustHV draws the segment or panics.
     27 +func MustHV(bc *braille.Canvas, ar image.Rectangle, st segment.Type, opts ...segment.Option) {
     28 + if err := segment.HV(bc, ar, st, opts...); err != nil {
     29 + panic(fmt.Sprintf("segment.HV => unexpected error: %v", err))
     30 + }
     31 +}
     32 + 
     33 +// MustDiagonal draws the segment or panics.
     34 +func MustDiagonal(bc *braille.Canvas, ar image.Rectangle, width int, dt segment.DiagonalType, opts ...segment.DiagonalOption) {
     35 + if err := segment.Diagonal(bc, ar, width, dt, opts...); err != nil {
     36 + panic(fmt.Sprintf("segment.Diagonal => unexpected error: %v", err))
     37 + }
     38 +}
     39 + 
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/sixteen/attributes.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +package sixteen
     16 + 
     17 +// attributes.go calculates attributes needed when determining placement of
     18 +// segments.
     19 + 
     20 +import (
     21 + "fmt"
     22 + "image"
     23 + "math"
     24 + 
     25 + "github.com/mum4k/termdash/draw/segdisp/segment"
     26 + "github.com/mum4k/termdash/numbers"
     27 +)
     28 + 
     29 +// hvSegType maps horizontal and vertical segments to their type.
     30 +var hvSegType = map[Segment]segment.Type{
     31 + A1: segment.Horizontal,
     32 + A2: segment.Horizontal,
     33 + B: segment.Vertical,
     34 + C: segment.Vertical,
     35 + D1: segment.Horizontal,
     36 + D2: segment.Horizontal,
     37 + E: segment.Vertical,
     38 + F: segment.Vertical,
     39 + G1: segment.Horizontal,
     40 + G2: segment.Horizontal,
     41 + J: segment.Vertical,
     42 + M: segment.Vertical,
     43 +}
     44 + 
     45 +// diaSegType maps diagonal segments to their type.
     46 +var diaSegType = map[Segment]segment.DiagonalType{
     47 + H: segment.LeftToRight,
     48 + K: segment.RightToLeft,
     49 + N: segment.RightToLeft,
     50 + L: segment.LeftToRight,
     51 +}
     52 + 
     53 +// segmentSize given an area for the display determines the size of individual
     54 +// segments, i.e. the width of a vertical or the height of a horizontal
     55 +// segment.
     56 +func segmentSize(ar image.Rectangle) int {
     57 + // widthPerc is the relative width of a segment to the width of the canvas.
     58 + const widthPerc = 9
     59 + s := int(numbers.Round(float64(ar.Dx()) * widthPerc / 100))
     60 + if s > 3 && s%2 == 0 {
     61 + // Segments with odd number of pixels in their width/height look
     62 + // better, since the spike at the top of their slopes has only one
     63 + // pixel.
     64 + s++
     65 + }
     66 + return s
     67 +}
     68 + 
     69 +// attributes contains attributes needed to draw the segment display.
     70 +// Refer to doc/segment_placement.svg for a visual aid and explanation of the
     71 +// usage of the square roots.
     72 +type attributes struct {
     73 + // segSize is the width of a vertical or height of a horizontal segment.
     74 + segSize int
     75 + 
     76 + // diaGap is the shortest distance between slopes on two neighboring
     77 + // perpendicular segments.
     78 + diaGap float64
     79 + 
     80 + // segPeakDist is the distance between the peak of the slope on a segment
     81 + // and the point where the slope ends.
     82 + segPeakDist float64
     83 + 
     84 + // diaLeg is the leg of a square whose hypotenuse is the diaGap.
     85 + diaLeg float64
     86 + 
     87 + // peakToPeak is a horizontal or vertical distance between peaks of two
     88 + // segments.
     89 + peakToPeak int
     90 + 
     91 + // shortLen is length of the shorter segment, e.g. D1.
     92 + shortLen int
     93 + 
     94 + // longLen is length of the longer segment, e.g. F.
     95 + longLen int
     96 + 
     97 + // horizLeftX is the X coordinate where the area of the segment horizontally
     98 + // on the left starts, i.e. X coordinate of F and E.
     99 + horizLeftX int
     100 + // horizMidX is the X coordinate where the area of the segment horizontally in
     101 + // the middle starts, i.e. X coordinate of J and M.
     102 + horizMidX int
     103 + // horizRightX is the X coordinate where the area of the segment horizontally
     104 + // on the right starts, i.e. X coordinate of B and C.
     105 + horizRightX int
     106 + 
     107 + // vertCenY is the Y coordinate where the area of the segment vertically
     108 + // in the center starts, i.e. Y coordinate of G1 and G2.
     109 + vertCenY int
     110 + // vertBotY is the Y coordinate where the area of the segment vertically
     111 + // at the bottom starts, i.e. Y coordinate of D1 and D2.
     112 + vertBotY int
     113 +}
     114 + 
     115 +// newAttributes calculates attributes needed to place the segments for the
     116 +// provided pixel area.
     117 +func newAttributes(bcAr image.Rectangle) *attributes {
     118 + segSize := segmentSize(bcAr)
     119 + 
     120 + // diaPerc is the size of the diaGap in percentage of the segment's size.
     121 + const diaPerc = 40
     122 + // Ensure there is at least one pixel diagonally between segments so they
     123 + // don't visually blend.
     124 + _, dg := numbers.MinMaxInts([]int{
     125 + int(float64(segSize) * diaPerc / 100),
     126 + 1,
     127 + })
     128 + diaGap := float64(dg)
     129 + 
     130 + segLeg := float64(segSize) / math.Sqrt2
     131 + segPeakDist := segLeg / math.Sqrt2
     132 + 
     133 + diaLeg := diaGap / math.Sqrt2
     134 + peakToPeak := diaLeg * 2
     135 + if segSize == 2 {
     136 + // Display that has segment size of two looks more balanced with peak
     137 + // distance of two.
     138 + peakToPeak = 2
     139 + }
     140 + if peakToPeak > 3 && int(peakToPeak)%2 == 0 {
     141 + // Prefer odd distances to create centered look.
     142 + peakToPeak++
     143 + }
     144 + 
     145 + twoSegHypo := 2*segLeg + diaGap
     146 + twoSegLeg := twoSegHypo / math.Sqrt2
     147 + edgeSegGap := twoSegLeg - segPeakDist
     148 + 
     149 + spaces := int(numbers.Round(2*edgeSegGap + peakToPeak))
     150 + shortLen := (bcAr.Dx()-spaces)/2 - 1
     151 + longLen := (bcAr.Dy()-spaces)/2 - 1
     152 + 
     153 + ptp := int(numbers.Round(peakToPeak))
     154 + horizLeftX := int(numbers.Round(edgeSegGap))
     155 + 
     156 + // Refer to doc/segment_placement.svg.
     157 + // Diagram labeled "A mid point".
     158 + offset := int(numbers.Round(diaLeg - segPeakDist))
     159 + horizMidX := horizLeftX + shortLen + offset
     160 + horizRightX := horizLeftX + shortLen + ptp + shortLen + offset
     161 + 
     162 + vertCenY := horizLeftX + longLen + offset
     163 + vertBotY := horizLeftX + longLen + ptp + longLen + offset
     164 + 
     165 + return &attributes{
     166 + segSize: segSize,
     167 + diaGap: diaGap,
     168 + segPeakDist: segPeakDist,
     169 + diaLeg: diaLeg,
     170 + peakToPeak: ptp,
     171 + shortLen: shortLen,
     172 + longLen: longLen,
     173 + 
     174 + horizLeftX: horizLeftX,
     175 + horizMidX: horizMidX,
     176 + horizRightX: horizRightX,
     177 + vertCenY: vertCenY,
     178 + vertBotY: vertBotY,
     179 + }
     180 +}
     181 + 
     182 +// hvSegArea returns the area for the specified horizontal or vertical segment.
     183 +func (a *attributes) hvSegArea(s Segment) image.Rectangle {
     184 + var (
     185 + start image.Point
     186 + length int
     187 + )
     188 + 
     189 + switch s {
     190 + case A1:
     191 + start = image.Point{a.horizLeftX, 0}
     192 + length = a.shortLen
     193 + 
     194 + case A2:
     195 + a1 := a.hvSegArea(A1)
     196 + start = image.Point{a1.Max.X + a.peakToPeak, 0}
     197 + length = a.shortLen
     198 + 
     199 + case F:
     200 + start = image.Point{0, a.horizLeftX}
     201 + length = a.longLen
     202 + 
     203 + case J:
     204 + start = image.Point{a.horizMidX, a.horizLeftX}
     205 + length = a.longLen
     206 + 
     207 + case B:
     208 + start = image.Point{a.horizRightX, a.horizLeftX}
     209 + length = a.longLen
     210 + 
     211 + case G1:
     212 + start = image.Point{a.horizLeftX, a.vertCenY}
     213 + length = a.shortLen
     214 + 
     215 + case G2:
     216 + g1 := a.hvSegArea(G1)
     217 + start = image.Point{g1.Max.X + a.peakToPeak, a.vertCenY}
     218 + length = a.shortLen
     219 + 
     220 + case E:
     221 + f := a.hvSegArea(F)
     222 + start = image.Point{0, f.Max.Y + a.peakToPeak}
     223 + length = a.longLen
     224 + 
     225 + case M:
     226 + j := a.hvSegArea(J)
     227 + start = image.Point{a.horizMidX, j.Max.Y + a.peakToPeak}
     228 + length = a.longLen
     229 + 
     230 + case C:
     231 + b := a.hvSegArea(B)
     232 + start = image.Point{a.horizRightX, b.Max.Y + a.peakToPeak}
     233 + length = a.longLen
     234 + 
     235 + case D1:
     236 + start = image.Point{a.horizLeftX, a.vertBotY}
     237 + length = a.shortLen
     238 + 
     239 + case D2:
     240 + d1 := a.hvSegArea(D1)
     241 + start = image.Point{d1.Max.X + a.peakToPeak, a.vertBotY}
     242 + length = a.shortLen
     243 + 
     244 + default:
     245 + panic(fmt.Sprintf("cannot determine area for unknown horizontal or vertical segment %v(%d)", s, s))
     246 + }
     247 + 
     248 + return a.hvArFromStart(start, s, length)
     249 +}
     250 + 
     251 +// hvArFromStart given start coordinates of a segment, its length and its type,
     252 +// determines its area.
     253 +func (a *attributes) hvArFromStart(start image.Point, s Segment, length int) image.Rectangle {
     254 + st := hvSegType[s]
     255 + switch st {
     256 + case segment.Horizontal:
     257 + return image.Rect(start.X, start.Y, start.X+length, start.Y+a.segSize)
     258 + case segment.Vertical:
     259 + return image.Rect(start.X, start.Y, start.X+a.segSize, start.Y+length)
     260 + default:
     261 + panic(fmt.Sprintf("cannot create area for segment of unknown type %v(%d)", st, st))
     262 + }
     263 +}
     264 + 
     265 +// diaSegArea returns the area for the specified diagonal segment.
     266 +func (a *attributes) diaSegArea(s Segment) image.Rectangle {
     267 + switch s {
     268 + case H:
     269 + return a.diaBetween(A1, F, J, G1)
     270 + case K:
     271 + return a.diaBetween(A2, B, J, G2)
     272 + case N:
     273 + return a.diaBetween(G1, M, E, D1)
     274 + case L:
     275 + return a.diaBetween(G2, M, C, D2)
     276 + 
     277 + default:
     278 + panic(fmt.Sprintf("cannot determine area for unknown diagonal segment %v(%d)", s, s))
     279 + }
     280 +}
     281 + 
     282 +// diaBetween given four segments (two horizontal and two vertical) returns the
     283 +// area between them for a diagonal segment.
     284 +func (a *attributes) diaBetween(top, left, right, bottom Segment) image.Rectangle {
     285 + topAr := a.hvSegArea(top)
     286 + leftAr := a.hvSegArea(left)
     287 + rightAr := a.hvSegArea(right)
     288 + bottomAr := a.hvSegArea(bottom)
     289 + 
     290 + // hvToDiaGapPerc is the size of gap between horizontal or vertical segment
     291 + // and the diagonal segment between them in percentage of the diaGap.
     292 + const hvToDiaGapPerc = 30
     293 + hvToDiaGap := a.diaGap * hvToDiaGapPerc / 100
     294 + 
     295 + startX := int(numbers.Round(float64(topAr.Min.X) + a.segPeakDist - a.diaLeg + hvToDiaGap))
     296 + startY := int(numbers.Round(float64(leftAr.Min.Y) + a.segPeakDist - a.diaLeg + hvToDiaGap))
     297 + endX := int(numbers.Round(float64(bottomAr.Max.X) - a.segPeakDist + a.diaLeg - hvToDiaGap))
     298 + endY := int(numbers.Round(float64(rightAr.Max.Y) - a.segPeakDist + a.diaLeg - hvToDiaGap))
     299 + return image.Rect(startX, startY, endX, endY)
     300 +}
     301 + 
  • draw/segdisp/sixteen/doc/16-Segment-ASCII-All.jpg
  • draw/segdisp/sixteen/doc/segment_placement.graffle
    Binary file.
  • draw/segdisp/sixteen/doc/segment_placement.svg
  • ■ ■ ■ ■ ■ ■
    draw/segdisp/sixteen/sixteen.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +/*
     16 +Package sixteen simulates a 16-segment display drawn on a canvas.
     17 + 
     18 +Given a canvas, determines the placement and size of the individual
     19 +segments and exposes API that can turn individual segments on and off or
     20 +display ASCII characters.
     21 + 
     22 +The following outlines segments in the display and their names.
     23 + 
     24 + A1 A2
     25 + ------- -------
     26 + | \ | / |
     27 + | \ | / |
     28 + F | H J K | B
     29 + | \ | / |
     30 + | \ | / |
     31 + -G1---- ----G2-
     32 + | / | \ |
     33 + | / | \ |
     34 + E | N M L | C
     35 + | / | \ |
     36 + | / | \ |
     37 + ------- -------
     38 + D1 D2
     39 +*/
     40 +package sixteen
     41 + 
     42 +import (
     43 + "bytes"
     44 + "fmt"
     45 + "image"
     46 + "math"
     47 + 
     48 + "github.com/mum4k/termdash/area"
     49 + "github.com/mum4k/termdash/canvas"
     50 + "github.com/mum4k/termdash/canvas/braille"
     51 + "github.com/mum4k/termdash/cell"
     52 + "github.com/mum4k/termdash/draw/segdisp/segment"
     53 +)
     54 + 
     55 +// Segment represents a single segment in the display.
     56 +type Segment int
     57 + 
     58 +// String implements fmt.Stringer()
     59 +func (s Segment) String() string {
     60 + if n, ok := segmentNames[s]; ok {
     61 + return n
     62 + }
     63 + return "SegmentUnknown"
     64 +}
     65 + 
     66 +// segmentNames maps Segment values to human readable names.
     67 +var segmentNames = map[Segment]string{
     68 + A1: "A1",
     69 + A2: "A2",
     70 + B: "B",
     71 + C: "C",
     72 + D1: "D1",
     73 + D2: "D2",
     74 + E: "E",
     75 + F: "F",
     76 + G1: "G1",
     77 + G2: "G2",
     78 + H: "H",
     79 + J: "J",
     80 + K: "K",
     81 + L: "L",
     82 + M: "M",
     83 + N: "N",
     84 +}
     85 + 
     86 +const (
     87 + segmentUnknown Segment = iota
     88 + 
     89 + A1
     90 + A2
     91 + B
     92 + C
     93 + D1
     94 + D2
     95 + E
     96 + F
     97 + G1
     98 + G2
     99 + H
     100 + J
     101 + K
     102 + L
     103 + M
     104 + N
     105 + 
     106 + segmentMax // Used for validation.
     107 +)
     108 + 
     109 +// characterSegments maps characters that can be displayed on their segments.
     110 +// See doc/16-Segment-ASCII-All.jpg and:
     111 +// https://www.partsnotincluded.com/electronics/segmented-led-display-ascii-library
     112 +var characterSegments = map[rune][]Segment{
     113 + ' ': nil,
     114 + '!': {B, C},
     115 + '"': {J, B},
     116 + '#': {J, B, G1, G2, M, C, D1, D2},
     117 + '$': {A1, A2, F, J, G1, G2, M, C, D1, D2},
     118 + '%': {A1, F, J, K, G1, G2, N, M, C, D2},
     119 + '&': {A1, H, J, G1, E, L, D1, D2},
     120 + '\'': {J},
     121 + '(': {K, L},
     122 + ')': {H, N},
     123 + '*': {H, J, K, G1, G2, N, M, L},
     124 + '+': {J, G1, G2, M},
     125 + ',': {N},
     126 + '-': {G1, G2},
     127 + '/': {N, K},
     128 + 
     129 + '0': {A1, A2, F, K, B, E, N, C, D1, D2},
     130 + '1': {K, B, C},
     131 + '2': {A1, A2, B, G1, G2, E, D1, D2},
     132 + '3': {A1, A2, B, G2, C, D1, D2},
     133 + '4': {F, B, G1, G2, C},
     134 + '5': {A1, A2, F, G1, L, D1, D2},
     135 + '6': {A1, A2, F, G1, G2, E, C, D1, D2},
     136 + '7': {A1, A2, B, C},
     137 + '8': {A1, A2, F, B, G1, G2, E, C, D1, D2},
     138 + '9': {A1, A2, F, B, G1, G2, C, D1, D2},
     139 + 
     140 + ':': {J, M},
     141 + ';': {J, N},
     142 + '<': {K, G1, L},
     143 + '=': {G1, G2, D1, D2},
     144 + '>': {H, G2, N},
     145 + '?': {A1, A2, B, G2, M},
     146 + '@': {A1, A2, F, J, B, G2, E, D1, D2},
     147 + 
     148 + 'A': {A1, A2, F, B, G1, G2, E, C},
     149 + 'B': {A1, A2, J, B, G2, M, C, D1, D2},
     150 + 'C': {A1, A2, F, E, D1, D2},
     151 + 'D': {A1, A2, J, B, M, C, D1, D2},
     152 + 'E': {A1, A2, F, G1, E, D1, D2},
     153 + 'F': {A1, A2, F, G1, E},
     154 + 'G': {A1, A2, F, G2, E, C, D1, D2},
     155 + 'H': {F, B, G1, G2, E, C},
     156 + 'I': {A1, A2, J, M, D1, D2},
     157 + 'J': {B, E, C, D1, D2},
     158 + 'K': {F, K, G1, E, L},
     159 + 'L': {F, E, D1, D2},
     160 + 'M': {F, H, K, B, E, C},
     161 + 'N': {F, H, B, E, L, C},
     162 + 'O': {A1, A2, F, B, E, C, D1, D2},
     163 + 'P': {A1, A2, F, B, G1, G2, E},
     164 + 'Q': {A1, A2, F, B, E, L, C, D1, D2},
     165 + 'R': {A1, A2, F, B, G1, G2, E, L},
     166 + 'S': {A1, A2, F, G1, G2, C, D1, D2},
     167 + 'T': {A1, A2, J, M},
     168 + 'U': {F, B, E, C, D1, D2},
     169 + 'V': {F, K, E, N},
     170 + 'W': {F, E, N, L, C, B},
     171 + 'X': {H, K, N, L},
     172 + 'Y': {F, B, G1, G2, C, D1, D2},
     173 + 'Z': {A1, A2, K, N, D1, D2},
     174 + 
     175 + '[': {A2, J, M, D2},
     176 + '\\': {H, L},
     177 + ']': {A1, J, M, D1},
     178 + '^': {N, L},
     179 + '_': {D1, D2},
     180 + '`': {H},
     181 + 
     182 + 'a': {G1, E, M, D1, D2},
     183 + 'b': {F, G1, E, M, D1},
     184 + 'c': {G1, E, D1},
     185 + 'd': {B, G2, M, C, D2},
     186 + 'e': {G1, E, N, D1},
     187 + 'f': {A2, J, G1, G2, M},
     188 + 'g': {A1, F, J, G1, M, D1},
     189 + 'h': {F, G1, E, M},
     190 + 'i': {M},
     191 + 'j': {J, E, M, D1},
     192 + 'k': {J, K, M, L},
     193 + 'l': {F, E},
     194 + 'm': {G1, G2, E, M, C},
     195 + 'n': {G1, E, M},
     196 + 'o': {G1, E, M, D1},
     197 + 'p': {A1, F, J, G1, E},
     198 + 'q': {A1, F, J, G1, M},
     199 + 'r': {G1, E},
     200 + 's': {A1, F, G1, M, D1},
     201 + 't': {F, G1, E, D1},
     202 + 'u': {E, M, D1},
     203 + 'v': {E, N},
     204 + 'w': {E, N, L, C},
     205 + 'x': {H, K, N, L},
     206 + 'y': {J, B, G2, C, D2},
     207 + 'z': {G1, N, D1},
     208 + 
     209 + '{': {A2, J, G1, M, D2},
     210 + '|': {J, M},
     211 + '}': {A1, J, G2, M, D1},
     212 + '~': {K, G1, G2, N},
     213 +}
     214 + 
     215 +// SupportsChars asserts whether the display supports all runes in the
     216 +// provided string.
     217 +// The display only supports a subset of ASCII characters.
     218 +// Returns any unsupported runes found in the string in an unspecified order.
     219 +func SupportsChars(s string) (bool, []rune) {
     220 + unsupp := map[rune]bool{}
     221 + for _, r := range s {
     222 + if _, ok := characterSegments[r]; !ok {
     223 + unsupp[r] = true
     224 + }
     225 + }
     226 + 
     227 + var res []rune
     228 + for r := range unsupp {
     229 + res = append(res, r)
     230 + }
     231 + return len(res) == 0, res
     232 +}
     233 + 
     234 +// Sanitize returns a copy of the string, replacing all unsupported characters
     235 +// with a space character.
     236 +func Sanitize(s string) string {
     237 + var b bytes.Buffer
     238 + for _, r := range s {
     239 + if _, ok := characterSegments[r]; !ok {
     240 + b.WriteRune(' ')
     241 + continue
     242 + }
     243 + b.WriteRune(r)
     244 + }
     245 + return b.String()
     246 +}
     247 + 
     248 +// AllSegments returns all 16 segments in an undefined order.
     249 +func AllSegments() []Segment {
     250 + var res []Segment
     251 + for s := range segmentNames {
     252 + res = append(res, s)
     253 + }
     254 + return res
     255 +}
     256 + 
     257 +// Option is used to provide options.
     258 +type Option interface {
     259 + // set sets the provided option.
     260 + set(*Display)
     261 +}
     262 + 
     263 +// option implements Option.
     264 +type option func(*Display)
     265 + 
     266 +// set implements Option.set.
     267 +func (o option) set(d *Display) {
     268 + o(d)
     269 +}
     270 + 
     271 +// CellOpts sets the cell options on the cells that contain the segment display.
     272 +func CellOpts(cOpts ...cell.Option) Option {
     273 + return option(func(d *Display) {
     274 + d.cellOpts = cOpts
     275 + })
     276 +}
     277 + 
     278 +// Display represents the segment display.
     279 +// This object is not thread-safe.
     280 +type Display struct {
     281 + // segments maps segments to their current status.
     282 + segments map[Segment]bool
     283 + 
     284 + cellOpts []cell.Option
     285 +}
     286 + 
     287 +// New creates a new segment display.
     288 +// Initially all the segments are off.
     289 +func New(opts ...Option) *Display {
     290 + d := &Display{
     291 + segments: map[Segment]bool{},
     292 + }
     293 + 
     294 + for _, opt := range opts {
     295 + opt.set(d)
     296 + }
     297 + return d
     298 +}
     299 + 
     300 +// Clear clears the entire display, turning all segments off.
     301 +func (d *Display) Clear(opts ...Option) {
     302 + for _, opt := range opts {
     303 + opt.set(d)
     304 + }
     305 + 
     306 + d.segments = map[Segment]bool{}
     307 +}
     308 + 
     309 +// SetSegment sets the specified segment on.
     310 +// This method is idempotent.
     311 +func (d *Display) SetSegment(s Segment) error {
     312 + if s <= segmentUnknown || s >= segmentMax {
     313 + return fmt.Errorf("unknown segment %v(%d)", s, s)
     314 + }
     315 + d.segments[s] = true
     316 + return nil
     317 +}
     318 + 
     319 +// ClearSegment sets the specified segment off.
     320 +// This method is idempotent.
     321 +func (d *Display) ClearSegment(s Segment) error {
     322 + if s <= segmentUnknown || s >= segmentMax {
     323 + return fmt.Errorf("unknown segment %v(%d)", s, s)
     324 + }
     325 + d.segments[s] = false
     326 + return nil
     327 +}
     328 + 
     329 +// ToggleSegment toggles the state of the specified segment, i.e it either sets
     330 +// or clears it depending on its current state.
     331 +func (d *Display) ToggleSegment(s Segment) error {
     332 + if s <= segmentUnknown || s >= segmentMax {
     333 + return fmt.Errorf("unknown segment %v(%d)", s, s)
     334 + }
     335 + if d.segments[s] {
     336 + d.segments[s] = false
     337 + } else {
     338 + d.segments[s] = true
     339 + }
     340 + return nil
     341 +}
     342 + 
     343 +// Character sets all the segments that are needed to display the provided character.
     344 +// The display only supports a subset of ASCII characters, use SupportsChars()
     345 +// or Sanitize() to ensure the provided character is supported.
     346 +// Doesn't clear the display of segments set previously.
     347 +func (d *Display) SetCharacter(c rune) error {
     348 + seg, ok := characterSegments[c]
     349 + if !ok {
     350 + return fmt.Errorf("display doesn't support character %q rune(%v)", c, c)
     351 + }
     352 + 
     353 + for _, s := range seg {
     354 + if err := d.SetSegment(s); err != nil {
     355 + return err
     356 + }
     357 + }
     358 + return nil
     359 +}
     360 + 
     361 +// Minimum valid size of a cell canvas in order to draw the segment display.
     362 +const (
     363 + // MinCols is the smallest valid amount of columns in a cell area.
     364 + MinCols = 6
     365 + // MinRowPixels is the smallest valid amount of rows in a cell area.
     366 + MinRows = 5
     367 +)
     368 + 
     369 +// aspectRatio is the desired aspect ratio of a single segment display.
     370 +var aspectRatio = image.Point{3, 5}
     371 + 
     372 +// Draw draws the current state of the segment display onto the canvas.
     373 +// The canvas must be at least MinCols x MinRows cells, or an error will be
     374 +// returned.
     375 +// Any options provided to draw overwrite the values provided to New.
     376 +func (d *Display) Draw(cvs *canvas.Canvas, opts ...Option) error {
     377 + for _, o := range opts {
     378 + o.set(d)
     379 + }
     380 + 
     381 + bc, bcAr, err := toBraille(cvs)
     382 + if err != nil {
     383 + return err
     384 + }
     385 + 
     386 + attr := newAttributes(bcAr)
     387 + var sOpts []segment.Option
     388 + if len(d.cellOpts) > 0 {
     389 + sOpts = append(sOpts, segment.CellOpts(d.cellOpts...))
     390 + }
     391 + for _, segArg := range []struct {
     392 + s Segment
     393 + opts []segment.Option
     394 + }{
     395 + {A1, nil},
     396 + {A2, nil},
     397 + 
     398 + {F, nil},
     399 + {J, []segment.Option{segment.SkipSlopesLTE(2)}},
     400 + {B, []segment.Option{segment.ReverseSlopes()}},
     401 + 
     402 + {G1, []segment.Option{segment.SkipSlopesLTE(2)}},
     403 + {G2, []segment.Option{segment.SkipSlopesLTE(2)}},
     404 + 
     405 + {E, nil},
     406 + {M, []segment.Option{segment.SkipSlopesLTE(2)}},
     407 + {C, []segment.Option{segment.ReverseSlopes()}},
     408 + 
     409 + {D1, []segment.Option{segment.ReverseSlopes()}},
     410 + {D2, []segment.Option{segment.ReverseSlopes()}},
     411 + } {
     412 + if !d.segments[segArg.s] {
     413 + continue
     414 + }
     415 + sOpts := append(sOpts, segArg.opts...)
     416 + ar := attr.hvSegArea(segArg.s)
     417 + if err := segment.HV(bc, ar, hvSegType[segArg.s], sOpts...); err != nil {
     418 + return fmt.Errorf("failed to draw segment %v, segment.HV => %v", segArg.s, err)
     419 + }
     420 + }
     421 + 
     422 + var dsOpts []segment.DiagonalOption
     423 + if len(d.cellOpts) > 0 {
     424 + dsOpts = append(dsOpts, segment.DiagonalCellOpts(d.cellOpts...))
     425 + }
     426 + for _, seg := range []Segment{H, K, N, L} {
     427 + if !d.segments[seg] {
     428 + continue
     429 + }
     430 + ar := attr.diaSegArea(seg)
     431 + if err := segment.Diagonal(bc, ar, attr.segSize, diaSegType[seg], dsOpts...); err != nil {
     432 + return fmt.Errorf("failed to draw segment %v, segment.Diagonal => %v", seg, err)
     433 + }
     434 + }
     435 + return bc.CopyTo(cvs)
     436 +}
     437 + 
     438 +// Required, when given an area of cells, returns either an area of the same
     439 +// size or a smaller area that is required to draw one display.
     440 +// Returns a smaller area when the provided area didn't have the required
     441 +// aspect ratio.
     442 +// Returns an error if the area is too small to draw a segment display, i.e.
     443 +// smaller than MinCols x MinRows.
     444 +func Required(cellArea image.Rectangle) (image.Rectangle, error) {
     445 + if cols, rows := cellArea.Dx(), cellArea.Dy(); cols < MinCols || rows < MinRows {
     446 + 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",
     447 + cellArea, cols, rows, MinCols, MinRows)
     448 + }
     449 + 
     450 + bcAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Max.X*braille.ColMult, cellArea.Max.Y*braille.RowMult)
     451 + bcArAdj := area.WithRatio(bcAr, aspectRatio)
     452 + 
     453 + needCols := int(math.Ceil(float64(bcArAdj.Dx()) / braille.ColMult))
     454 + needRows := int(math.Ceil(float64(bcArAdj.Dy()) / braille.RowMult))
     455 + needAr := image.Rect(cellArea.Min.X, cellArea.Min.Y, cellArea.Min.X+needCols, cellArea.Min.Y+needRows)
     456 + return needAr, nil
     457 +}
     458 + 
     459 +// toBraille converts the canvas into a braille canvas and returns a pixel area
     460 +// with aspect ratio adjusted for the segment display.
     461 +func toBraille(cvs *canvas.Canvas) (*braille.Canvas, image.Rectangle, error) {
     462 + ar, err := Required(cvs.Area())
     463 + if err != nil {
     464 + return nil, image.ZR, fmt.Errorf("Required => %v", err)
     465 + }
     466 + 
     467 + bc, err := braille.New(ar)
     468 + if err != nil {
     469 + return nil, image.ZR, fmt.Errorf("braille.New => %v", err)
     470 + }
     471 + return bc, area.WithRatio(bc.Area(), aspectRatio), nil
     472 +}
     473 + 
Please wait...
Page is in error, reload to recover