Projects STRLCPY termdash Commits 60e0e5f9
🤬
Showing first 22 files as there are too many
  • ■ ■ ■ ■ ■ ■
    README.md
    skipped 97 lines
    98 98   
    99 99  [<img src="./images/barchartdemo.gif" alt="barchartdemo" type="image/gif">](widgets/barchart/barchartdemo/barchartdemo.go)
    100 100   
     101 +### The LineChart
     102 + 
     103 +Displays series of values as line charts. Run the
     104 +[linechartdemo](widgets/linechart/linechartdemo/linechartdemo.go).
     105 + 
     106 +[<img src="./images/linechartdemo.gif" alt="linechartdemo" type="image/gif">](widgets/linechart/linechartdemo/linechartdemo.go)
     107 + 
    101 108   
    102 109  ## Disclaimer
    103 110   
    skipped 2 lines
  • ■ ■ ■ ■ ■ ■
    canvas/braille/braille.go
     1 +// Copyright 2018 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 braille provides a canvas that uses braille characters.
     17 + 
     18 +This is inspired by https://github.com/asciimoo/drawille.
     19 + 
     20 +The braille patterns documentation:
     21 +http://www.alanwood.net/unicode/braille_patterns.html
     22 + 
     23 +The use of braille characters gives additional points (higher resolution) on
     24 +the canvas, each character cell now has eight pixels that can be set
     25 +independently. Specifically each cell has the following pixels, the axes grow
     26 +right and down.
     27 + 
     28 +Each cell:
     29 + 
     30 + X→ 0 1 Y
     31 + ┌───┐ ↓
     32 + │● ●│ 0
     33 + │● ●│ 1
     34 + │● ●│ 2
     35 + │● ●│ 3
     36 + └───┘
     37 + 
     38 +When using the braille canvas, the coordinates address the sub-cell points
     39 +rather then cells themselves. However all points in the cell still share the
     40 +same cell options.
     41 +*/
     42 +package braille
     43 + 
     44 +import (
     45 + "fmt"
     46 + "image"
     47 + 
     48 + "github.com/mum4k/termdash/canvas"
     49 + "github.com/mum4k/termdash/cell"
     50 + "github.com/mum4k/termdash/terminalapi"
     51 +)
     52 + 
     53 +const (
     54 + // ColMult is the resolution multiplier for the width, i.e. two pixels per cell.
     55 + ColMult = 2
     56 + 
     57 + // RowMult is the resolution multiplier for the height, i.e. four pixels per cell.
     58 + RowMult = 4
     59 + 
     60 + // brailleCharOffset is the offset of the braille pattern unicode characters.
     61 + // From: http://www.alanwood.net/unicode/braille_patterns.html
     62 + brailleCharOffset = 0x2800
     63 + 
     64 + // brailleLastChar is the last braille pattern rune.
     65 + brailleLastChar = 0x28FF
     66 +)
     67 + 
     68 +// pixelRunes maps points addressing individual pixels in a cell into character
     69 +// offset. I.e. the correct character to set pixel(0,0) is
     70 +// brailleCharOffset|pixelRunes[image.Point{0,0}].
     71 +var pixelRunes = map[image.Point]rune{
     72 + {0, 0}: 0x01, {1, 0}: 0x08,
     73 + {0, 1}: 0x02, {1, 1}: 0x10,
     74 + {0, 2}: 0x04, {1, 2}: 0x20,
     75 + {0, 3}: 0x40, {1, 3}: 0x80,
     76 +}
     77 + 
     78 +// Canvas is a canvas that uses the braille patterns. It is two times wider
     79 +// and four times taller than a regular canvas that uses just plain characters,
     80 +// since each cell now has 2x4 pixels that can be independently set.
     81 +//
     82 +// The braille canvas is an abstraction built on top of a regular character
     83 +// canvas. After setting and toggling pixels on the braille canvas, it should
     84 +// be copied to a regular character canvas or applied to a terminal which
     85 +// results in setting of braille pattern characters.
     86 +// See the examples for more details.
     87 +//
     88 +// The created braille canvas can be smaller and even misaligned relatively to
     89 +// the regular character canvas or terminal, allowing the callers to create a
     90 +// "view" of just a portion of the canvas or terminal.
     91 +type Canvas struct {
     92 + // regular is the regular character canvas the braille canvas is based on.
     93 + regular *canvas.Canvas
     94 +}
     95 + 
     96 +// New returns a new braille canvas for the provided area.
     97 +func New(ar image.Rectangle) (*Canvas, error) {
     98 + rc, err := canvas.New(ar)
     99 + if err != nil {
     100 + return nil, err
     101 + }
     102 + return &Canvas{
     103 + regular: rc,
     104 + }, nil
     105 +}
     106 + 
     107 +// Size returns the size of the braille canvas in pixels.
     108 +func (c *Canvas) Size() image.Point {
     109 + s := c.regular.Size()
     110 + return image.Point{s.X * ColMult, s.Y * RowMult}
     111 +}
     112 + 
     113 +// Area returns the area of the braille canvas in pixels.
     114 +// This will be zero-based area that is two times wider and four times taller
     115 +// than the area used to create the braille canvas.
     116 +func (c *Canvas) Area() image.Rectangle {
     117 + ar := c.regular.Area()
     118 + return image.Rect(0, 0, ar.Dx()*ColMult, ar.Dx()*RowMult)
     119 +}
     120 + 
     121 +// Clear clears all the content on the canvas.
     122 +func (c *Canvas) Clear() error {
     123 + return c.regular.Clear()
     124 +}
     125 + 
     126 +// SetPixel turns on pixel at the specified point.
     127 +// The provided cell options will be applied to the entire cell (all of its
     128 +// pixels). This method is idempotent.
     129 +func (c *Canvas) SetPixel(p image.Point, opts ...cell.Option) error {
     130 + cp, err := c.cellPoint(p)
     131 + if err != nil {
     132 + return err
     133 + }
     134 + cell, err := c.regular.Cell(cp)
     135 + if err != nil {
     136 + return err
     137 + }
     138 + 
     139 + var r rune
     140 + if isBraille(cell.Rune) {
     141 + // If the cell already has a braille pattern rune, we will be adding
     142 + // the pixel.
     143 + r = cell.Rune
     144 + } else {
     145 + r = brailleCharOffset
     146 + }
     147 + 
     148 + r |= pixelRunes[pixelPoint(p)]
     149 + if _, err := c.regular.SetCell(cp, r, opts...); err != nil {
     150 + return err
     151 + }
     152 + return nil
     153 +}
     154 + 
     155 +// ClearPixel turns off pixel at the specified point.
     156 +// The provided cell options will be applied to the entire cell (all of its
     157 +// pixels). This method is idempotent.
     158 +func (c *Canvas) ClearPixel(p image.Point, opts ...cell.Option) error {
     159 + cp, err := c.cellPoint(p)
     160 + if err != nil {
     161 + return err
     162 + }
     163 + cell, err := c.regular.Cell(cp)
     164 + if err != nil {
     165 + return err
     166 + }
     167 + 
     168 + // Clear is idempotent.
     169 + if !isBraille(cell.Rune) || !pixelSet(cell.Rune, p) {
     170 + return nil
     171 + }
     172 + 
     173 + r := cell.Rune & ^pixelRunes[pixelPoint(p)]
     174 + if _, err := c.regular.SetCell(cp, r, opts...); err != nil {
     175 + return err
     176 + }
     177 + return nil
     178 +}
     179 + 
     180 +// TogglePixel toggles the state of the pixel at the specified point, i.e. it
     181 +// either sets or clear it depending on its current state.
     182 +// The provided cell options will be applied to the entire cell (all of its
     183 +// pixels).
     184 +func (c *Canvas) TogglePixel(p image.Point, opts ...cell.Option) error {
     185 + cp, err := c.cellPoint(p)
     186 + if err != nil {
     187 + return err
     188 + }
     189 + cell, err := c.regular.Cell(cp)
     190 + if err != nil {
     191 + return err
     192 + }
     193 + 
     194 + if isBraille(cell.Rune) && pixelSet(cell.Rune, p) {
     195 + return c.ClearPixel(p, opts...)
     196 + }
     197 + return c.SetPixel(p, opts...)
     198 +}
     199 + 
     200 +// Apply applies the canvas to the corresponding area of the terminal.
     201 +// Guarantees to stay within limits of the area the canvas was created with.
     202 +func (c *Canvas) Apply(t terminalapi.Terminal) error {
     203 + return c.regular.Apply(t)
     204 +}
     205 + 
     206 +// CopyTo copies the content of this canvas onto the destination canvas.
     207 +// This canvas can have an offset when compared to the destination canvas, i.e.
     208 +// the area of this canvas doesn't have to be zero-based.
     209 +func (c *Canvas) CopyTo(dst *canvas.Canvas) error {
     210 + return c.regular.CopyTo(dst)
     211 +}
     212 + 
     213 +// cellPoint determines the point (coordinate) of the character cell given
     214 +// coordinates in pixels.
     215 +func (c *Canvas) cellPoint(p image.Point) (image.Point, error) {
     216 + cp := image.Point{p.X / ColMult, p.Y / RowMult}
     217 + if ar := c.regular.Area(); !cp.In(ar) {
     218 + return image.ZP, fmt.Errorf("pixel at%v would be in a character cell at%v which falls outside of the canvas area %v", p, cp, ar)
     219 + }
     220 + return cp, nil
     221 +}
     222 + 
     223 +// isBraille determines if the rune is a braille pattern rune.
     224 +func isBraille(r rune) bool {
     225 + return r >= brailleCharOffset && r <= brailleLastChar
     226 +}
     227 + 
     228 +// pixelSet returns true if the provided rune has the specified pixel set.
     229 +func pixelSet(r rune, p image.Point) bool {
     230 + return r&pixelRunes[p] == 1
     231 +}
     232 + 
     233 +// pixelPoint translates point within canvas to point within the target cell.
     234 +func pixelPoint(p image.Point) image.Point {
     235 + return image.Point{p.X % ColMult, p.Y % RowMult}
     236 +}
     237 + 
  • ■ ■ ■ ■ ■ ■
    canvas/braille/braille_test.go
     1 +// Copyright 2018 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 braille
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 + "github.com/mum4k/termdash/area"
     23 + "github.com/mum4k/termdash/canvas"
     24 + "github.com/mum4k/termdash/canvas/testcanvas"
     25 + "github.com/mum4k/termdash/cell"
     26 + "github.com/mum4k/termdash/terminal/faketerm"
     27 +)
     28 + 
     29 +func Example_copiedToCanvas() {
     30 + // Given a parent canvas the widget receives from the infrastructure:
     31 + parent, err := canvas.New(image.Rect(0, 0, 3, 3))
     32 + if err != nil {
     33 + panic(err)
     34 + }
     35 + 
     36 + // The widget can create a braille canvas with the same or smaller area:
     37 + braille, err := New(parent.Area())
     38 + if err != nil {
     39 + panic(err)
     40 + }
     41 + 
     42 + // After setting / clearing / toggling of pixels on the braille canvas, it
     43 + // can be copied back to the parent canvas.
     44 + if err := braille.SetPixel(image.Point{0, 0}); err != nil {
     45 + panic(err)
     46 + }
     47 + if err := braille.CopyTo(parent); err != nil {
     48 + panic(err)
     49 + }
     50 +}
     51 + 
     52 +func Example_applidToTerminal() {
     53 + // When working with a terminal directly:
     54 + ft, err := faketerm.New(image.Point{3, 3})
     55 + if err != nil {
     56 + panic(err)
     57 + }
     58 + 
     59 + // The widget can create a braille canvas with the same or smaller area:
     60 + braille, err := New(ft.Area())
     61 + if err != nil {
     62 + panic(err)
     63 + }
     64 + 
     65 + // After setting / clearing / toggling of pixels on the braille canvas, it
     66 + // can be applied to the terminal.
     67 + if err := braille.SetPixel(image.Point{0, 0}); err != nil {
     68 + panic(err)
     69 + }
     70 + if err := braille.Apply(ft); err != nil {
     71 + panic(err)
     72 + }
     73 +}
     74 + 
     75 +func TestNew(t *testing.T) {
     76 + tests := []struct {
     77 + desc string
     78 + ar image.Rectangle
     79 + wantSize image.Point
     80 + wantArea image.Rectangle
     81 + wantErr bool
     82 + }{
     83 + {
     84 + desc: "fails on a negative area",
     85 + ar: image.Rect(-1, -1, -2, -2),
     86 + wantErr: true,
     87 + },
     88 + {
     89 + desc: "braille from zero-based single-cell area",
     90 + ar: image.Rect(0, 0, 1, 1),
     91 + wantSize: image.Point{2, 4},
     92 + wantArea: image.Rect(0, 0, 2, 4),
     93 + },
     94 + {
     95 + desc: "braille from non-zero-based single-cell area",
     96 + ar: image.Rect(3, 3, 4, 4),
     97 + wantSize: image.Point{2, 4},
     98 + wantArea: image.Rect(0, 0, 2, 4),
     99 + },
     100 + {
     101 + desc: "braille from zero-based multi-cell area",
     102 + ar: image.Rect(0, 0, 3, 3),
     103 + wantSize: image.Point{6, 12},
     104 + wantArea: image.Rect(0, 0, 6, 12),
     105 + },
     106 + {
     107 + desc: "braille from non-zero-based multi-cell area",
     108 + ar: image.Rect(6, 6, 9, 9),
     109 + wantSize: image.Point{6, 12},
     110 + wantArea: image.Rect(0, 0, 6, 12),
     111 + },
     112 + }
     113 + 
     114 + for _, tc := range tests {
     115 + t.Run(tc.desc, func(t *testing.T) {
     116 + got, err := New(tc.ar)
     117 + if (err != nil) != tc.wantErr {
     118 + t.Errorf("New => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     119 + }
     120 + if err != nil {
     121 + return
     122 + }
     123 + 
     124 + gotSize := got.Size()
     125 + if diff := pretty.Compare(tc.wantSize, gotSize); diff != "" {
     126 + t.Errorf("Size => unexpected diff (-want, +got):\n%s", diff)
     127 + }
     128 + 
     129 + gotArea := got.Area()
     130 + if diff := pretty.Compare(tc.wantArea, gotArea); diff != "" {
     131 + t.Errorf("Area => unexpected diff (-want, +got):\n%s", diff)
     132 + }
     133 + })
     134 + }
     135 +}
     136 + 
     137 +func TestBraille(t *testing.T) {
     138 + tests := []struct {
     139 + desc string
     140 + ar image.Rectangle
     141 + pixelOps func(*Canvas) error
     142 + want func(size image.Point) *faketerm.Terminal
     143 + wantErr bool
     144 + }{
     145 + {
     146 + desc: "set pixel 0,0",
     147 + ar: image.Rect(0, 0, 1, 1),
     148 + pixelOps: func(c *Canvas) error {
     149 + return c.SetPixel(image.Point{0, 0})
     150 + },
     151 + want: func(size image.Point) *faketerm.Terminal {
     152 + ft := faketerm.MustNew(size)
     153 + c := testcanvas.MustNew(ft.Area())
     154 + 
     155 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠁')
     156 + 
     157 + testcanvas.MustApply(c, ft)
     158 + return ft
     159 + },
     160 + },
     161 + {
     162 + desc: "set is idempotent",
     163 + ar: image.Rect(0, 0, 1, 1),
     164 + pixelOps: func(c *Canvas) error {
     165 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     166 + return err
     167 + }
     168 + return c.SetPixel(image.Point{0, 0})
     169 + },
     170 + want: func(size image.Point) *faketerm.Terminal {
     171 + ft := faketerm.MustNew(size)
     172 + c := testcanvas.MustNew(ft.Area())
     173 + 
     174 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠁')
     175 + 
     176 + testcanvas.MustApply(c, ft)
     177 + return ft
     178 + },
     179 + },
     180 + {
     181 + desc: "set pixel 1,0",
     182 + ar: image.Rect(0, 0, 1, 1),
     183 + pixelOps: func(c *Canvas) error {
     184 + return c.SetPixel(image.Point{1, 0})
     185 + },
     186 + want: func(size image.Point) *faketerm.Terminal {
     187 + ft := faketerm.MustNew(size)
     188 + c := testcanvas.MustNew(ft.Area())
     189 + 
     190 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠈')
     191 + 
     192 + testcanvas.MustApply(c, ft)
     193 + return ft
     194 + },
     195 + },
     196 + {
     197 + desc: "set pixel 0,1",
     198 + ar: image.Rect(0, 0, 1, 1),
     199 + pixelOps: func(c *Canvas) error {
     200 + return c.SetPixel(image.Point{0, 1})
     201 + },
     202 + want: func(size image.Point) *faketerm.Terminal {
     203 + ft := faketerm.MustNew(size)
     204 + c := testcanvas.MustNew(ft.Area())
     205 + 
     206 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠂')
     207 + 
     208 + testcanvas.MustApply(c, ft)
     209 + return ft
     210 + },
     211 + },
     212 + {
     213 + desc: "set pixel 1,1",
     214 + ar: image.Rect(0, 0, 1, 1),
     215 + pixelOps: func(c *Canvas) error {
     216 + return c.SetPixel(image.Point{1, 1})
     217 + },
     218 + want: func(size image.Point) *faketerm.Terminal {
     219 + ft := faketerm.MustNew(size)
     220 + c := testcanvas.MustNew(ft.Area())
     221 + 
     222 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠐')
     223 + 
     224 + testcanvas.MustApply(c, ft)
     225 + return ft
     226 + },
     227 + },
     228 + {
     229 + desc: "set pixel 0,2",
     230 + ar: image.Rect(0, 0, 1, 1),
     231 + pixelOps: func(c *Canvas) error {
     232 + return c.SetPixel(image.Point{0, 2})
     233 + },
     234 + want: func(size image.Point) *faketerm.Terminal {
     235 + ft := faketerm.MustNew(size)
     236 + c := testcanvas.MustNew(ft.Area())
     237 + 
     238 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠄')
     239 + 
     240 + testcanvas.MustApply(c, ft)
     241 + return ft
     242 + },
     243 + },
     244 + {
     245 + desc: "set pixel 1,2",
     246 + ar: image.Rect(0, 0, 1, 1),
     247 + pixelOps: func(c *Canvas) error {
     248 + return c.SetPixel(image.Point{1, 2})
     249 + },
     250 + want: func(size image.Point) *faketerm.Terminal {
     251 + ft := faketerm.MustNew(size)
     252 + c := testcanvas.MustNew(ft.Area())
     253 + 
     254 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠠')
     255 + 
     256 + testcanvas.MustApply(c, ft)
     257 + return ft
     258 + },
     259 + },
     260 + {
     261 + desc: "set pixel 0,3",
     262 + ar: image.Rect(0, 0, 1, 1),
     263 + pixelOps: func(c *Canvas) error {
     264 + return c.SetPixel(image.Point{0, 3})
     265 + },
     266 + want: func(size image.Point) *faketerm.Terminal {
     267 + ft := faketerm.MustNew(size)
     268 + c := testcanvas.MustNew(ft.Area())
     269 + 
     270 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⡀')
     271 + 
     272 + testcanvas.MustApply(c, ft)
     273 + return ft
     274 + },
     275 + },
     276 + {
     277 + desc: "set pixel 1,3",
     278 + ar: image.Rect(0, 0, 1, 1),
     279 + pixelOps: func(c *Canvas) error {
     280 + return c.SetPixel(image.Point{1, 3})
     281 + },
     282 + want: func(size image.Point) *faketerm.Terminal {
     283 + ft := faketerm.MustNew(size)
     284 + c := testcanvas.MustNew(ft.Area())
     285 + 
     286 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⢀')
     287 + 
     288 + testcanvas.MustApply(c, ft)
     289 + return ft
     290 + },
     291 + },
     292 + {
     293 + desc: "fails on point outside of the canvas",
     294 + ar: image.Rect(0, 0, 1, 1),
     295 + pixelOps: func(c *Canvas) error {
     296 + return c.SetPixel(image.Point{2, 0})
     297 + },
     298 + want: func(size image.Point) *faketerm.Terminal {
     299 + return faketerm.MustNew(size)
     300 + },
     301 + wantErr: true,
     302 + },
     303 + {
     304 + desc: "clears the canvas",
     305 + ar: image.Rect(0, 0, 1, 1),
     306 + pixelOps: func(c *Canvas) error {
     307 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     308 + return err
     309 + }
     310 + return c.Clear()
     311 + },
     312 + want: func(size image.Point) *faketerm.Terminal {
     313 + return faketerm.MustNew(size)
     314 + },
     315 + },
     316 + {
     317 + desc: "sets multiple pixels",
     318 + ar: image.Rect(0, 0, 1, 1),
     319 + pixelOps: func(c *Canvas) error {
     320 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     321 + return err
     322 + }
     323 + if err := c.SetPixel(image.Point{1, 0}); err != nil {
     324 + return err
     325 + }
     326 + return c.SetPixel(image.Point{0, 1})
     327 + },
     328 + want: func(size image.Point) *faketerm.Terminal {
     329 + ft := faketerm.MustNew(size)
     330 + c := testcanvas.MustNew(ft.Area())
     331 + 
     332 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠋')
     333 + 
     334 + testcanvas.MustApply(c, ft)
     335 + return ft
     336 + },
     337 + },
     338 + {
     339 + desc: "sets all the pixels in a cell",
     340 + ar: image.Rect(0, 0, 1, 1),
     341 + pixelOps: func(c *Canvas) error {
     342 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     343 + return err
     344 + }
     345 + if err := c.SetPixel(image.Point{1, 0}); err != nil {
     346 + return err
     347 + }
     348 + if err := c.SetPixel(image.Point{0, 1}); err != nil {
     349 + return err
     350 + }
     351 + if err := c.SetPixel(image.Point{1, 1}); err != nil {
     352 + return err
     353 + }
     354 + if err := c.SetPixel(image.Point{0, 2}); err != nil {
     355 + return err
     356 + }
     357 + if err := c.SetPixel(image.Point{1, 2}); err != nil {
     358 + return err
     359 + }
     360 + if err := c.SetPixel(image.Point{0, 3}); err != nil {
     361 + return err
     362 + }
     363 + return c.SetPixel(image.Point{1, 3})
     364 + },
     365 + want: func(size image.Point) *faketerm.Terminal {
     366 + ft := faketerm.MustNew(size)
     367 + c := testcanvas.MustNew(ft.Area())
     368 + 
     369 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⣿')
     370 + 
     371 + testcanvas.MustApply(c, ft)
     372 + return ft
     373 + },
     374 + },
     375 + {
     376 + desc: "set cell options",
     377 + ar: image.Rect(0, 0, 1, 1),
     378 + pixelOps: func(c *Canvas) error {
     379 + return c.SetPixel(image.Point{0, 0}, cell.FgColor(cell.ColorRed))
     380 + },
     381 + want: func(size image.Point) *faketerm.Terminal {
     382 + ft := faketerm.MustNew(size)
     383 + c := testcanvas.MustNew(ft.Area())
     384 + 
     385 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠁', cell.FgColor(cell.ColorRed))
     386 + 
     387 + testcanvas.MustApply(c, ft)
     388 + return ft
     389 + },
     390 + },
     391 + {
     392 + desc: "set pixels in multiple cells",
     393 + ar: image.Rect(0, 0, 2, 2),
     394 + pixelOps: func(c *Canvas) error {
     395 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     396 + return err
     397 + }
     398 + if err := c.SetPixel(image.Point{2, 2}); err != nil {
     399 + return err
     400 + }
     401 + return c.SetPixel(image.Point{1, 7})
     402 + },
     403 + want: func(size image.Point) *faketerm.Terminal {
     404 + ft := faketerm.MustNew(size)
     405 + c := testcanvas.MustNew(ft.Area())
     406 + 
     407 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠁') // pixel 0,0
     408 + testcanvas.MustSetCell(c, image.Point{1, 0}, '⠄') // pixel 0,2
     409 + testcanvas.MustSetCell(c, image.Point{0, 1}, '⢀') // pixel 1,3
     410 + 
     411 + testcanvas.MustApply(c, ft)
     412 + return ft
     413 + },
     414 + },
     415 + {
     416 + desc: "sets and clears pixels",
     417 + ar: image.Rect(0, 0, 1, 1),
     418 + pixelOps: func(c *Canvas) error {
     419 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     420 + return err
     421 + }
     422 + if err := c.SetPixel(image.Point{1, 0}); err != nil {
     423 + return err
     424 + }
     425 + if err := c.SetPixel(image.Point{0, 1}); err != nil {
     426 + return err
     427 + }
     428 + return c.ClearPixel(image.Point{0, 0})
     429 + },
     430 + want: func(size image.Point) *faketerm.Terminal {
     431 + ft := faketerm.MustNew(size)
     432 + c := testcanvas.MustNew(ft.Area())
     433 + 
     434 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠊')
     435 + 
     436 + testcanvas.MustApply(c, ft)
     437 + return ft
     438 + },
     439 + },
     440 + {
     441 + desc: "clear is idempotent when cell doesn't contain braille pattern character",
     442 + ar: image.Rect(0, 0, 1, 1),
     443 + pixelOps: func(c *Canvas) error {
     444 + return c.ClearPixel(image.Point{0, 0})
     445 + },
     446 + want: func(size image.Point) *faketerm.Terminal {
     447 + return faketerm.MustNew(size)
     448 + },
     449 + },
     450 + {
     451 + desc: "clear fails on point outside of the canvas",
     452 + ar: image.Rect(0, 0, 1, 1),
     453 + pixelOps: func(c *Canvas) error {
     454 + return c.ClearPixel(image.Point{3, 1})
     455 + },
     456 + want: func(size image.Point) *faketerm.Terminal {
     457 + return faketerm.MustNew(size)
     458 + },
     459 + wantErr: true,
     460 + },
     461 + {
     462 + desc: "clear is idempotent when the pixel is already cleared",
     463 + ar: image.Rect(0, 0, 1, 1),
     464 + pixelOps: func(c *Canvas) error {
     465 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     466 + return err
     467 + }
     468 + if err := c.ClearPixel(image.Point{0, 0}); err != nil {
     469 + return err
     470 + }
     471 + return c.ClearPixel(image.Point{0, 0})
     472 + },
     473 + want: func(size image.Point) *faketerm.Terminal {
     474 + ft := faketerm.MustNew(size)
     475 + c := testcanvas.MustNew(ft.Area())
     476 + 
     477 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠀')
     478 + 
     479 + testcanvas.MustApply(c, ft)
     480 + return ft
     481 + },
     482 + },
     483 + {
     484 + desc: "clearing a pixel sets options on the cell",
     485 + ar: image.Rect(0, 0, 1, 1),
     486 + pixelOps: func(c *Canvas) error {
     487 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     488 + return err
     489 + }
     490 + if err := c.SetPixel(image.Point{1, 0}); err != nil {
     491 + return err
     492 + }
     493 + if err := c.SetPixel(image.Point{0, 1}); err != nil {
     494 + return err
     495 + }
     496 + return c.ClearPixel(image.Point{0, 0}, cell.FgColor(cell.ColorBlue))
     497 + },
     498 + want: func(size image.Point) *faketerm.Terminal {
     499 + ft := faketerm.MustNew(size)
     500 + c := testcanvas.MustNew(ft.Area())
     501 + 
     502 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠊', cell.FgColor(cell.ColorBlue))
     503 + 
     504 + testcanvas.MustApply(c, ft)
     505 + return ft
     506 + },
     507 + },
     508 + {
     509 + desc: "toggles a pixel which adds the first braille pattern character",
     510 + ar: image.Rect(0, 0, 1, 1),
     511 + pixelOps: func(c *Canvas) error {
     512 + return c.TogglePixel(image.Point{0, 0})
     513 + },
     514 + want: func(size image.Point) *faketerm.Terminal {
     515 + ft := faketerm.MustNew(size)
     516 + c := testcanvas.MustNew(ft.Area())
     517 + 
     518 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠁')
     519 + 
     520 + testcanvas.MustApply(c, ft)
     521 + return ft
     522 + },
     523 + },
     524 + {
     525 + desc: "toggles a pixel on",
     526 + ar: image.Rect(0, 0, 1, 1),
     527 + pixelOps: func(c *Canvas) error {
     528 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     529 + return err
     530 + }
     531 + return c.TogglePixel(image.Point{1, 0})
     532 + },
     533 + want: func(size image.Point) *faketerm.Terminal {
     534 + ft := faketerm.MustNew(size)
     535 + c := testcanvas.MustNew(ft.Area())
     536 + 
     537 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠉')
     538 + 
     539 + testcanvas.MustApply(c, ft)
     540 + return ft
     541 + },
     542 + },
     543 + {
     544 + desc: "toggles a pixel on and sets options",
     545 + ar: image.Rect(0, 0, 1, 1),
     546 + pixelOps: func(c *Canvas) error {
     547 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     548 + return err
     549 + }
     550 + return c.TogglePixel(image.Point{1, 0}, cell.FgColor(cell.ColorBlue))
     551 + },
     552 + want: func(size image.Point) *faketerm.Terminal {
     553 + ft := faketerm.MustNew(size)
     554 + c := testcanvas.MustNew(ft.Area())
     555 + 
     556 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠉', cell.FgColor(cell.ColorBlue))
     557 + 
     558 + testcanvas.MustApply(c, ft)
     559 + return ft
     560 + },
     561 + },
     562 + {
     563 + desc: "toggles a pixel off",
     564 + ar: image.Rect(0, 0, 1, 1),
     565 + pixelOps: func(c *Canvas) error {
     566 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     567 + return err
     568 + }
     569 + if err := c.SetPixel(image.Point{1, 0}); err != nil {
     570 + return err
     571 + }
     572 + return c.TogglePixel(image.Point{0, 0})
     573 + },
     574 + want: func(size image.Point) *faketerm.Terminal {
     575 + ft := faketerm.MustNew(size)
     576 + c := testcanvas.MustNew(ft.Area())
     577 + 
     578 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠈')
     579 + 
     580 + testcanvas.MustApply(c, ft)
     581 + return ft
     582 + },
     583 + },
     584 + {
     585 + desc: "toggles a pixel off and sets options",
     586 + ar: image.Rect(0, 0, 1, 1),
     587 + pixelOps: func(c *Canvas) error {
     588 + if err := c.SetPixel(image.Point{0, 0}); err != nil {
     589 + return err
     590 + }
     591 + if err := c.SetPixel(image.Point{1, 0}); err != nil {
     592 + return err
     593 + }
     594 + return c.TogglePixel(image.Point{0, 0}, cell.FgColor(cell.ColorBlue))
     595 + },
     596 + want: func(size image.Point) *faketerm.Terminal {
     597 + ft := faketerm.MustNew(size)
     598 + c := testcanvas.MustNew(ft.Area())
     599 + 
     600 + testcanvas.MustSetCell(c, image.Point{0, 0}, '⠈', cell.FgColor(cell.ColorBlue))
     601 + 
     602 + testcanvas.MustApply(c, ft)
     603 + return ft
     604 + },
     605 + },
     606 + {
     607 + desc: "toggle fails on point outside of the canvas",
     608 + ar: image.Rect(0, 0, 1, 1),
     609 + pixelOps: func(c *Canvas) error {
     610 + return c.TogglePixel(image.Point{3, 3})
     611 + },
     612 + want: func(size image.Point) *faketerm.Terminal {
     613 + return faketerm.MustNew(size)
     614 + },
     615 + wantErr: true,
     616 + },
     617 + }
     618 + 
     619 + for _, tc := range tests {
     620 + t.Run(tc.desc, func(t *testing.T) {
     621 + bc, err := New(tc.ar)
     622 + if err != nil {
     623 + t.Fatalf("New => unexpected error: %v", err)
     624 + }
     625 + 
     626 + err = tc.pixelOps(bc)
     627 + if (err != nil) != tc.wantErr {
     628 + t.Errorf("pixelOps => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     629 + }
     630 + if err != nil {
     631 + return
     632 + }
     633 + 
     634 + size := area.Size(tc.ar)
     635 + gotApplied, err := faketerm.New(size)
     636 + if err != nil {
     637 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     638 + }
     639 + if err := bc.Apply(gotApplied); err != nil {
     640 + t.Fatalf("bc.Apply => unexpected error: %v", err)
     641 + }
     642 + if diff := faketerm.Diff(tc.want(size), gotApplied); diff != "" {
     643 + t.Fatalf("Direct Apply => %v", diff)
     644 + }
     645 + 
     646 + // When copied to another another canvas, the result on the
     647 + // terminal must be the same.
     648 + rc, err := canvas.New(tc.ar)
     649 + if err != nil {
     650 + t.Fatalf("canvas.New => unexpected error: %v", err)
     651 + }
     652 + 
     653 + if err := bc.CopyTo(rc); err != nil {
     654 + t.Fatalf("CopyTo => unexpected error: %v", err)
     655 + }
     656 + 
     657 + gotCopied, err := faketerm.New(size)
     658 + if err != nil {
     659 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     660 + }
     661 + if err := rc.Apply(gotCopied); err != nil {
     662 + t.Fatalf("rc.Apply => unexpected error: %v", err)
     663 + }
     664 + if diff := faketerm.Diff(tc.want(size), gotCopied); diff != "" {
     665 + t.Fatalf("Copy then Apply => %v", diff)
     666 + }
     667 + })
     668 + }
     669 +}
     670 + 
  • ■ ■ ■ ■ ■ ■
    canvas/braille/testbraille/testbraille.go
     1 +// Copyright 2018 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 testbraille provides helpers for tests that use the braille package.
     16 +package testbraille
     17 + 
     18 +import (
     19 + "fmt"
     20 + "image"
     21 + 
     22 + "github.com/mum4k/termdash/canvas"
     23 + "github.com/mum4k/termdash/canvas/braille"
     24 + "github.com/mum4k/termdash/cell"
     25 + "github.com/mum4k/termdash/terminal/faketerm"
     26 +)
     27 + 
     28 +// MustNew returns a new canvas or panics.
     29 +func MustNew(area image.Rectangle) *braille.Canvas {
     30 + cvs, err := braille.New(area)
     31 + if err != nil {
     32 + panic(fmt.Sprintf("braille.New => unexpected error: %v", err))
     33 + }
     34 + return cvs
     35 +}
     36 + 
     37 +// MustApply applies the canvas on the terminal or panics.
     38 +func MustApply(bc *braille.Canvas, t *faketerm.Terminal) {
     39 + if err := bc.Apply(t); err != nil {
     40 + panic(fmt.Sprintf("braille.Apply => unexpected error: %v", err))
     41 + }
     42 +}
     43 + 
     44 +// MustSetPixel sets the specified pixel or panics.
     45 +func MustSetPixel(bc *braille.Canvas, p image.Point, opts ...cell.Option) {
     46 + if err := bc.SetPixel(p, opts...); err != nil {
     47 + panic(fmt.Sprintf("braille.SetPixel => unexpected error: %v", err))
     48 + }
     49 +}
     50 + 
     51 +// MustCopyTo copies the braille canvas onto the provided canvas or panics.
     52 +func MustCopyTo(bc *braille.Canvas, dst *canvas.Canvas) {
     53 + if err := bc.CopyTo(dst); err != nil {
     54 + panic(fmt.Sprintf("bc.CopyTo => unexpected error: %v", err))
     55 + }
     56 +}
     57 + 
  • ■ ■ ■ ■ ■ ■
    canvas/canvas.go
    skipped 93 lines
    94 94   return c.buffer[p.X][p.Y].Copy(), nil
    95 95  }
    96 96   
    97  -// Apply applies the canvas to the corresponding area of the terminal.
    98  -// Guarantees to stay within limits of the area the canvas was created with.
    99  -func (c *Canvas) Apply(t terminalapi.Terminal) error {
    100  - termArea, err := area.FromSize(t.Size())
    101  - if err != nil {
    102  - return err
    103  - }
     97 +// setCellFunc is a function that sets cell content on a terminal or a canvas.
     98 +type setCellFunc func(image.Point, rune, ...cell.Option) error
    104 99   
    105  - bufArea, err := area.FromSize(c.buffer.Size())
    106  - if err != nil {
    107  - return err
    108  - }
    109  - 
    110  - if !bufArea.In(termArea) {
    111  - return fmt.Errorf("the canvas area %+v doesn't fit onto the terminal %+v", bufArea, termArea)
    112  - }
    113  - 
     100 +// copyTo is the internal implementation of code that copies the content of a
     101 +// canvas. If a non zero offset is provided, all the copied points are offset by
     102 +// this amount.
     103 +// The dstSetCell function is called for every point in this canvas when
     104 +// copying it to the destination.
     105 +func (c *Canvas) copyTo(offset image.Point, dstSetCell setCellFunc) error {
    114 106   for col := range c.buffer {
    115 107   for row := range c.buffer[col] {
    116 108   partial, err := c.buffer.IsPartial(image.Point{col, row})
    skipped 8 lines
    125 117   continue
    126 118   }
    127 119   cell := c.buffer[col][row]
    128  - // The image.Point{0, 0} of this canvas isn't always exactly at
    129  - // image.Point{0, 0} on the terminal.
    130  - // Depends on area assigned by the container.
    131  - offset := c.area.Min
    132 120   p := image.Point{col, row}.Add(offset)
    133  - if err := t.SetCell(p, cell.Rune, cell.Opts); err != nil {
    134  - return fmt.Errorf("terminal.SetCell(%+v) => error: %v", p, err)
     121 + if err := dstSetCell(p, cell.Rune, cell.Opts); err != nil {
     122 + return fmt.Errorf("setCellFunc%v => error: %v", p, err)
    135 123   }
    136 124   }
    137 125   }
    138 126   return nil
    139 127  }
    140 128   
     129 +// Apply applies the canvas to the corresponding area of the terminal.
     130 +// Guarantees to stay within limits of the area the canvas was created with.
     131 +func (c *Canvas) Apply(t terminalapi.Terminal) error {
     132 + termArea, err := area.FromSize(t.Size())
     133 + if err != nil {
     134 + return err
     135 + }
     136 + 
     137 + bufArea, err := area.FromSize(c.buffer.Size())
     138 + if err != nil {
     139 + return err
     140 + }
     141 + 
     142 + if !bufArea.In(termArea) {
     143 + return fmt.Errorf("the canvas area %+v doesn't fit onto the terminal %+v", bufArea, termArea)
     144 + }
     145 + 
     146 + // The image.Point{0, 0} of this canvas isn't always exactly at
     147 + // image.Point{0, 0} on the terminal.
     148 + // Depends on area assigned by the container.
     149 + offset := c.area.Min
     150 + return c.copyTo(offset, t.SetCell)
     151 +}
     152 + 
     153 +// CopyTo copies the content of this canvas onto the destination canvas.
     154 +// This canvas can have an offset when compared to the destination canvas, i.e.
     155 +// the area of this canvas doesn't have to be zero-based.
     156 +func (c *Canvas) CopyTo(dst *Canvas) error {
     157 + if !c.area.In(dst.Area()) {
     158 + return fmt.Errorf("the canvas area %v doesn't fit or lie inside the destination canvas area %v", c.area, dst.Area())
     159 + }
     160 + 
     161 + fn := setCellFunc(func(p image.Point, r rune, opts ...cell.Option) error {
     162 + if _, err := dst.SetCell(p, r, opts...); err != nil {
     163 + return fmt.Errorf("dst.SetCell => %v", err)
     164 + }
     165 + return nil
     166 + })
     167 + 
     168 + // Neither of the two canvases (source and destination) have to be zero
     169 + // based. Canvas is not zero based if it is positioned elsewhere, i.e.
     170 + // providing a smaller view of another canvas.
     171 + // E.g. a widget can assign a smaller portion of its canvas to a component
     172 + // in order to restrict drawing of this component to a smaller area. To do
     173 + // this it can create a sub-canvas. This sub-canvas can have a specific
     174 + // starting position other than image.Point{0, 0} relative to the parent
     175 + // canvas. Copying this sub-canvas back onto the parent accounts for this
     176 + // offset.
     177 + offset := c.area.Min
     178 + return c.copyTo(offset, fn)
     179 +}
     180 + 
  • ■ ■ ■ ■ ■ ■
    canvas/canvas_test.go
    skipped 531 lines
    532 532   }
    533 533  }
    534 534   
     535 +// mustNew creates a new Canvas or panics.
     536 +func mustNew(ar image.Rectangle) *Canvas {
     537 + c, err := New(ar)
     538 + if err != nil {
     539 + panic(err)
     540 + }
     541 + return c
     542 +}
     543 + 
     544 +// mustFill fills the canvas with the specified runes or panics.
     545 +func mustFill(c *Canvas, r rune) {
     546 + ar := c.Area()
     547 + for col := 0; col < ar.Max.X; col++ {
     548 + for row := 0; row < ar.Max.Y; row++ {
     549 + if _, err := c.SetCell(image.Point{col, row}, r); err != nil {
     550 + panic(err)
     551 + }
     552 + }
     553 + }
     554 +}
     555 + 
     556 +// mustSetCell sets cell at the specified point of the canvas or panics.
     557 +func mustSetCell(c *Canvas, p image.Point, r rune, opts ...cell.Option) {
     558 + if _, err := c.SetCell(p, r, opts...); err != nil {
     559 + panic(err)
     560 + }
     561 +}
     562 + 
     563 +func TestCopyTo(t *testing.T) {
     564 + tests := []struct {
     565 + desc string
     566 + src *Canvas
     567 + dst *Canvas
     568 + want *Canvas
     569 + wantErr bool
     570 + }{
     571 + {
     572 + desc: "fails when the canvas doesn't fit",
     573 + src: func() *Canvas {
     574 + c := mustNew(image.Rect(0, 0, 3, 3))
     575 + mustFill(c, 'X')
     576 + return c
     577 + }(),
     578 + dst: mustNew(image.Rect(0, 0, 2, 2)),
     579 + want: mustNew(image.Rect(0, 0, 3, 3)),
     580 + wantErr: true,
     581 + },
     582 + {
     583 + desc: "fails when the area lies outside of the destination canvas",
     584 + src: func() *Canvas {
     585 + c := mustNew(image.Rect(3, 3, 4, 4))
     586 + mustFill(c, 'X')
     587 + return c
     588 + }(),
     589 + dst: mustNew(image.Rect(0, 0, 3, 3)),
     590 + want: mustNew(image.Rect(0, 0, 3, 3)),
     591 + wantErr: true,
     592 + },
     593 + {
     594 + desc: "copies zero based same size canvases",
     595 + src: func() *Canvas {
     596 + c := mustNew(image.Rect(0, 0, 3, 3))
     597 + mustFill(c, 'X')
     598 + return c
     599 + }(),
     600 + dst: mustNew(image.Rect(0, 0, 3, 3)),
     601 + want: func() *Canvas {
     602 + c := mustNew(image.Rect(0, 0, 3, 3))
     603 + mustSetCell(c, image.Point{0, 0}, 'X')
     604 + mustSetCell(c, image.Point{1, 0}, 'X')
     605 + mustSetCell(c, image.Point{2, 0}, 'X')
     606 + 
     607 + mustSetCell(c, image.Point{0, 1}, 'X')
     608 + mustSetCell(c, image.Point{1, 1}, 'X')
     609 + mustSetCell(c, image.Point{2, 1}, 'X')
     610 + 
     611 + mustSetCell(c, image.Point{0, 2}, 'X')
     612 + mustSetCell(c, image.Point{1, 2}, 'X')
     613 + mustSetCell(c, image.Point{2, 2}, 'X')
     614 + return c
     615 + }(),
     616 + },
     617 + {
     618 + desc: "copies smaller canvas with an offset",
     619 + src: func() *Canvas {
     620 + c := mustNew(image.Rect(1, 1, 2, 2))
     621 + mustFill(c, 'X')
     622 + return c
     623 + }(),
     624 + dst: mustNew(image.Rect(0, 0, 3, 3)),
     625 + want: func() *Canvas {
     626 + c := mustNew(image.Rect(0, 0, 3, 3))
     627 + mustSetCell(c, image.Point{1, 1}, 'X')
     628 + return c
     629 + }(),
     630 + },
     631 + {
     632 + desc: "copies smaller canvas with an offset into a canvas with offset from terminal",
     633 + src: func() *Canvas {
     634 + c := mustNew(image.Rect(1, 1, 2, 2))
     635 + mustFill(c, 'X')
     636 + return c
     637 + }(),
     638 + dst: mustNew(image.Rect(3, 3, 6, 6)),
     639 + want: func() *Canvas {
     640 + c := mustNew(image.Rect(3, 3, 6, 6))
     641 + mustSetCell(c, image.Point{1, 1}, 'X')
     642 + return c
     643 + }(),
     644 + },
     645 + {
     646 + desc: "copies cell options",
     647 + src: func() *Canvas {
     648 + c := mustNew(image.Rect(0, 0, 1, 1))
     649 + mustSetCell(c, image.Point{0, 0}, 'X',
     650 + cell.FgColor(cell.ColorRed),
     651 + cell.BgColor(cell.ColorBlue),
     652 + )
     653 + return c
     654 + }(),
     655 + dst: mustNew(image.Rect(0, 0, 3, 1)),
     656 + want: func() *Canvas {
     657 + c := mustNew(image.Rect(0, 0, 3, 1))
     658 + mustSetCell(c, image.Point{0, 0}, 'X',
     659 + cell.FgColor(cell.ColorRed),
     660 + cell.BgColor(cell.ColorBlue),
     661 + )
     662 + return c
     663 + }(),
     664 + },
     665 + {
     666 + desc: "copies cells with full-width runes",
     667 + src: func() *Canvas {
     668 + c := mustNew(image.Rect(0, 0, 3, 3))
     669 + mustSetCell(c, image.Point{0, 0}, '界')
     670 + mustSetCell(c, image.Point{1, 1}, '界')
     671 + return c
     672 + }(),
     673 + dst: mustNew(image.Rect(0, 0, 3, 3)),
     674 + want: func() *Canvas {
     675 + c := mustNew(image.Rect(0, 0, 3, 3))
     676 + mustSetCell(c, image.Point{0, 0}, '界')
     677 + mustSetCell(c, image.Point{1, 1}, '界')
     678 + return c
     679 + }(),
     680 + },
     681 + }
     682 + 
     683 + for _, tc := range tests {
     684 + t.Run(tc.desc, func(t *testing.T) {
     685 + err := tc.src.CopyTo(tc.dst)
     686 + if (err != nil) != tc.wantErr {
     687 + t.Errorf("CopyTo => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     688 + }
     689 + if err != nil {
     690 + return
     691 + }
     692 + 
     693 + ftSize := image.Point{10, 10}
     694 + got, err := faketerm.New(ftSize)
     695 + if err != nil {
     696 + t.Fatalf("faketerm.New(tc.dst.Size()) => unexpected error: %v", err)
     697 + }
     698 + if err := tc.dst.Apply(got); err != nil {
     699 + t.Fatalf("tc.dst.Apply => unexpected error: %v", err)
     700 + }
     701 + 
     702 + want, err := faketerm.New(ftSize)
     703 + if err != nil {
     704 + t.Fatalf("faketerm.New(tc.want.Size()) => unexpected error: %v", err)
     705 + }
     706 + 
     707 + if err := tc.want.Apply(want); err != nil {
     708 + t.Fatalf("tc.want.Apply => unexpected error: %v", err)
     709 + }
     710 + 
     711 + if diff := faketerm.Diff(want, got); diff != "" {
     712 + t.Errorf("CopyTo => %v", diff)
     713 + }
     714 + })
     715 + }
     716 +}
     717 + 
  • ■ ■ ■ ■ ■ ■
    draw/braille_line.go
     1 +// Copyright 2018 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 draw
     16 + 
     17 +// braille_line.go contains code that draws lines on a braille canvas.
     18 + 
     19 +import (
     20 + "fmt"
     21 + "image"
     22 + 
     23 + "github.com/mum4k/termdash/canvas/braille"
     24 + "github.com/mum4k/termdash/cell"
     25 +)
     26 + 
     27 +// BrailleLineOption is used to provide options to BrailleLine().
     28 +type BrailleLineOption interface {
     29 + // set sets the provided option.
     30 + set(*brailleLineOptions)
     31 +}
     32 + 
     33 +// brailleLineOptions stores the provided options.
     34 +type brailleLineOptions struct {
     35 + cellOpts []cell.Option
     36 +}
     37 + 
     38 +// newBrailleLineOptions returns a new brailleLineOptions instance.
     39 +func newBrailleLineOptions() *brailleLineOptions {
     40 + return &brailleLineOptions{}
     41 +}
     42 + 
     43 +// brailleLineOption implements BrailleLineOption.
     44 +type brailleLineOption func(*brailleLineOptions)
     45 + 
     46 +// set implements BrailleLineOption.set.
     47 +func (o brailleLineOption) set(opts *brailleLineOptions) {
     48 + o(opts)
     49 +}
     50 + 
     51 +// BrailleLineCellOpts sets options on the cells that contain the line.
     52 +func BrailleLineCellOpts(cOpts ...cell.Option) BrailleLineOption {
     53 + return brailleLineOption(func(opts *brailleLineOptions) {
     54 + opts.cellOpts = cOpts
     55 + })
     56 +}
     57 + 
     58 +// BrailleLine draws an approximated line segment on the braille canvas between
     59 +// the two provided points.
     60 +// Both start and end must be valid points within the canvas. Start and end can
     61 +// be the same point in which case only one pixel will be set on the braille
     62 +// canvas.
     63 +// The start or end coordinates must not be negative.
     64 +func BrailleLine(bc *braille.Canvas, start, end image.Point, opts ...BrailleLineOption) error {
     65 + if start.X < 0 || start.Y < 0 {
     66 + return fmt.Errorf("the start coordinates cannot be negative, got: %v", start)
     67 + }
     68 + if end.X < 0 || end.Y < 0 {
     69 + return fmt.Errorf("the end coordinates cannot be negative, got: %v", end)
     70 + }
     71 + 
     72 + opt := newBrailleLineOptions()
     73 + for _, o := range opts {
     74 + o.set(opt)
     75 + }
     76 + 
     77 + // Implements Bresenham's line algorithm.
     78 + // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
     79 + 
     80 + vertProj := abs(end.Y - start.Y)
     81 + horizProj := abs(end.X - start.X)
     82 + if vertProj < horizProj {
     83 + if start.X > end.X {
     84 + return lineLow(bc, end.X, end.Y, start.X, start.Y, opt)
     85 + } else {
     86 + return lineLow(bc, start.X, start.Y, end.X, end.Y, opt)
     87 + }
     88 + } else {
     89 + if start.Y > end.Y {
     90 + return lineHigh(bc, end.X, end.Y, start.X, start.Y, opt)
     91 + } else {
     92 + return lineHigh(bc, start.X, start.Y, end.X, end.Y, opt)
     93 + }
     94 + }
     95 +}
     96 + 
     97 +// lineLow draws a line whose horizontal projection (end.X - start.X) is longer
     98 +// than its vertical projection (end.Y - start.Y).
     99 +func lineLow(bc *braille.Canvas, x0, y0, x1, y1 int, opt *brailleLineOptions) error {
     100 + deltaX := x1 - x0
     101 + deltaY := y1 - y0
     102 + 
     103 + stepY := 1
     104 + if deltaY < 0 {
     105 + stepY = -1
     106 + deltaY = -deltaY
     107 + }
     108 + 
     109 + diff := 2*deltaY - deltaX
     110 + y := y0
     111 + for x := x0; x <= x1; x++ {
     112 + p := image.Point{x, y}
     113 + if err := bc.SetPixel(p, opt.cellOpts...); err != nil {
     114 + return fmt.Errorf("lineLow bc.SetPixel(%v) => %v", p, err)
     115 + }
     116 + 
     117 + if diff > 0 {
     118 + y += stepY
     119 + diff -= 2 * deltaX
     120 + }
     121 + diff += 2 * deltaY
     122 + }
     123 + return nil
     124 +}
     125 + 
     126 +// lineHigh draws a line whose vertical projection (end.Y - start.Y) is longer
     127 +// than its horizontal projection (end.X - start.X).
     128 +func lineHigh(bc *braille.Canvas, x0, y0, x1, y1 int, opt *brailleLineOptions) error {
     129 + deltaX := x1 - x0
     130 + deltaY := y1 - y0
     131 + 
     132 + stepX := 1
     133 + if deltaX < 0 {
     134 + stepX = -1
     135 + deltaX = -deltaX
     136 + }
     137 + 
     138 + diff := 2*deltaX - deltaY
     139 + x := x0
     140 + for y := y0; y <= y1; y++ {
     141 + p := image.Point{x, y}
     142 + if err := bc.SetPixel(p, opt.cellOpts...); err != nil {
     143 + return fmt.Errorf("lineHigh bc.SetPixel(%v) => %v", p, err)
     144 + }
     145 + 
     146 + if diff > 0 {
     147 + x += stepX
     148 + diff -= 2 * deltaY
     149 + }
     150 + diff += 2 * deltaX
     151 + }
     152 + return nil
     153 +}
     154 + 
     155 +// abs returns the absolute value of x.
     156 +func abs(x int) int {
     157 + if x < 0 {
     158 + return -x
     159 + }
     160 + return x
     161 +}
     162 + 
  • ■ ■ ■ ■ ■ ■
    draw/braille_line_test.go
     1 +// Copyright 2018 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 draw
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/mum4k/termdash/area"
     22 + "github.com/mum4k/termdash/canvas/braille"
     23 + "github.com/mum4k/termdash/canvas/braille/testbraille"
     24 + "github.com/mum4k/termdash/cell"
     25 + "github.com/mum4k/termdash/terminal/faketerm"
     26 +)
     27 + 
     28 +func TestBrailleLine(t *testing.T) {
     29 + tests := []struct {
     30 + desc string
     31 + canvas image.Rectangle
     32 + start image.Point
     33 + end image.Point
     34 + opts []BrailleLineOption
     35 + want func(size image.Point) *faketerm.Terminal
     36 + wantErr bool
     37 + }{
     38 + {
     39 + desc: "fails when start has negative X",
     40 + canvas: image.Rect(0, 0, 1, 1),
     41 + start: image.Point{-1, 0},
     42 + end: image.Point{0, 0},
     43 + wantErr: true,
     44 + },
     45 + {
     46 + desc: "fails when start has negative Y",
     47 + canvas: image.Rect(0, 0, 1, 1),
     48 + start: image.Point{0, -1},
     49 + end: image.Point{0, 0},
     50 + wantErr: true,
     51 + },
     52 + {
     53 + desc: "fails when end has negative X",
     54 + canvas: image.Rect(0, 0, 1, 1),
     55 + start: image.Point{0, 0},
     56 + end: image.Point{-1, 0},
     57 + wantErr: true,
     58 + },
     59 + {
     60 + desc: "fails when end has negative Y",
     61 + canvas: image.Rect(0, 0, 1, 1),
     62 + start: image.Point{0, 0},
     63 + end: image.Point{0, -1},
     64 + wantErr: true,
     65 + },
     66 + {
     67 + desc: "high line, fails on start point outside of the canvas",
     68 + canvas: image.Rect(0, 0, 1, 1),
     69 + start: image.Point{2, 2},
     70 + end: image.Point{2, 2},
     71 + wantErr: true,
     72 + },
     73 + {
     74 + desc: "low line, fails on end point outside of the canvas",
     75 + canvas: image.Rect(0, 0, 3, 1),
     76 + start: image.Point{0, 0},
     77 + end: image.Point{6, 3},
     78 + wantErr: true,
     79 + },
     80 + {
     81 + desc: "high line, fails on end point outside of the canvas",
     82 + canvas: image.Rect(0, 0, 1, 1),
     83 + start: image.Point{0, 0},
     84 + end: image.Point{2, 2},
     85 + wantErr: true,
     86 + },
     87 + {
     88 + desc: "draws single point",
     89 + canvas: image.Rect(0, 0, 1, 1),
     90 + start: image.Point{0, 0},
     91 + end: image.Point{0, 0},
     92 + want: func(size image.Point) *faketerm.Terminal {
     93 + ft := faketerm.MustNew(size)
     94 + bc := testbraille.MustNew(ft.Area())
     95 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     96 + testbraille.MustApply(bc, ft)
     97 + return ft
     98 + },
     99 + },
     100 + {
     101 + desc: "draws single point with cell options",
     102 + canvas: image.Rect(0, 0, 1, 1),
     103 + start: image.Point{0, 0},
     104 + end: image.Point{0, 0},
     105 + opts: []BrailleLineOption{
     106 + BrailleLineCellOpts(
     107 + cell.FgColor(cell.ColorRed),
     108 + ),
     109 + },
     110 + want: func(size image.Point) *faketerm.Terminal {
     111 + ft := faketerm.MustNew(size)
     112 + bc := testbraille.MustNew(ft.Area())
     113 + testbraille.MustSetPixel(bc, image.Point{0, 0}, cell.FgColor(cell.ColorRed))
     114 + testbraille.MustApply(bc, ft)
     115 + return ft
     116 + },
     117 + },
     118 + {
     119 + desc: "draws high line, octant SE",
     120 + canvas: image.Rect(0, 0, 1, 1),
     121 + start: image.Point{0, 0},
     122 + end: image.Point{1, 3},
     123 + want: func(size image.Point) *faketerm.Terminal {
     124 + ft := faketerm.MustNew(size)
     125 + bc := testbraille.MustNew(ft.Area())
     126 + 
     127 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     128 + testbraille.MustSetPixel(bc, image.Point{0, 1})
     129 + testbraille.MustSetPixel(bc, image.Point{1, 2})
     130 + testbraille.MustSetPixel(bc, image.Point{1, 3})
     131 + 
     132 + testbraille.MustApply(bc, ft)
     133 + return ft
     134 + },
     135 + },
     136 + {
     137 + desc: "draws high line, octant NW",
     138 + canvas: image.Rect(0, 0, 1, 1),
     139 + start: image.Point{1, 3},
     140 + end: image.Point{0, 0},
     141 + want: func(size image.Point) *faketerm.Terminal {
     142 + ft := faketerm.MustNew(size)
     143 + bc := testbraille.MustNew(ft.Area())
     144 + 
     145 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     146 + testbraille.MustSetPixel(bc, image.Point{0, 1})
     147 + testbraille.MustSetPixel(bc, image.Point{1, 2})
     148 + testbraille.MustSetPixel(bc, image.Point{1, 3})
     149 + 
     150 + testbraille.MustApply(bc, ft)
     151 + return ft
     152 + },
     153 + },
     154 + {
     155 + desc: "draws high line, octant SW",
     156 + canvas: image.Rect(0, 0, 1, 1),
     157 + start: image.Point{1, 0},
     158 + end: image.Point{0, 3},
     159 + want: func(size image.Point) *faketerm.Terminal {
     160 + ft := faketerm.MustNew(size)
     161 + bc := testbraille.MustNew(ft.Area())
     162 + 
     163 + testbraille.MustSetPixel(bc, image.Point{1, 0})
     164 + testbraille.MustSetPixel(bc, image.Point{1, 1})
     165 + testbraille.MustSetPixel(bc, image.Point{0, 2})
     166 + testbraille.MustSetPixel(bc, image.Point{0, 3})
     167 + 
     168 + testbraille.MustApply(bc, ft)
     169 + return ft
     170 + },
     171 + },
     172 + {
     173 + desc: "draws high line, octant NE",
     174 + canvas: image.Rect(0, 0, 1, 1),
     175 + start: image.Point{0, 3},
     176 + end: image.Point{1, 0},
     177 + want: func(size image.Point) *faketerm.Terminal {
     178 + ft := faketerm.MustNew(size)
     179 + bc := testbraille.MustNew(ft.Area())
     180 + 
     181 + testbraille.MustSetPixel(bc, image.Point{1, 0})
     182 + testbraille.MustSetPixel(bc, image.Point{1, 1})
     183 + testbraille.MustSetPixel(bc, image.Point{0, 2})
     184 + testbraille.MustSetPixel(bc, image.Point{0, 3})
     185 + 
     186 + testbraille.MustApply(bc, ft)
     187 + return ft
     188 + },
     189 + },
     190 + {
     191 + desc: "draws low line, octant SE",
     192 + canvas: image.Rect(0, 0, 3, 1),
     193 + start: image.Point{0, 0},
     194 + end: image.Point{4, 3},
     195 + want: func(size image.Point) *faketerm.Terminal {
     196 + ft := faketerm.MustNew(size)
     197 + bc := testbraille.MustNew(ft.Area())
     198 + 
     199 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     200 + testbraille.MustSetPixel(bc, image.Point{1, 1})
     201 + testbraille.MustSetPixel(bc, image.Point{2, 1})
     202 + testbraille.MustSetPixel(bc, image.Point{3, 2})
     203 + testbraille.MustSetPixel(bc, image.Point{4, 3})
     204 + 
     205 + testbraille.MustApply(bc, ft)
     206 + return ft
     207 + },
     208 + },
     209 + {
     210 + desc: "draws low line, octant NW",
     211 + canvas: image.Rect(0, 0, 3, 1),
     212 + start: image.Point{4, 3},
     213 + end: image.Point{0, 0},
     214 + want: func(size image.Point) *faketerm.Terminal {
     215 + ft := faketerm.MustNew(size)
     216 + bc := testbraille.MustNew(ft.Area())
     217 + 
     218 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     219 + testbraille.MustSetPixel(bc, image.Point{1, 1})
     220 + testbraille.MustSetPixel(bc, image.Point{2, 1})
     221 + testbraille.MustSetPixel(bc, image.Point{3, 2})
     222 + testbraille.MustSetPixel(bc, image.Point{4, 3})
     223 + 
     224 + testbraille.MustApply(bc, ft)
     225 + return ft
     226 + },
     227 + },
     228 + {
     229 + desc: "draws high line, octant SW",
     230 + canvas: image.Rect(0, 0, 3, 1),
     231 + start: image.Point{4, 0},
     232 + end: image.Point{0, 3},
     233 + want: func(size image.Point) *faketerm.Terminal {
     234 + ft := faketerm.MustNew(size)
     235 + bc := testbraille.MustNew(ft.Area())
     236 + 
     237 + testbraille.MustSetPixel(bc, image.Point{4, 0})
     238 + testbraille.MustSetPixel(bc, image.Point{3, 1})
     239 + testbraille.MustSetPixel(bc, image.Point{2, 2})
     240 + testbraille.MustSetPixel(bc, image.Point{1, 2})
     241 + testbraille.MustSetPixel(bc, image.Point{0, 3})
     242 + 
     243 + testbraille.MustApply(bc, ft)
     244 + return ft
     245 + },
     246 + },
     247 + {
     248 + desc: "draws high line, octant NE",
     249 + canvas: image.Rect(0, 0, 3, 1),
     250 + start: image.Point{0, 3},
     251 + end: image.Point{4, 0},
     252 + want: func(size image.Point) *faketerm.Terminal {
     253 + ft := faketerm.MustNew(size)
     254 + bc := testbraille.MustNew(ft.Area())
     255 + 
     256 + testbraille.MustSetPixel(bc, image.Point{4, 0})
     257 + testbraille.MustSetPixel(bc, image.Point{3, 1})
     258 + testbraille.MustSetPixel(bc, image.Point{2, 2})
     259 + testbraille.MustSetPixel(bc, image.Point{1, 2})
     260 + testbraille.MustSetPixel(bc, image.Point{0, 3})
     261 + 
     262 + testbraille.MustApply(bc, ft)
     263 + return ft
     264 + },
     265 + },
     266 + {
     267 + desc: "draws horizontal line, octant E",
     268 + canvas: image.Rect(0, 0, 1, 1),
     269 + start: image.Point{0, 0},
     270 + end: image.Point{1, 0},
     271 + want: func(size image.Point) *faketerm.Terminal {
     272 + ft := faketerm.MustNew(size)
     273 + bc := testbraille.MustNew(ft.Area())
     274 + 
     275 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     276 + testbraille.MustSetPixel(bc, image.Point{1, 0})
     277 + 
     278 + testbraille.MustApply(bc, ft)
     279 + return ft
     280 + },
     281 + },
     282 + {
     283 + desc: "draws horizontal line, octant W",
     284 + canvas: image.Rect(0, 0, 1, 1),
     285 + start: image.Point{1, 0},
     286 + end: image.Point{0, 0},
     287 + want: func(size image.Point) *faketerm.Terminal {
     288 + ft := faketerm.MustNew(size)
     289 + bc := testbraille.MustNew(ft.Area())
     290 + 
     291 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     292 + testbraille.MustSetPixel(bc, image.Point{1, 0})
     293 + 
     294 + testbraille.MustApply(bc, ft)
     295 + return ft
     296 + },
     297 + },
     298 + {
     299 + desc: "draws vertical line, octant S",
     300 + canvas: image.Rect(0, 0, 1, 1),
     301 + start: image.Point{0, 0},
     302 + end: image.Point{0, 1},
     303 + want: func(size image.Point) *faketerm.Terminal {
     304 + ft := faketerm.MustNew(size)
     305 + bc := testbraille.MustNew(ft.Area())
     306 + 
     307 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     308 + testbraille.MustSetPixel(bc, image.Point{0, 1})
     309 + 
     310 + testbraille.MustApply(bc, ft)
     311 + return ft
     312 + },
     313 + },
     314 + {
     315 + desc: "draws vertical line, octant N",
     316 + canvas: image.Rect(0, 0, 1, 1),
     317 + start: image.Point{0, 1},
     318 + end: image.Point{0, 0},
     319 + want: func(size image.Point) *faketerm.Terminal {
     320 + ft := faketerm.MustNew(size)
     321 + bc := testbraille.MustNew(ft.Area())
     322 + 
     323 + testbraille.MustSetPixel(bc, image.Point{0, 0})
     324 + testbraille.MustSetPixel(bc, image.Point{0, 1})
     325 + 
     326 + testbraille.MustApply(bc, ft)
     327 + return ft
     328 + },
     329 + },
     330 + }
     331 + 
     332 + for _, tc := range tests {
     333 + t.Run(tc.desc, func(t *testing.T) {
     334 + bc, err := braille.New(tc.canvas)
     335 + if err != nil {
     336 + t.Fatalf("braille.New => unexpected error: %v", err)
     337 + }
     338 + 
     339 + err = BrailleLine(bc, tc.start, tc.end, tc.opts...)
     340 + if (err != nil) != tc.wantErr {
     341 + t.Errorf("BrailleLine => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     342 + }
     343 + if err != nil {
     344 + return
     345 + }
     346 + 
     347 + size := area.Size(tc.canvas)
     348 + want := faketerm.MustNew(size)
     349 + if tc.want != nil {
     350 + want = tc.want(size)
     351 + }
     352 + 
     353 + got, err := faketerm.New(size)
     354 + if err != nil {
     355 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     356 + }
     357 + if err := bc.Apply(got); err != nil {
     358 + t.Fatalf("bc.Apply => unexpected error: %v", err)
     359 + }
     360 + if diff := faketerm.Diff(want, got); diff != "" {
     361 + t.Fatalf("BrailleLine => %v", diff)
     362 + }
     363 + 
     364 + })
     365 + }
     366 +}
     367 + 
  • ■ ■ ■ ■ ■ ■
    draw/hv_line.go
     1 +// Copyright 2018 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 draw
     16 + 
     17 +// hv_line.go contains code that draws horizontal and vertical lines.
     18 + 
     19 +import (
     20 + "fmt"
     21 + "image"
     22 + 
     23 + "github.com/mum4k/termdash/canvas"
     24 + "github.com/mum4k/termdash/cell"
     25 +)
     26 + 
     27 +// HVLineOption is used to provide options to HVLine().
     28 +type HVLineOption interface {
     29 + // set sets the provided option.
     30 + set(*hVLineOptions)
     31 +}
     32 + 
     33 +// hVLineOptions stores the provided options.
     34 +type hVLineOptions struct {
     35 + cellOpts []cell.Option
     36 + lineStyle LineStyle
     37 +}
     38 + 
     39 +// newHVLineOptions returns a new hVLineOptions instance.
     40 +func newHVLineOptions() *hVLineOptions {
     41 + return &hVLineOptions{
     42 + lineStyle: DefaultHVLineStyle,
     43 + }
     44 +}
     45 + 
     46 +// hVLineOption implements HVLineOption.
     47 +type hVLineOption func(*hVLineOptions)
     48 + 
     49 +// set implements HVLineOption.set.
     50 +func (o hVLineOption) set(opts *hVLineOptions) {
     51 + o(opts)
     52 +}
     53 + 
     54 +// DefaultHVLineStyle is the default value for the HVLineStyle option.
     55 +const DefaultHVLineStyle = LineStyleLight
     56 + 
     57 +// HVLineStyle sets the style of the line.
     58 +// Defaults to DefaultHVLineStyle.
     59 +func HVLineStyle(ls LineStyle) HVLineOption {
     60 + return hVLineOption(func(opts *hVLineOptions) {
     61 + opts.lineStyle = ls
     62 + })
     63 +}
     64 + 
     65 +// HVLineCellOpts sets options on the cells that contain the line.
     66 +func HVLineCellOpts(cOpts ...cell.Option) HVLineOption {
     67 + return hVLineOption(func(opts *hVLineOptions) {
     68 + opts.cellOpts = cOpts
     69 + })
     70 +}
     71 + 
     72 +// HVLine represents one horizontal or vertical line.
     73 +type HVLine struct {
     74 + // Start is the cell where the line starts.
     75 + Start image.Point
     76 + // End is the cell where the line ends.
     77 + End image.Point
     78 +}
     79 + 
     80 +// HVLines draws horizontal or vertical lines. Handles drawing of the correct
     81 +// characters for locations where any two lines cross (e.g. a corner, a T shape
     82 +// or a cross). Each line must be at least two cells long. Both start and end
     83 +// must be on the same horizontal (same X coordinate) or same vertical (same Y
     84 +// coordinate) line.
     85 +func HVLines(c *canvas.Canvas, lines []HVLine, opts ...HVLineOption) error {
     86 + opt := newHVLineOptions()
     87 + for _, o := range opts {
     88 + o.set(opt)
     89 + }
     90 + 
     91 + g := newHVLineGraph()
     92 + for _, l := range lines {
     93 + line, err := newHVLine(c, l.Start, l.End, opt)
     94 + if err != nil {
     95 + return err
     96 + }
     97 + g.addLine(line)
     98 + 
     99 + switch {
     100 + case line.horizontal():
     101 + for curX := line.start.X; ; curX++ {
     102 + cur := image.Point{curX, line.start.Y}
     103 + if _, err := c.SetCell(cur, line.mainPart, opt.cellOpts...); err != nil {
     104 + return err
     105 + }
     106 + 
     107 + if curX == line.end.X {
     108 + break
     109 + }
     110 + }
     111 + 
     112 + case line.vertical():
     113 + for curY := line.start.Y; ; curY++ {
     114 + cur := image.Point{line.start.X, curY}
     115 + if _, err := c.SetCell(cur, line.mainPart, opt.cellOpts...); err != nil {
     116 + return err
     117 + }
     118 + 
     119 + if curY == line.end.Y {
     120 + break
     121 + }
     122 + }
     123 + }
     124 + }
     125 + 
     126 + for _, n := range g.multiEdgeNodes() {
     127 + r, err := n.rune(opt.lineStyle)
     128 + if err != nil {
     129 + return err
     130 + }
     131 + if _, err := c.SetCell(n.p, r, opt.cellOpts...); err != nil {
     132 + return err
     133 + }
     134 + }
     135 + 
     136 + return nil
     137 +}
     138 + 
     139 +// hVLine represents a line that will be drawn on the canvas.
     140 +type hVLine struct {
     141 + // start is the starting point of the line.
     142 + start image.Point
     143 + 
     144 + // end is the ending point of the line.
     145 + end image.Point
     146 + 
     147 + // mainPart is either parts[vLine] or parts[hLine] depending on whether
     148 + // this is horizontal or vertical line.
     149 + mainPart rune
     150 + 
     151 + // opts are the options provided in a call to HVLine().
     152 + opts *hVLineOptions
     153 +}
     154 + 
     155 +// newHVLine creates a new hVLine instance.
     156 +// Swaps start and end if necessary, so that horizontal drawing is always left
     157 +// to right and vertical is always top down.
     158 +func newHVLine(c *canvas.Canvas, start, end image.Point, opts *hVLineOptions) (*hVLine, error) {
     159 + if ar := c.Area(); !start.In(ar) || !end.In(ar) {
     160 + return nil, fmt.Errorf("both the start%v and the end%v must be in the canvas area: %v", start, end, ar)
     161 + }
     162 + 
     163 + parts, err := lineParts(opts.lineStyle)
     164 + if err != nil {
     165 + return nil, err
     166 + }
     167 + 
     168 + var mainPart rune
     169 + switch {
     170 + case start.X != end.X && start.Y != end.Y:
     171 + return nil, fmt.Errorf("can only draw horizontal (same X coordinates) or vertical (same Y coordinates), got start:%v end:%v", start, end)
     172 + 
     173 + case start.X == end.X && start.Y == end.Y:
     174 + return nil, fmt.Errorf("the line must at least one cell long, got start%v, end%v", start, end)
     175 + 
     176 + case start.X == end.X:
     177 + mainPart = parts[vLine]
     178 + if start.Y > end.Y {
     179 + start, end = end, start
     180 + }
     181 + 
     182 + case start.Y == end.Y:
     183 + mainPart = parts[hLine]
     184 + if start.X > end.X {
     185 + start, end = end, start
     186 + }
     187 + 
     188 + }
     189 + 
     190 + return &hVLine{
     191 + start: start,
     192 + end: end,
     193 + mainPart: mainPart,
     194 + opts: opts,
     195 + }, nil
     196 +}
     197 + 
     198 +// horizontal determines if this is a horizontal line.
     199 +func (hvl *hVLine) horizontal() bool {
     200 + return hvl.mainPart == lineStyleChars[hvl.opts.lineStyle][hLine]
     201 +}
     202 + 
     203 +// vertical determines if this is a vertical line.
     204 +func (hvl *hVLine) vertical() bool {
     205 + return hvl.mainPart == lineStyleChars[hvl.opts.lineStyle][vLine]
     206 +}
     207 + 
  • ■ ■ ■ ■ ■ ■
    draw/hv_line_graph.go
     1 +// Copyright 2018 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 draw
     16 + 
     17 +// hv_line_graph.go helps to keep track of locations where lines cross.
     18 + 
     19 +import (
     20 + "fmt"
     21 + "image"
     22 +)
     23 + 
     24 +// hVLineEdge is an edge between two points on the graph.
     25 +type hVLineEdge struct {
     26 + // from is the starting node of this edge.
     27 + // From is guaranteed to be less than to.
     28 + from image.Point
     29 + 
     30 + // to is the ending point of this edge.
     31 + to image.Point
     32 +}
     33 + 
     34 +// newHVLineEdge returns a new edge between the two points.
     35 +func newHVLineEdge(from, to image.Point) hVLineEdge {
     36 + return hVLineEdge{
     37 + from: from,
     38 + to: to,
     39 + }
     40 +}
     41 + 
     42 +// hVLineNode represents one node in the graph.
     43 +// I.e. one cell.
     44 +type hVLineNode struct {
     45 + // p is the point where this node is.
     46 + p image.Point
     47 + 
     48 + // edges are the edges between this node and the surrounding nodes.
     49 + // The code only supports horizontal and vertical lines so there can only
     50 + // ever be edges to nodes on these planes.
     51 + edges map[hVLineEdge]bool
     52 +}
     53 + 
     54 +// newHVLineNode creates a new newHVLineNode.
     55 +func newHVLineNode(p image.Point) *hVLineNode {
     56 + return &hVLineNode{
     57 + p: p,
     58 + edges: map[hVLineEdge]bool{},
     59 + }
     60 +}
     61 + 
     62 +// hasDown determines if this node has an edge to the one below it.
     63 +func (n *hVLineNode) hasDown() bool {
     64 + target := newHVLineEdge(n.p, image.Point{n.p.X, n.p.Y + 1})
     65 + _, ok := n.edges[target]
     66 + return ok
     67 +}
     68 + 
     69 +// hasUp determines if this node has an edge to the one above it.
     70 +func (n *hVLineNode) hasUp() bool {
     71 + target := newHVLineEdge(image.Point{n.p.X, n.p.Y - 1}, n.p)
     72 + _, ok := n.edges[target]
     73 + return ok
     74 +}
     75 + 
     76 +// hasLeft determines if this node has an edge to the next node on the left.
     77 +func (n *hVLineNode) hasLeft() bool {
     78 + target := newHVLineEdge(image.Point{n.p.X - 1, n.p.Y}, n.p)
     79 + _, ok := n.edges[target]
     80 + return ok
     81 +}
     82 + 
     83 +// hasRight determines if this node has an edge to the next node on the right.
     84 +func (n *hVLineNode) hasRight() bool {
     85 + target := newHVLineEdge(n.p, image.Point{n.p.X + 1, n.p.Y})
     86 + _, ok := n.edges[target]
     87 + return ok
     88 +}
     89 + 
     90 +// rune, given the selected line style returns the correct line character to
     91 +// represent this node.
     92 +// Only handles nodes with two or more edges, as returned by multiEdgeNodes().
     93 +func (n *hVLineNode) rune(ls LineStyle) (rune, error) {
     94 + parts, err := lineParts(ls)
     95 + if err != nil {
     96 + return -1, err
     97 + }
     98 + 
     99 + switch len(n.edges) {
     100 + case 2:
     101 + switch {
     102 + case n.hasLeft() && n.hasRight():
     103 + return parts[hLine], nil
     104 + case n.hasUp() && n.hasDown():
     105 + return parts[vLine], nil
     106 + case n.hasDown() && n.hasRight():
     107 + return parts[topLeftCorner], nil
     108 + case n.hasDown() && n.hasLeft():
     109 + return parts[topRightCorner], nil
     110 + case n.hasUp() && n.hasRight():
     111 + return parts[bottomLeftCorner], nil
     112 + case n.hasUp() && n.hasLeft():
     113 + return parts[bottomRightCorner], nil
     114 + default:
     115 + return -1, fmt.Errorf("unexpected two edges in node representing point %v: %v", n.p, n.edges)
     116 + }
     117 + 
     118 + case 3:
     119 + switch {
     120 + case n.hasUp() && n.hasLeft() && n.hasRight():
     121 + return parts[hAndUp], nil
     122 + case n.hasDown() && n.hasLeft() && n.hasRight():
     123 + return parts[hAndDown], nil
     124 + case n.hasUp() && n.hasDown() && n.hasRight():
     125 + return parts[vAndRight], nil
     126 + case n.hasUp() && n.hasDown() && n.hasLeft():
     127 + return parts[vAndLeft], nil
     128 + 
     129 + default:
     130 + return -1, fmt.Errorf("unexpected three edges in node representing point %v: %v", n.p, n.edges)
     131 + }
     132 + 
     133 + case 4:
     134 + return parts[vAndH], nil
     135 + default:
     136 + return -1, fmt.Errorf("unexpected number of edges(%d) in node representing point %v", len(n.edges), n.p)
     137 + }
     138 +}
     139 + 
     140 +// hVLineGraph represents lines on the canvas as a bidirectional graph of
     141 +// nodes. Helps to determine the characters that should be used where multiple
     142 +// lines cross.
     143 +type hVLineGraph struct {
     144 + nodes map[image.Point]*hVLineNode
     145 +}
     146 + 
     147 +// newHVLineGraph creates a new hVLineGraph.
     148 +func newHVLineGraph() *hVLineGraph {
     149 + return &hVLineGraph{
     150 + nodes: make(map[image.Point]*hVLineNode),
     151 + }
     152 +}
     153 + 
     154 +// getOrCreateNode gets an existing or creates a new node for the point.
     155 +func (g *hVLineGraph) getOrCreateNode(p image.Point) *hVLineNode {
     156 + if n, ok := g.nodes[p]; ok {
     157 + return n
     158 + }
     159 + n := newHVLineNode(p)
     160 + g.nodes[p] = n
     161 + return n
     162 +}
     163 + 
     164 +// addLine adds a line to the graph.
     165 +// This adds edges between all the points on the line.
     166 +func (g *hVLineGraph) addLine(line *hVLine) {
     167 + switch {
     168 + case line.horizontal():
     169 + for curX := line.start.X; curX < line.end.X; curX++ {
     170 + from := image.Point{curX, line.start.Y}
     171 + to := image.Point{curX + 1, line.start.Y}
     172 + n1 := g.getOrCreateNode(from)
     173 + n2 := g.getOrCreateNode(to)
     174 + edge := newHVLineEdge(from, to)
     175 + n1.edges[edge] = true
     176 + n2.edges[edge] = true
     177 + }
     178 + 
     179 + case line.vertical():
     180 + for curY := line.start.Y; curY < line.end.Y; curY++ {
     181 + from := image.Point{line.start.X, curY}
     182 + to := image.Point{line.start.X, curY + 1}
     183 + n1 := g.getOrCreateNode(from)
     184 + n2 := g.getOrCreateNode(to)
     185 + edge := newHVLineEdge(from, to)
     186 + n1.edges[edge] = true
     187 + n2.edges[edge] = true
     188 + }
     189 + }
     190 +}
     191 + 
     192 +// multiEdgeNodes returns all nodes that have more than one edge. These are
     193 +// the nodes where we might need to use different line characters to represent
     194 +// the crossing of multiple lines.
     195 +func (g *hVLineGraph) multiEdgeNodes() []*hVLineNode {
     196 + var nodes []*hVLineNode
     197 + for _, n := range g.nodes {
     198 + if len(n.edges) <= 1 {
     199 + continue
     200 + }
     201 + nodes = append(nodes, n)
     202 + }
     203 + return nodes
     204 +}
     205 + 
  • ■ ■ ■ ■ ■ ■
    draw/hv_line_graph_test.go
     1 +// Copyright 2018 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 draw
     16 + 
     17 +import (
     18 + "image"
     19 + "sort"
     20 + "testing"
     21 + 
     22 + "github.com/kylelemons/godebug/pretty"
     23 + "github.com/mum4k/termdash/canvas"
     24 +)
     25 + 
     26 +func TestMultiEdgeNodes(t *testing.T) {
     27 + tests := []struct {
     28 + desc string
     29 + lines []HVLine
     30 + want []*hVLineNode
     31 + }{
     32 + {
     33 + desc: "no lines added",
     34 + },
     35 + {
     36 + desc: "single-edge nodes only",
     37 + lines: []HVLine{
     38 + {
     39 + Start: image.Point{0, 0},
     40 + End: image.Point{0, 1},
     41 + },
     42 + {
     43 + Start: image.Point{1, 0},
     44 + End: image.Point{1, 1},
     45 + },
     46 + },
     47 + },
     48 + {
     49 + desc: "lines don't cross",
     50 + lines: []HVLine{
     51 + {
     52 + Start: image.Point{0, 0},
     53 + End: image.Point{0, 2},
     54 + },
     55 + {
     56 + Start: image.Point{1, 0},
     57 + End: image.Point{1, 2},
     58 + },
     59 + },
     60 + want: []*hVLineNode{
     61 + {
     62 + p: image.Point{0, 1},
     63 + edges: map[hVLineEdge]bool{
     64 + newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
     65 + newHVLineEdge(image.Point{0, 1}, image.Point{0, 2}): true,
     66 + },
     67 + },
     68 + {
     69 + p: image.Point{1, 1},
     70 + edges: map[hVLineEdge]bool{
     71 + newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
     72 + newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
     73 + },
     74 + },
     75 + },
     76 + },
     77 + {
     78 + desc: "lines cross, node has two edges",
     79 + lines: []HVLine{
     80 + {
     81 + Start: image.Point{0, 0},
     82 + End: image.Point{0, 1},
     83 + },
     84 + {
     85 + Start: image.Point{0, 0},
     86 + End: image.Point{1, 0},
     87 + },
     88 + },
     89 + want: []*hVLineNode{
     90 + {
     91 + p: image.Point{0, 0},
     92 + edges: map[hVLineEdge]bool{
     93 + newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
     94 + newHVLineEdge(image.Point{0, 0}, image.Point{1, 0}): true,
     95 + },
     96 + },
     97 + },
     98 + },
     99 + {
     100 + desc: "lines cross, node has three edges",
     101 + lines: []HVLine{
     102 + {
     103 + Start: image.Point{0, 0},
     104 + End: image.Point{0, 2},
     105 + },
     106 + {
     107 + Start: image.Point{0, 1},
     108 + End: image.Point{1, 1},
     109 + },
     110 + },
     111 + want: []*hVLineNode{
     112 + {
     113 + p: image.Point{0, 1},
     114 + edges: map[hVLineEdge]bool{
     115 + newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
     116 + newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
     117 + newHVLineEdge(image.Point{0, 1}, image.Point{0, 2}): true,
     118 + },
     119 + },
     120 + },
     121 + },
     122 + {
     123 + desc: "lines cross, node has four edges",
     124 + lines: []HVLine{
     125 + {
     126 + Start: image.Point{1, 0},
     127 + End: image.Point{1, 2},
     128 + },
     129 + {
     130 + Start: image.Point{0, 1},
     131 + End: image.Point{2, 1},
     132 + },
     133 + },
     134 + want: []*hVLineNode{
     135 + {
     136 + p: image.Point{1, 1},
     137 + edges: map[hVLineEdge]bool{
     138 + newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
     139 + newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
     140 + newHVLineEdge(image.Point{1, 1}, image.Point{2, 1}): true,
     141 + newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
     142 + },
     143 + },
     144 + },
     145 + },
     146 + }
     147 + 
     148 + for _, tc := range tests {
     149 + t.Run(tc.desc, func(t *testing.T) {
     150 + c, err := canvas.New(image.Rect(0, 0, 3, 3))
     151 + if err != nil {
     152 + t.Fatalf("canvas.New => unexpected error: %v", err)
     153 + }
     154 + 
     155 + g := newHVLineGraph()
     156 + for i, l := range tc.lines {
     157 + line, err := newHVLine(c, l.Start, l.End, newHVLineOptions())
     158 + if err != nil {
     159 + t.Fatalf("newHVLine[%d] => unexpected error: %v", i, err)
     160 + }
     161 + g.addLine(line)
     162 + }
     163 + 
     164 + got := g.multiEdgeNodes()
     165 + 
     166 + lessFn := func(i, j int) bool {
     167 + return got[i].p.X < got[j].p.X || got[i].p.Y < got[j].p.Y
     168 + }
     169 + sort.Slice(got, lessFn)
     170 + sort.Slice(tc.want, lessFn)
     171 + if diff := pretty.Compare(tc.want, got); diff != "" {
     172 + t.Errorf("multiEdgeNodes => unexpected diff (-want, +got):\n%s", diff)
     173 + }
     174 + })
     175 + }
     176 + 
     177 +}
     178 + 
     179 +func TestNodeRune(t *testing.T) {
     180 + tests := []struct {
     181 + desc string
     182 + node *hVLineNode
     183 + ls LineStyle
     184 + want rune
     185 + wantErr bool
     186 + }{
     187 + {
     188 + desc: "fails on node with no edges",
     189 + node: &hVLineNode{},
     190 + wantErr: true,
     191 + },
     192 + {
     193 + desc: "fails on unsupported two edge combination",
     194 + node: &hVLineNode{
     195 + edges: map[hVLineEdge]bool{
     196 + newHVLineEdge(image.Point{0, 0}, image.Point{1, 1}): true,
     197 + newHVLineEdge(image.Point{1, 1}, image.Point{2, 2}): true,
     198 + },
     199 + },
     200 + ls: LineStyleLight,
     201 + wantErr: true,
     202 + },
     203 + {
     204 + desc: "fails on unsupported three edge combination",
     205 + node: &hVLineNode{
     206 + edges: map[hVLineEdge]bool{
     207 + newHVLineEdge(image.Point{0, 0}, image.Point{1, 1}): true,
     208 + newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
     209 + newHVLineEdge(image.Point{1, 1}, image.Point{2, 2}): true,
     210 + },
     211 + },
     212 + ls: LineStyleLight,
     213 + wantErr: true,
     214 + },
     215 + {
     216 + desc: "fails on unsupported line style",
     217 + node: &hVLineNode{},
     218 + ls: LineStyle(-1),
     219 + wantErr: true,
     220 + },
     221 + {
     222 + desc: "horizontal line",
     223 + node: &hVLineNode{
     224 + p: image.Point{1, 1},
     225 + edges: map[hVLineEdge]bool{
     226 + newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
     227 + newHVLineEdge(image.Point{1, 1}, image.Point{2, 1}): true,
     228 + },
     229 + },
     230 + ls: LineStyleLight,
     231 + want: lineStyleChars[LineStyleLight][hLine],
     232 + },
     233 + {
     234 + desc: "vertical line",
     235 + node: &hVLineNode{
     236 + p: image.Point{1, 1},
     237 + edges: map[hVLineEdge]bool{
     238 + newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
     239 + newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
     240 + },
     241 + },
     242 + ls: LineStyleLight,
     243 + want: lineStyleChars[LineStyleLight][vLine],
     244 + },
     245 + {
     246 + desc: "top left corner",
     247 + node: &hVLineNode{
     248 + p: image.Point{0, 0},
     249 + edges: map[hVLineEdge]bool{
     250 + newHVLineEdge(image.Point{0, 0}, image.Point{1, 0}): true,
     251 + newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
     252 + },
     253 + },
     254 + ls: LineStyleLight,
     255 + want: lineStyleChars[LineStyleLight][topLeftCorner],
     256 + },
     257 + {
     258 + desc: "top right corner",
     259 + node: &hVLineNode{
     260 + p: image.Point{2, 0},
     261 + edges: map[hVLineEdge]bool{
     262 + newHVLineEdge(image.Point{1, 0}, image.Point{2, 0}): true,
     263 + newHVLineEdge(image.Point{2, 0}, image.Point{2, 1}): true,
     264 + },
     265 + },
     266 + ls: LineStyleLight,
     267 + want: lineStyleChars[LineStyleLight][topRightCorner],
     268 + },
     269 + {
     270 + desc: "bottom left corner",
     271 + node: &hVLineNode{
     272 + p: image.Point{0, 2},
     273 + edges: map[hVLineEdge]bool{
     274 + newHVLineEdge(image.Point{0, 1}, image.Point{0, 2}): true,
     275 + newHVLineEdge(image.Point{0, 2}, image.Point{1, 2}): true,
     276 + },
     277 + },
     278 + ls: LineStyleLight,
     279 + want: lineStyleChars[LineStyleLight][bottomLeftCorner],
     280 + },
     281 + {
     282 + desc: "bottom right corner",
     283 + node: &hVLineNode{
     284 + p: image.Point{2, 2},
     285 + edges: map[hVLineEdge]bool{
     286 + newHVLineEdge(image.Point{1, 2}, image.Point{2, 2}): true,
     287 + newHVLineEdge(image.Point{2, 1}, image.Point{2, 2}): true,
     288 + },
     289 + },
     290 + ls: LineStyleLight,
     291 + want: lineStyleChars[LineStyleLight][bottomRightCorner],
     292 + },
     293 + {
     294 + desc: "T horizontal and up",
     295 + node: &hVLineNode{
     296 + p: image.Point{1, 2},
     297 + edges: map[hVLineEdge]bool{
     298 + newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
     299 + newHVLineEdge(image.Point{0, 2}, image.Point{1, 2}): true,
     300 + newHVLineEdge(image.Point{1, 2}, image.Point{2, 2}): true,
     301 + },
     302 + },
     303 + ls: LineStyleLight,
     304 + want: lineStyleChars[LineStyleLight][hAndUp],
     305 + },
     306 + {
     307 + desc: "T horizontal and down",
     308 + node: &hVLineNode{
     309 + p: image.Point{1, 0},
     310 + edges: map[hVLineEdge]bool{
     311 + newHVLineEdge(image.Point{0, 0}, image.Point{1, 0}): true,
     312 + newHVLineEdge(image.Point{1, 0}, image.Point{2, 0}): true,
     313 + newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
     314 + },
     315 + },
     316 + ls: LineStyleLight,
     317 + want: lineStyleChars[LineStyleLight][hAndDown],
     318 + },
     319 + {
     320 + desc: "T vertical and right",
     321 + node: &hVLineNode{
     322 + p: image.Point{0, 1},
     323 + edges: map[hVLineEdge]bool{
     324 + newHVLineEdge(image.Point{0, 0}, image.Point{0, 1}): true,
     325 + newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
     326 + newHVLineEdge(image.Point{0, 1}, image.Point{0, 2}): true,
     327 + },
     328 + },
     329 + ls: LineStyleLight,
     330 + want: lineStyleChars[LineStyleLight][vAndRight],
     331 + },
     332 + {
     333 + desc: "T vertical and left",
     334 + node: &hVLineNode{
     335 + p: image.Point{2, 1},
     336 + edges: map[hVLineEdge]bool{
     337 + newHVLineEdge(image.Point{2, 0}, image.Point{2, 1}): true,
     338 + newHVLineEdge(image.Point{1, 1}, image.Point{2, 1}): true,
     339 + newHVLineEdge(image.Point{2, 1}, image.Point{2, 2}): true,
     340 + },
     341 + },
     342 + ls: LineStyleLight,
     343 + want: lineStyleChars[LineStyleLight][vAndLeft],
     344 + },
     345 + {
     346 + desc: "cross",
     347 + node: &hVLineNode{
     348 + p: image.Point{1, 1},
     349 + edges: map[hVLineEdge]bool{
     350 + newHVLineEdge(image.Point{1, 0}, image.Point{1, 1}): true,
     351 + newHVLineEdge(image.Point{0, 1}, image.Point{1, 1}): true,
     352 + newHVLineEdge(image.Point{1, 1}, image.Point{2, 1}): true,
     353 + newHVLineEdge(image.Point{1, 1}, image.Point{1, 2}): true,
     354 + },
     355 + },
     356 + ls: LineStyleLight,
     357 + want: lineStyleChars[LineStyleLight][vAndH],
     358 + },
     359 + }
     360 + 
     361 + for _, tc := range tests {
     362 + t.Run(tc.desc, func(t *testing.T) {
     363 + got, err := tc.node.rune(tc.ls)
     364 + if (err != nil) != tc.wantErr {
     365 + t.Errorf("rune => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     366 + }
     367 + if err != nil {
     368 + return
     369 + }
     370 + if got != tc.want {
     371 + t.Errorf("rune => got %c, want %c", got, tc.want)
     372 + }
     373 + })
     374 + }
     375 +}
     376 + 
  • ■ ■ ■ ■ ■ ■
    draw/hv_line_test.go
     1 +// Copyright 2018 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 draw
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/mum4k/termdash/canvas"
     22 + "github.com/mum4k/termdash/canvas/testcanvas"
     23 + "github.com/mum4k/termdash/cell"
     24 + "github.com/mum4k/termdash/terminal/faketerm"
     25 +)
     26 + 
     27 +func TestHVLines(t *testing.T) {
     28 + tests := []struct {
     29 + desc string
     30 + canvas image.Rectangle // Size of the canvas for the test.
     31 + lines []HVLine
     32 + opts []HVLineOption
     33 + want func(size image.Point) *faketerm.Terminal
     34 + wantErr bool
     35 + }{
     36 + {
     37 + desc: "fails when line isn't horizontal or vertical",
     38 + canvas: image.Rect(0, 0, 2, 2),
     39 + lines: []HVLine{
     40 + {
     41 + Start: image.Point{0, 0},
     42 + End: image.Point{1, 1},
     43 + },
     44 + },
     45 + want: func(size image.Point) *faketerm.Terminal {
     46 + return faketerm.MustNew(size)
     47 + },
     48 + wantErr: true,
     49 + },
     50 + {
     51 + desc: "fails when start isn't in the canvas",
     52 + canvas: image.Rect(0, 0, 1, 1),
     53 + lines: []HVLine{
     54 + {
     55 + Start: image.Point{2, 0},
     56 + End: image.Point{0, 0},
     57 + },
     58 + },
     59 + want: func(size image.Point) *faketerm.Terminal {
     60 + return faketerm.MustNew(size)
     61 + },
     62 + wantErr: true,
     63 + },
     64 + {
     65 + desc: "fails when end isn't in the canvas",
     66 + canvas: image.Rect(0, 0, 1, 1),
     67 + lines: []HVLine{
     68 + {
     69 + Start: image.Point{0, 0},
     70 + End: image.Point{0, 2},
     71 + },
     72 + },
     73 + want: func(size image.Point) *faketerm.Terminal {
     74 + return faketerm.MustNew(size)
     75 + },
     76 + wantErr: true,
     77 + },
     78 + {
     79 + desc: "fails when the line has zero length",
     80 + canvas: image.Rect(0, 0, 1, 1),
     81 + lines: []HVLine{
     82 + {
     83 + Start: image.Point{0, 0},
     84 + End: image.Point{0, 0},
     85 + },
     86 + },
     87 + want: func(size image.Point) *faketerm.Terminal {
     88 + return faketerm.MustNew(size)
     89 + },
     90 + wantErr: true,
     91 + },
     92 + {
     93 + desc: "draws single horizontal line",
     94 + canvas: image.Rect(0, 0, 3, 1),
     95 + lines: []HVLine{
     96 + {
     97 + Start: image.Point{0, 0},
     98 + End: image.Point{2, 0},
     99 + },
     100 + },
     101 + want: func(size image.Point) *faketerm.Terminal {
     102 + ft := faketerm.MustNew(size)
     103 + c := testcanvas.MustNew(ft.Area())
     104 + 
     105 + parts := lineStyleChars[LineStyleLight]
     106 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine])
     107 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine])
     108 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine])
     109 + 
     110 + testcanvas.MustApply(c, ft)
     111 + return ft
     112 + },
     113 + },
     114 + {
     115 + desc: "respects line style set explicitly",
     116 + canvas: image.Rect(0, 0, 3, 1),
     117 + lines: []HVLine{
     118 + {
     119 + Start: image.Point{0, 0},
     120 + End: image.Point{2, 0},
     121 + },
     122 + },
     123 + opts: []HVLineOption{
     124 + HVLineStyle(LineStyleLight),
     125 + },
     126 + want: func(size image.Point) *faketerm.Terminal {
     127 + ft := faketerm.MustNew(size)
     128 + c := testcanvas.MustNew(ft.Area())
     129 + 
     130 + parts := lineStyleChars[LineStyleLight]
     131 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine])
     132 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine])
     133 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine])
     134 + 
     135 + testcanvas.MustApply(c, ft)
     136 + return ft
     137 + },
     138 + },
     139 + {
     140 + desc: "respects cell options",
     141 + canvas: image.Rect(0, 0, 3, 1),
     142 + lines: []HVLine{
     143 + {
     144 + Start: image.Point{0, 0},
     145 + End: image.Point{2, 0},
     146 + },
     147 + },
     148 + opts: []HVLineOption{
     149 + HVLineCellOpts(
     150 + cell.FgColor(cell.ColorYellow),
     151 + cell.BgColor(cell.ColorBlue),
     152 + ),
     153 + },
     154 + want: func(size image.Point) *faketerm.Terminal {
     155 + ft := faketerm.MustNew(size)
     156 + c := testcanvas.MustNew(ft.Area())
     157 + 
     158 + parts := lineStyleChars[LineStyleLight]
     159 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine],
     160 + cell.FgColor(cell.ColorYellow),
     161 + cell.BgColor(cell.ColorBlue),
     162 + )
     163 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine],
     164 + cell.FgColor(cell.ColorYellow),
     165 + cell.BgColor(cell.ColorBlue),
     166 + )
     167 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine],
     168 + cell.FgColor(cell.ColorYellow),
     169 + cell.BgColor(cell.ColorBlue),
     170 + )
     171 + 
     172 + testcanvas.MustApply(c, ft)
     173 + return ft
     174 + },
     175 + },
     176 + {
     177 + desc: "draws single horizontal line, supplied in reverse direction",
     178 + canvas: image.Rect(0, 0, 3, 1),
     179 + lines: []HVLine{
     180 + {
     181 + Start: image.Point{1, 0},
     182 + End: image.Point{0, 0},
     183 + },
     184 + },
     185 + want: func(size image.Point) *faketerm.Terminal {
     186 + ft := faketerm.MustNew(size)
     187 + c := testcanvas.MustNew(ft.Area())
     188 + 
     189 + parts := lineStyleChars[LineStyleLight]
     190 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine])
     191 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine])
     192 + 
     193 + testcanvas.MustApply(c, ft)
     194 + return ft
     195 + },
     196 + },
     197 + {
     198 + desc: "draws single vertical line",
     199 + canvas: image.Rect(0, 0, 3, 3),
     200 + lines: []HVLine{
     201 + {
     202 + Start: image.Point{1, 0},
     203 + End: image.Point{1, 2},
     204 + },
     205 + },
     206 + want: func(size image.Point) *faketerm.Terminal {
     207 + ft := faketerm.MustNew(size)
     208 + c := testcanvas.MustNew(ft.Area())
     209 + 
     210 + parts := lineStyleChars[LineStyleLight]
     211 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine])
     212 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine])
     213 + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[vLine])
     214 + 
     215 + testcanvas.MustApply(c, ft)
     216 + return ft
     217 + },
     218 + },
     219 + {
     220 + desc: "draws single vertical line, supplied in reverse direction",
     221 + canvas: image.Rect(0, 0, 3, 3),
     222 + lines: []HVLine{
     223 + {
     224 + Start: image.Point{1, 1},
     225 + End: image.Point{1, 0},
     226 + },
     227 + },
     228 + want: func(size image.Point) *faketerm.Terminal {
     229 + ft := faketerm.MustNew(size)
     230 + c := testcanvas.MustNew(ft.Area())
     231 + 
     232 + parts := lineStyleChars[LineStyleLight]
     233 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine])
     234 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine])
     235 + 
     236 + testcanvas.MustApply(c, ft)
     237 + return ft
     238 + },
     239 + },
     240 + {
     241 + desc: "parallel horizontal lines don't affect each other",
     242 + canvas: image.Rect(0, 0, 3, 3),
     243 + lines: []HVLine{
     244 + {
     245 + Start: image.Point{0, 0},
     246 + End: image.Point{2, 0},
     247 + },
     248 + {
     249 + Start: image.Point{0, 1},
     250 + End: image.Point{2, 1},
     251 + },
     252 + },
     253 + want: func(size image.Point) *faketerm.Terminal {
     254 + ft := faketerm.MustNew(size)
     255 + c := testcanvas.MustNew(ft.Area())
     256 + 
     257 + parts := lineStyleChars[LineStyleLight]
     258 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine])
     259 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine])
     260 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine])
     261 + 
     262 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[hLine])
     263 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[hLine])
     264 + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[hLine])
     265 + 
     266 + testcanvas.MustApply(c, ft)
     267 + return ft
     268 + },
     269 + },
     270 + {
     271 + desc: "parallel vertical lines don't affect each other",
     272 + canvas: image.Rect(0, 0, 3, 3),
     273 + lines: []HVLine{
     274 + {
     275 + Start: image.Point{0, 0},
     276 + End: image.Point{0, 2},
     277 + },
     278 + {
     279 + Start: image.Point{1, 0},
     280 + End: image.Point{1, 2},
     281 + },
     282 + },
     283 + want: func(size image.Point) *faketerm.Terminal {
     284 + ft := faketerm.MustNew(size)
     285 + c := testcanvas.MustNew(ft.Area())
     286 + 
     287 + parts := lineStyleChars[LineStyleLight]
     288 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[vLine])
     289 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vLine])
     290 + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[vLine])
     291 + 
     292 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine])
     293 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine])
     294 + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[vLine])
     295 + 
     296 + testcanvas.MustApply(c, ft)
     297 + return ft
     298 + },
     299 + },
     300 + {
     301 + desc: "perpendicular lines that don't cross don't affect each other",
     302 + canvas: image.Rect(0, 0, 3, 3),
     303 + lines: []HVLine{
     304 + {
     305 + Start: image.Point{0, 0},
     306 + End: image.Point{0, 2},
     307 + },
     308 + {
     309 + Start: image.Point{1, 1},
     310 + End: image.Point{2, 1},
     311 + },
     312 + },
     313 + want: func(size image.Point) *faketerm.Terminal {
     314 + ft := faketerm.MustNew(size)
     315 + c := testcanvas.MustNew(ft.Area())
     316 + 
     317 + parts := lineStyleChars[LineStyleLight]
     318 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[vLine])
     319 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vLine])
     320 + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[vLine])
     321 + 
     322 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[hLine])
     323 + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[hLine])
     324 + 
     325 + testcanvas.MustApply(c, ft)
     326 + return ft
     327 + },
     328 + },
     329 + {
     330 + desc: "draws top left corner",
     331 + canvas: image.Rect(0, 0, 3, 3),
     332 + lines: []HVLine{
     333 + {
     334 + Start: image.Point{0, 0},
     335 + End: image.Point{0, 2},
     336 + },
     337 + {
     338 + Start: image.Point{0, 0},
     339 + End: image.Point{2, 0},
     340 + },
     341 + },
     342 + want: func(size image.Point) *faketerm.Terminal {
     343 + ft := faketerm.MustNew(size)
     344 + c := testcanvas.MustNew(ft.Area())
     345 + 
     346 + parts := lineStyleChars[LineStyleLight]
     347 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[topLeftCorner])
     348 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vLine])
     349 + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[vLine])
     350 + 
     351 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine])
     352 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine])
     353 + 
     354 + testcanvas.MustApply(c, ft)
     355 + return ft
     356 + },
     357 + },
     358 + {
     359 + desc: "draws top right corner",
     360 + canvas: image.Rect(0, 0, 3, 3),
     361 + lines: []HVLine{
     362 + {
     363 + Start: image.Point{2, 0},
     364 + End: image.Point{2, 2},
     365 + },
     366 + {
     367 + Start: image.Point{0, 0},
     368 + End: image.Point{2, 0},
     369 + },
     370 + },
     371 + want: func(size image.Point) *faketerm.Terminal {
     372 + ft := faketerm.MustNew(size)
     373 + c := testcanvas.MustNew(ft.Area())
     374 + 
     375 + parts := lineStyleChars[LineStyleLight]
     376 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[topRightCorner])
     377 + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[vLine])
     378 + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[vLine])
     379 + 
     380 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine])
     381 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hLine])
     382 + 
     383 + testcanvas.MustApply(c, ft)
     384 + return ft
     385 + },
     386 + },
     387 + {
     388 + desc: "draws bottom left corner",
     389 + canvas: image.Rect(0, 0, 3, 3),
     390 + lines: []HVLine{
     391 + {
     392 + Start: image.Point{0, 0},
     393 + End: image.Point{0, 2},
     394 + },
     395 + {
     396 + Start: image.Point{0, 2},
     397 + End: image.Point{2, 2},
     398 + },
     399 + },
     400 + want: func(size image.Point) *faketerm.Terminal {
     401 + ft := faketerm.MustNew(size)
     402 + c := testcanvas.MustNew(ft.Area())
     403 + 
     404 + parts := lineStyleChars[LineStyleLight]
     405 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[vLine])
     406 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vLine])
     407 + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[bottomLeftCorner])
     408 + 
     409 + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[hLine])
     410 + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[hLine])
     411 + 
     412 + testcanvas.MustApply(c, ft)
     413 + return ft
     414 + },
     415 + },
     416 + {
     417 + desc: "draws bottom right corner",
     418 + canvas: image.Rect(0, 0, 3, 3),
     419 + lines: []HVLine{
     420 + {
     421 + Start: image.Point{2, 0},
     422 + End: image.Point{2, 2},
     423 + },
     424 + {
     425 + Start: image.Point{0, 2},
     426 + End: image.Point{2, 2},
     427 + },
     428 + },
     429 + want: func(size image.Point) *faketerm.Terminal {
     430 + ft := faketerm.MustNew(size)
     431 + c := testcanvas.MustNew(ft.Area())
     432 + 
     433 + parts := lineStyleChars[LineStyleLight]
     434 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[vLine])
     435 + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[vLine])
     436 + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[bottomRightCorner])
     437 + 
     438 + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[hLine])
     439 + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[hLine])
     440 + 
     441 + testcanvas.MustApply(c, ft)
     442 + return ft
     443 + },
     444 + },
     445 + {
     446 + desc: "draws T horizontal and up",
     447 + canvas: image.Rect(0, 0, 3, 3),
     448 + lines: []HVLine{
     449 + {
     450 + Start: image.Point{0, 2},
     451 + End: image.Point{2, 2},
     452 + },
     453 + {
     454 + Start: image.Point{1, 0},
     455 + End: image.Point{1, 2},
     456 + },
     457 + },
     458 + want: func(size image.Point) *faketerm.Terminal {
     459 + ft := faketerm.MustNew(size)
     460 + c := testcanvas.MustNew(ft.Area())
     461 + 
     462 + parts := lineStyleChars[LineStyleLight]
     463 + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[hLine])
     464 + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[hAndUp])
     465 + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[hLine])
     466 + 
     467 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine])
     468 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine])
     469 + 
     470 + testcanvas.MustApply(c, ft)
     471 + return ft
     472 + },
     473 + },
     474 + {
     475 + desc: "draws T horizontal and down",
     476 + canvas: image.Rect(0, 0, 3, 3),
     477 + lines: []HVLine{
     478 + {
     479 + Start: image.Point{0, 0},
     480 + End: image.Point{2, 0},
     481 + },
     482 + {
     483 + Start: image.Point{1, 0},
     484 + End: image.Point{1, 2},
     485 + },
     486 + },
     487 + want: func(size image.Point) *faketerm.Terminal {
     488 + ft := faketerm.MustNew(size)
     489 + c := testcanvas.MustNew(ft.Area())
     490 + 
     491 + parts := lineStyleChars[LineStyleLight]
     492 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[hLine])
     493 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hAndDown])
     494 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[hLine])
     495 + 
     496 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vLine])
     497 + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[vLine])
     498 + 
     499 + testcanvas.MustApply(c, ft)
     500 + return ft
     501 + },
     502 + },
     503 + {
     504 + desc: "draws T vertical and left",
     505 + canvas: image.Rect(0, 0, 3, 3),
     506 + lines: []HVLine{
     507 + {
     508 + Start: image.Point{0, 1},
     509 + End: image.Point{2, 1},
     510 + },
     511 + {
     512 + Start: image.Point{2, 0},
     513 + End: image.Point{2, 2},
     514 + },
     515 + },
     516 + want: func(size image.Point) *faketerm.Terminal {
     517 + ft := faketerm.MustNew(size)
     518 + c := testcanvas.MustNew(ft.Area())
     519 + 
     520 + parts := lineStyleChars[LineStyleLight]
     521 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[hLine])
     522 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[hLine])
     523 + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[vAndLeft])
     524 + 
     525 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[vLine])
     526 + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[vLine])
     527 + 
     528 + testcanvas.MustApply(c, ft)
     529 + return ft
     530 + },
     531 + },
     532 + {
     533 + desc: "draws T vertical and right",
     534 + canvas: image.Rect(0, 0, 3, 3),
     535 + lines: []HVLine{
     536 + {
     537 + Start: image.Point{0, 1},
     538 + End: image.Point{2, 1},
     539 + },
     540 + {
     541 + Start: image.Point{0, 0},
     542 + End: image.Point{0, 2},
     543 + },
     544 + },
     545 + want: func(size image.Point) *faketerm.Terminal {
     546 + ft := faketerm.MustNew(size)
     547 + c := testcanvas.MustNew(ft.Area())
     548 + 
     549 + parts := lineStyleChars[LineStyleLight]
     550 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vAndRight])
     551 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[hLine])
     552 + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[hLine])
     553 + 
     554 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[vLine])
     555 + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[vLine])
     556 + 
     557 + testcanvas.MustApply(c, ft)
     558 + return ft
     559 + },
     560 + },
     561 + {
     562 + desc: "draws a cross",
     563 + canvas: image.Rect(0, 0, 3, 3),
     564 + lines: []HVLine{
     565 + {
     566 + Start: image.Point{0, 1},
     567 + End: image.Point{2, 1},
     568 + },
     569 + {
     570 + Start: image.Point{1, 0},
     571 + End: image.Point{1, 2},
     572 + },
     573 + },
     574 + want: func(size image.Point) *faketerm.Terminal {
     575 + ft := faketerm.MustNew(size)
     576 + c := testcanvas.MustNew(ft.Area())
     577 + 
     578 + parts := lineStyleChars[LineStyleLight]
     579 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[hLine])
     580 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vAndH])
     581 + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[hLine])
     582 + 
     583 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[vLine])
     584 + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[vLine])
     585 + 
     586 + testcanvas.MustApply(c, ft)
     587 + return ft
     588 + },
     589 + },
     590 + {
     591 + desc: "draws multiple crossings",
     592 + canvas: image.Rect(0, 0, 3, 3),
     593 + lines: []HVLine{
     594 + // Three horizontal lines.
     595 + {
     596 + Start: image.Point{0, 0},
     597 + End: image.Point{2, 0},
     598 + },
     599 + {
     600 + Start: image.Point{0, 1},
     601 + End: image.Point{2, 1},
     602 + },
     603 + {
     604 + Start: image.Point{0, 2},
     605 + End: image.Point{2, 2},
     606 + },
     607 + // Three vertical lines.
     608 + {
     609 + Start: image.Point{0, 0},
     610 + End: image.Point{0, 2},
     611 + },
     612 + {
     613 + Start: image.Point{1, 0},
     614 + End: image.Point{1, 2},
     615 + },
     616 + {
     617 + Start: image.Point{2, 0},
     618 + End: image.Point{2, 2},
     619 + },
     620 + },
     621 + want: func(size image.Point) *faketerm.Terminal {
     622 + ft := faketerm.MustNew(size)
     623 + c := testcanvas.MustNew(ft.Area())
     624 + 
     625 + parts := lineStyleChars[LineStyleLight]
     626 + testcanvas.MustSetCell(c, image.Point{0, 0}, parts[topLeftCorner])
     627 + testcanvas.MustSetCell(c, image.Point{1, 0}, parts[hAndDown])
     628 + testcanvas.MustSetCell(c, image.Point{2, 0}, parts[topRightCorner])
     629 + 
     630 + testcanvas.MustSetCell(c, image.Point{0, 1}, parts[vAndRight])
     631 + testcanvas.MustSetCell(c, image.Point{1, 1}, parts[vAndH])
     632 + testcanvas.MustSetCell(c, image.Point{2, 1}, parts[vAndLeft])
     633 + 
     634 + testcanvas.MustSetCell(c, image.Point{0, 2}, parts[bottomLeftCorner])
     635 + testcanvas.MustSetCell(c, image.Point{1, 2}, parts[hAndUp])
     636 + testcanvas.MustSetCell(c, image.Point{2, 2}, parts[bottomRightCorner])
     637 + 
     638 + testcanvas.MustApply(c, ft)
     639 + return ft
     640 + },
     641 + },
     642 + }
     643 + 
     644 + for _, tc := range tests {
     645 + t.Run(tc.desc, func(t *testing.T) {
     646 + c, err := canvas.New(tc.canvas)
     647 + if err != nil {
     648 + t.Fatalf("canvas.New => unexpected error: %v", err)
     649 + }
     650 + 
     651 + err = HVLines(c, tc.lines, tc.opts...)
     652 + if (err != nil) != tc.wantErr {
     653 + t.Errorf("HVLines => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     654 + }
     655 + if err != nil {
     656 + return
     657 + }
     658 + 
     659 + got, err := faketerm.New(c.Size())
     660 + if err != nil {
     661 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     662 + }
     663 + 
     664 + if err := c.Apply(got); err != nil {
     665 + t.Fatalf("Apply => unexpected error: %v", err)
     666 + }
     667 + 
     668 + if diff := faketerm.Diff(tc.want(c.Size()), got); diff != "" {
     669 + t.Errorf("HVLines => %v", diff)
     670 + }
     671 + })
     672 + }
     673 +}
     674 + 
  • ■ ■ ■ ■ ■ ■
    draw/line_style.go
    skipped 13 lines
    14 14   
    15 15  package draw
    16 16   
    17  -import "fmt"
     17 +import (
     18 + "fmt"
     19 + 
     20 + runewidth "github.com/mattn/go-runewidth"
     21 +)
    18 22   
    19 23  // line_style.go contains the Unicode characters used for drawing lines of
    20 24  // different styles.
    21 25   
    22 26  // lineStyleChars maps the line styles to the corresponding component characters.
     27 +// Source: http://en.wikipedia.org/wiki/Box-drawing_character.
    23 28  var lineStyleChars = map[LineStyle]map[linePart]rune{
    24 29   LineStyleLight: {
    25 30   hLine: '─',
    skipped 2 lines
    28 33   topRightCorner: '┐',
    29 34   bottomLeftCorner: '└',
    30 35   bottomRightCorner: '┘',
     36 + hAndUp: '┴',
     37 + hAndDown: '┬',
     38 + vAndLeft: '┤',
     39 + vAndRight: '├',
     40 + vAndH: '┼',
    31 41   },
    32 42  }
    33 43   
     44 +// init verifies that all line parts are half-width runes (occupy only one
     45 +// cell).
     46 +func init() {
     47 + for ls, parts := range lineStyleChars {
     48 + for part, r := range parts {
     49 + if got := runewidth.RuneWidth(r); got > 1 {
     50 + panic(fmt.Errorf("line style %v line part %v is a rune %c with width %v, all parts must be half-width runes (width of one)", ls, part, r, got))
     51 + }
     52 + }
     53 + }
     54 +}
     55 + 
    34 56  // lineParts returns the line component characters for the provided line style.
    35 57  func lineParts(ls LineStyle) (map[linePart]rune, error) {
    36 58   parts, ok := lineStyleChars[ls]
    37 59   if !ok {
    38  - return nil, fmt.Errorf("unsupported line style %v", ls)
     60 + return nil, fmt.Errorf("unsupported line style %d", ls)
    39 61   }
    40 62   return parts, nil
    41 63  }
    skipped 38 lines
    80 102   topRightCorner: "linePartTopRightCorner",
    81 103   bottomLeftCorner: "linePartBottomLeftCorner",
    82 104   bottomRightCorner: "linePartBottomRightCorner",
     105 + hAndUp: "linePartHAndUp",
     106 + hAndDown: "linePartHAndDown",
     107 + vAndLeft: "linePartVAndLeft",
     108 + vAndRight: "linePartVAndRight",
     109 + vAndH: "linePartVAndH",
    83 110  }
    84 111   
    85 112  const (
    skipped 3 lines
    89 116   topRightCorner
    90 117   bottomLeftCorner
    91 118   bottomRightCorner
     119 + hAndUp
     120 + hAndDown
     121 + vAndLeft
     122 + vAndRight
     123 + vAndH
    92 124  )
    93 125   
  • ■ ■ ■ ■ ■ ■
    draw/testdraw/testdraw.go
    skipped 19 lines
    20 20   "image"
    21 21   
    22 22   "github.com/mum4k/termdash/canvas"
     23 + "github.com/mum4k/termdash/canvas/braille"
    23 24   "github.com/mum4k/termdash/draw"
    24 25  )
    25 26   
    skipped 18 lines
    44 45   }
    45 46  }
    46 47   
     48 +// MustHVLines draws the vertical / horizontal lines or panics.
     49 +func MustHVLines(c *canvas.Canvas, lines []draw.HVLine, opts ...draw.HVLineOption) {
     50 + if err := draw.HVLines(c, lines, opts...); err != nil {
     51 + panic(fmt.Sprintf("draw.HVLines => unexpected error: %v", err))
     52 + }
     53 +}
     54 + 
     55 +// MustBrailleLine draws the braille line or panics.
     56 +func MustBrailleLine(bc *braille.Canvas, start, end image.Point, opts ...draw.BrailleLineOption) {
     57 + if err := draw.BrailleLine(bc, start, end, opts...); err != nil {
     58 + panic(fmt.Sprintf("draw.BrailleLine => unexpected error: %v", err))
     59 + }
     60 +}
     61 + 
  • images/linechartdemo.gif
  • ■ ■ ■ ■ ■ ■
    numbers/numbers.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 numbers implements various numerical functions.
     16 +package numbers
     17 + 
     18 +import (
     19 + "math"
     20 +)
     21 + 
     22 +// RoundToNonZeroPlaces rounds the float up, so that it has at least the provided
     23 +// number of non-zero decimal places.
     24 +// Returns the rounded float and the number of leading decimal places that
     25 +// are zero. Returns the original float when places is zero. Negative places
     26 +// are treated as positive, so that -2 == 2.
     27 +func RoundToNonZeroPlaces(f float64, places int) (float64, int) {
     28 + if f == 0 {
     29 + return 0, 0
     30 + }
     31 + 
     32 + decOnly := zeroBeforeDecimal(f)
     33 + if decOnly == 0 {
     34 + return f, 0
     35 + }
     36 + nzMult := multToNonZero(decOnly)
     37 + if places == 0 {
     38 + return f, multToPlaces(nzMult)
     39 + }
     40 + plMult := placesToMult(places)
     41 + 
     42 + m := float64(nzMult * plMult)
     43 + return math.Ceil(f*m) / m, multToPlaces(nzMult)
     44 +}
     45 + 
     46 +// multToNonZero returns multiplier for the float, so that the first decimal
     47 +// place is non-zero. The float must not be zero.
     48 +func multToNonZero(f float64) int {
     49 + v := f
     50 + if v < 0 {
     51 + v *= -1
     52 + }
     53 + 
     54 + mult := 1
     55 + for v < 0.1 {
     56 + v *= 10
     57 + mult *= 10
     58 + }
     59 + return mult
     60 +}
     61 + 
     62 +// placesToMult translates the number of decimal places to a multiple of 10.
     63 +func placesToMult(places int) int {
     64 + if places < 0 {
     65 + places *= -1
     66 + }
     67 + 
     68 + mult := 1
     69 + for i := 0; i < places; i++ {
     70 + mult *= 10
     71 + }
     72 + return mult
     73 +}
     74 + 
     75 +// multToPlaces translates the multiple of 10 to a number of decimal places.
     76 +func multToPlaces(mult int) int {
     77 + places := 0
     78 + for mult > 1 {
     79 + mult /= 10
     80 + places++
     81 + }
     82 + return places
     83 +}
     84 + 
     85 +// zeroBeforeDecimal modifies the float so that it only has zero value before
     86 +// the decimal point.
     87 +func zeroBeforeDecimal(f float64) float64 {
     88 + var sign float64 = 1
     89 + if f < 0 {
     90 + f *= -1
     91 + sign = -1
     92 + }
     93 + 
     94 + floor := math.Floor(f)
     95 + return (f - floor) * sign
     96 +}
     97 + 
     98 +// Round returns the nearest integer, rounding half away from zero.
     99 +// Copied from the math package of Go 1.10 for backwards compatibility with Go
     100 +// 1.8 where the math.Round function doesn't exist yet.
     101 +func Round(x float64) float64 {
     102 + t := math.Trunc(x)
     103 + if math.Abs(x-t) >= 0.5 {
     104 + return t + math.Copysign(1, x)
     105 + }
     106 + return t
     107 +}
     108 + 
     109 +// MinMax returns the smallest and the largest value among the provided values.
     110 +// Returns (0, 0) if there are no values.
     111 +func MinMax(values []float64) (min, max float64) {
     112 + if len(values) == 0 {
     113 + return 0, 0
     114 + }
     115 + min = math.MaxFloat64
     116 + max = -1 * math.MaxFloat64
     117 + 
     118 + for _, v := range values {
     119 + if v < min {
     120 + min = v
     121 + }
     122 + if v > max {
     123 + max = v
     124 + }
     125 + }
     126 + return min, max
     127 +}
     128 + 
  • ■ ■ ■ ■ ■ ■
    numbers/numbers_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 numbers
     16 + 
     17 +import (
     18 + "fmt"
     19 + "math"
     20 + "testing"
     21 +)
     22 + 
     23 +func TestRoundToNonZeroPlaces(t *testing.T) {
     24 + tests := []struct {
     25 + float float64
     26 + places int
     27 + wantFloat float64
     28 + wantPlaces int
     29 + }{
     30 + {0, 0, 0, 0},
     31 + {1.1, 0, 1.1, 0},
     32 + {-1, 1, -1, 0},
     33 + {1, 1, 1, 0},
     34 + {1, 10, 1, 0},
     35 + {1, -1, 1, 0},
     36 + {0.12345, 2, 0.13, 0},
     37 + {0.12345, -2, 0.13, 0},
     38 + {0.12345, 10, 0.12345, 0},
     39 + {0.00012345, 2, 0.00013, 3},
     40 + {0.00012345, 3, 0.000124, 3},
     41 + {0.00012345, 10, 0.00012345, 3},
     42 + {-0.00012345, 10, -0.00012345, 3},
     43 + {1.234567, 2, 1.24, 0},
     44 + {-1.234567, 2, -1.23, 0},
     45 + {1099.0000234567, 3, 1099.0000235, 4},
     46 + {-1099.0000234567, 3, -1099.0000234, 4},
     47 + }
     48 + 
     49 + for _, tc := range tests {
     50 + t.Run(fmt.Sprintf("%v_%v", tc.float, tc.places), func(t *testing.T) {
     51 + gotFloat, gotPlaces := RoundToNonZeroPlaces(tc.float, tc.places)
     52 + if gotFloat != tc.wantFloat || gotPlaces != tc.wantPlaces {
     53 + t.Errorf("RoundToNonZeroPlaces(%v, %d) => (%v, %v), want (%v, %v)", tc.float, tc.places, gotFloat, gotPlaces, tc.wantFloat, tc.wantPlaces)
     54 + }
     55 + })
     56 + }
     57 +}
     58 + 
     59 +func TestZeroBeforeDecimal(t *testing.T) {
     60 + tests := []struct {
     61 + float float64
     62 + want float64
     63 + }{
     64 + {0, 0},
     65 + {-1, 0},
     66 + {1, 0},
     67 + {1.0, 0},
     68 + {1.123, 0.123},
     69 + {-1.123, -0.123},
     70 + }
     71 + 
     72 + for _, tc := range tests {
     73 + t.Run(fmt.Sprint(tc.float), func(t *testing.T) {
     74 + got := zeroBeforeDecimal(tc.float)
     75 + if got != tc.want {
     76 + t.Errorf("zeroBeforeDecimal(%v) => %v, want %v", tc.float, got, tc.want)
     77 + 
     78 + }
     79 + })
     80 + }
     81 +}
     82 + 
     83 +// Copied from the math package of Go 1.10 for backwards compatibility with Go
     84 +// 1.8 where the math.Round function doesn't exist yet.
     85 + 
     86 +func alike(a, b float64) bool {
     87 + switch {
     88 + case math.IsNaN(a) && math.IsNaN(b):
     89 + return true
     90 + case a == b:
     91 + return math.Signbit(a) == math.Signbit(b)
     92 + }
     93 + return false
     94 +}
     95 + 
     96 +var round = []float64{
     97 + 5,
     98 + 8,
     99 + math.Copysign(0, -1),
     100 + -5,
     101 + 10,
     102 + 3,
     103 + 5,
     104 + 3,
     105 + 2,
     106 + -9,
     107 +}
     108 + 
     109 +var vf = []float64{
     110 + 4.9790119248836735e+00,
     111 + 7.7388724745781045e+00,
     112 + -2.7688005719200159e-01,
     113 + -5.0106036182710749e+00,
     114 + 9.6362937071984173e+00,
     115 + 2.9263772392439646e+00,
     116 + 5.2290834314593066e+00,
     117 + 2.7279399104360102e+00,
     118 + 1.8253080916808550e+00,
     119 + -8.6859247685756013e+00,
     120 +}
     121 + 
     122 +var vfroundSC = [][2]float64{
     123 + {0, 0},
     124 + {1.390671161567e-309, 0}, // denormal
     125 + {0.49999999999999994, 0}, // 0.5-epsilon
     126 + {0.5, 1},
     127 + {0.5000000000000001, 1}, // 0.5+epsilon
     128 + {-1.5, -2},
     129 + {-2.5, -3},
     130 + {math.NaN(), math.NaN()},
     131 + {math.Inf(1), math.Inf(1)},
     132 + {2251799813685249.5, 2251799813685250}, // 1 bit fraction
     133 + {2251799813685250.5, 2251799813685251},
     134 + {4503599627370495.5, 4503599627370496}, // 1 bit fraction, rounding to 0 bit fraction
     135 + {4503599627370497, 4503599627370497}, // large integer
     136 +}
     137 + 
     138 +func TestRound(t *testing.T) {
     139 + for i := 0; i < len(vf); i++ {
     140 + if f := Round(vf[i]); !alike(round[i], f) {
     141 + t.Errorf("Round(%g) = %g, want %g", vf[i], f, round[i])
     142 + }
     143 + }
     144 + for i := 0; i < len(vfroundSC); i++ {
     145 + if f := Round(vfroundSC[i][0]); !alike(vfroundSC[i][1], f) {
     146 + t.Errorf("Round(%g) = %g, want %g", vfroundSC[i][0], f, vfroundSC[i][1])
     147 + }
     148 + }
     149 +}
     150 + 
     151 +func TestMinMax(t *testing.T) {
     152 + tests := []struct {
     153 + desc string
     154 + values []float64
     155 + wantMin float64
     156 + wantMax float64
     157 + }{
     158 + {
     159 + desc: "no values",
     160 + },
     161 + {
     162 + desc: "all values the same",
     163 + values: []float64{1.1, 1.1},
     164 + wantMin: 1.1,
     165 + wantMax: 1.1,
     166 + },
     167 + {
     168 + desc: "all values the same and negative",
     169 + values: []float64{-1.1, -1.1},
     170 + wantMin: -1.1,
     171 + wantMax: -1.1,
     172 + },
     173 + {
     174 + desc: "min and max among positive values",
     175 + values: []float64{1.1, 1.2, 1.3},
     176 + wantMin: 1.1,
     177 + wantMax: 1.3,
     178 + },
     179 + {
     180 + desc: "min and max among positive and zero values",
     181 + values: []float64{1.1, 0, 1.3},
     182 + wantMin: 0,
     183 + wantMax: 1.3,
     184 + },
     185 + {
     186 + desc: "min and max among negative, positive and zero values",
     187 + values: []float64{1.1, 0, 1.3, -11.3, 22.5},
     188 + wantMin: -11.3,
     189 + wantMax: 22.5,
     190 + },
     191 + }
     192 + 
     193 + for _, tc := range tests {
     194 + t.Run(tc.desc, func(t *testing.T) {
     195 + gotMin, gotMax := MinMax(tc.values)
     196 + if gotMin != tc.wantMin || gotMax != tc.wantMax {
     197 + t.Errorf("MinMax => (%v, %v), want (%v, %v)", gotMin, gotMax, tc.wantMin, tc.wantMax)
     198 + }
     199 + })
     200 + }
     201 +}
     202 + 
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/axes/axes.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 axes calculates the required layout and draws the X and Y axes of a line chart.
     16 +package axes
     17 + 
     18 +import (
     19 + "fmt"
     20 + "image"
     21 +)
     22 + 
     23 +const (
     24 + // nonZeroDecimals determines the overall precision of values displayed on the
     25 + // graph, it indicates the number of non-zero decimal places the values will be
     26 + // rounded up to.
     27 + nonZeroDecimals = 2
     28 + 
     29 + // yAxisWidth is width of the Y axis.
     30 + yAxisWidth = 1
     31 +)
     32 + 
     33 +// YDetails contain information about the Y axis that will be drawn onto the
     34 +// canvas.
     35 +type YDetails struct {
     36 + // Width in character cells of the Y axis and its character labels.
     37 + Width int
     38 + 
     39 + // Start is the point where the Y axis starts.
     40 + // Both coordinates of Start are less than End.
     41 + Start image.Point
     42 + // End is the point where the Y axis ends.
     43 + End image.Point
     44 + 
     45 + // Scale is the scale of the Y axis.
     46 + Scale *YScale
     47 + 
     48 + // Labels are the labels for values on the Y axis in an increasing order.
     49 + Labels []*Label
     50 +}
     51 + 
     52 +// Y tracks the state of the Y axis throughout the lifetime of a line chart.
     53 +// Implements lazy resize of the axis to decrease visual "jumping".
     54 +// This object is not thread-safe.
     55 +type Y struct {
     56 + // min is the smallest value on the Y axis.
     57 + min *Value
     58 + // max is the largest value on the Y axis.
     59 + max *Value
     60 + // details about the Y axis as it will be drawn.
     61 + details *YDetails
     62 +}
     63 + 
     64 +// NewY returns a new Y instance.
     65 +// The minVal and maxVal represent the minimum and maximum value that will be
     66 +// displayed on the line chart among all of the series.
     67 +func NewY(minVal, maxVal float64) *Y {
     68 + y := &Y{}
     69 + y.Update(minVal, maxVal)
     70 + return y
     71 +}
     72 + 
     73 +// Update updates the stored minVal and maxVal.
     74 +func (y *Y) Update(minVal, maxVal float64) {
     75 + y.min, y.max = NewValue(minVal, nonZeroDecimals), NewValue(maxVal, nonZeroDecimals)
     76 +}
     77 + 
     78 +// RequiredWidth calculates the minimum width required in order to draw the Y axis.
     79 +func (y *Y) RequiredWidth() int {
     80 + // This is an estimation only, it is possible that more labels in the
     81 + // middle will be generated and might be wider than this. Such cases are
     82 + // handled on the call to Details when the size of canvas is known.
     83 + return widestLabel([]*Label{
     84 + {Value: y.min},
     85 + {Value: y.max},
     86 + }) + yAxisWidth
     87 +}
     88 + 
     89 +// Details retrieves details about the Y axis required to draw it on a canvas
     90 +// of the provided area.
     91 +func (y *Y) Details(cvsAr image.Rectangle) (*YDetails, error) {
     92 + cvsWidth := cvsAr.Dx()
     93 + cvsHeight := cvsAr.Dy()
     94 + maxWidth := cvsWidth - 1 // Reserve one row for the line chart itself.
     95 + if req := y.RequiredWidth(); maxWidth < req {
     96 + return nil, fmt.Errorf("the received maxWidth %d is smaller than the reported required width %d", maxWidth, req)
     97 + }
     98 + 
     99 + graphHeight := cvsHeight - 2 // One row for the X axis and one for its labels.
     100 + scale, err := NewYScale(y.min.Value, y.max.Value, graphHeight, nonZeroDecimals)
     101 + if err != nil {
     102 + return nil, err
     103 + }
     104 + 
     105 + // See how the labels would look like on the entire maxWidth.
     106 + maxLabelWidth := maxWidth - yAxisWidth
     107 + labels, err := yLabels(scale, maxLabelWidth)
     108 + if err != nil {
     109 + return nil, err
     110 + }
     111 + 
     112 + var width int
     113 + // Determine the largest label, which might be less than maxWidth.
     114 + // Such case would allow us to save more space for the line chart itself.
     115 + widest := widestLabel(labels)
     116 + if widest < maxLabelWidth {
     117 + // Save the space and recalculate the labels, since they need to be realigned.
     118 + l, err := yLabels(scale, widest)
     119 + if err != nil {
     120 + return nil, err
     121 + }
     122 + labels = l
     123 + width = widest + yAxisWidth // One for the axis itself.
     124 + } else {
     125 + width = maxWidth
     126 + }
     127 + 
     128 + return &YDetails{
     129 + Width: width,
     130 + Start: image.Point{width - 1, 0},
     131 + End: image.Point{width - 1, graphHeight},
     132 + Scale: scale,
     133 + Labels: labels,
     134 + }, nil
     135 +}
     136 + 
     137 +// widestLabel returns the width of the widest label.
     138 +func widestLabel(labels []*Label) int {
     139 + var widest int
     140 + for _, label := range labels {
     141 + if l := len(label.Value.Text()); l > widest {
     142 + widest = l
     143 + }
     144 + }
     145 + return widest
     146 +}
     147 + 
     148 +// XDetails contain information about the X axis that will be drawn onto the
     149 +// canvas.
     150 +type XDetails struct {
     151 + // Start is the point where the X axis starts.
     152 + // Both coordinates of Start are less than End.
     153 + Start image.Point
     154 + // End is the point where the X axis ends.
     155 + End image.Point
     156 + 
     157 + // Scale is the scale of the X axis.
     158 + Scale *XScale
     159 + 
     160 + // Labels are the labels for values on the X axis in an increasing order.
     161 + Labels []*Label
     162 +}
     163 + 
     164 +// NewXDetails retrieves details about the X axis required to draw it on a canvas
     165 +// of the provided area. The yStart is the point where the Y axis starts.
     166 +// The numPoints is the number of points in the largest series that will be
     167 +// plotted.
     168 +// customLabels are the desired labels for the X axis, these are preferred if
     169 +// provided.
     170 +func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle, customLabels map[int]string) (*XDetails, error) {
     171 + if min := 3; cvsAr.Dy() < min {
     172 + return nil, fmt.Errorf("the canvas isn't tall enough to accommodate the X axis, its labels and the line chart, got height %d, minimum is %d", cvsAr.Dy(), min)
     173 + }
     174 + 
     175 + // The space between the start of the axis and the end of the canvas.
     176 + graphWidth := cvsAr.Dx() - yStart.X - 1
     177 + scale, err := NewXScale(numPoints, graphWidth, nonZeroDecimals)
     178 + if err != nil {
     179 + return nil, err
     180 + }
     181 + 
     182 + // One point horizontally for the Y axis.
     183 + // Two points vertically, one for the X axis and one for its labels.
     184 + graphZero := image.Point{yStart.X + 1, cvsAr.Dy() - 3}
     185 + labels, err := xLabels(scale, graphZero, customLabels)
     186 + if err != nil {
     187 + return nil, err
     188 + }
     189 + return &XDetails{
     190 + Start: image.Point{yStart.X, cvsAr.Dy() - 2}, // One row for the labels.
     191 + End: image.Point{yStart.X + graphWidth, cvsAr.Dy() - 2},
     192 + Scale: scale,
     193 + Labels: labels,
     194 + }, nil
     195 +}
     196 + 
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/axes/axes_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 axes
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 +)
     23 + 
     24 +type updateY struct {
     25 + minVal float64
     26 + maxVal float64
     27 +}
     28 + 
     29 +func TestY(t *testing.T) {
     30 + tests := []struct {
     31 + desc string
     32 + minVal float64
     33 + maxVal float64
     34 + update *updateY
     35 + cvsAr image.Rectangle
     36 + wantWidth int
     37 + want *YDetails
     38 + wantErr bool
     39 + }{
     40 + {
     41 + desc: "fails on canvas too small",
     42 + minVal: 0,
     43 + maxVal: 3,
     44 + cvsAr: image.Rect(0, 0, 3, 2),
     45 + wantWidth: 2,
     46 + wantErr: true,
     47 + },
     48 + {
     49 + desc: "fails on cvsWidth less than required width",
     50 + minVal: 0,
     51 + maxVal: 3,
     52 + cvsAr: image.Rect(0, 0, 2, 4),
     53 + wantWidth: 2,
     54 + wantErr: true,
     55 + },
     56 + {
     57 + desc: "fails when max is less than min",
     58 + minVal: 0,
     59 + maxVal: -1,
     60 + cvsAr: image.Rect(0, 0, 4, 4),
     61 + wantWidth: 3,
     62 + wantErr: true,
     63 + },
     64 + {
     65 + desc: "cvsWidth equals required width",
     66 + minVal: 0,
     67 + maxVal: 3,
     68 + cvsAr: image.Rect(0, 0, 3, 4),
     69 + wantWidth: 2,
     70 + want: &YDetails{
     71 + Width: 2,
     72 + Start: image.Point{1, 0},
     73 + End: image.Point{1, 2},
     74 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
     75 + Labels: []*Label{
     76 + {NewValue(0, nonZeroDecimals), image.Point{0, 1}},
     77 + {NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
     78 + },
     79 + },
     80 + },
     81 + {
     82 + desc: "cvsWidth just accommodates the longest label",
     83 + minVal: 0,
     84 + maxVal: 3,
     85 + cvsAr: image.Rect(0, 0, 6, 4),
     86 + wantWidth: 2,
     87 + want: &YDetails{
     88 + Width: 5,
     89 + Start: image.Point{4, 0},
     90 + End: image.Point{4, 2},
     91 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
     92 + Labels: []*Label{
     93 + {NewValue(0, nonZeroDecimals), image.Point{3, 1}},
     94 + {NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
     95 + },
     96 + },
     97 + },
     98 + {
     99 + desc: "cvsWidth is more than we need",
     100 + minVal: 0,
     101 + maxVal: 3,
     102 + cvsAr: image.Rect(0, 0, 7, 4),
     103 + wantWidth: 2,
     104 + want: &YDetails{
     105 + Width: 5,
     106 + Start: image.Point{4, 0},
     107 + End: image.Point{4, 2},
     108 + Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
     109 + Labels: []*Label{
     110 + {NewValue(0, nonZeroDecimals), image.Point{3, 1}},
     111 + {NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
     112 + },
     113 + },
     114 + },
     115 + }
     116 + 
     117 + for _, tc := range tests {
     118 + t.Run(tc.desc, func(t *testing.T) {
     119 + y := NewY(tc.minVal, tc.maxVal)
     120 + if tc.update != nil {
     121 + y.Update(tc.update.minVal, tc.update.maxVal)
     122 + }
     123 + 
     124 + gotWidth := y.RequiredWidth()
     125 + if gotWidth != tc.wantWidth {
     126 + t.Errorf("RequiredWidth => got %v, want %v", gotWidth, tc.wantWidth)
     127 + }
     128 + 
     129 + got, err := y.Details(tc.cvsAr)
     130 + if (err != nil) != tc.wantErr {
     131 + t.Errorf("Details => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     132 + }
     133 + if err != nil {
     134 + return
     135 + }
     136 + if diff := pretty.Compare(tc.want, got); diff != "" {
     137 + t.Errorf("Details => unexpected diff (-want, +got):\n%s", diff)
     138 + }
     139 + })
     140 + }
     141 +}
     142 + 
     143 +func TestNewXDetails(t *testing.T) {
     144 + tests := []struct {
     145 + desc string
     146 + numPoints int
     147 + yStart image.Point
     148 + cvsWidth int
     149 + cvsAr image.Rectangle
     150 + customLabels map[int]string
     151 + want *XDetails
     152 + wantErr bool
     153 + }{
     154 + {
     155 + desc: "fails when numPoints is negative",
     156 + numPoints: -1,
     157 + yStart: image.Point{0, 0},
     158 + cvsAr: image.Rect(0, 0, 2, 3),
     159 + wantErr: true,
     160 + },
     161 + {
     162 + desc: "fails when cvsAr isn't wide enough",
     163 + numPoints: 1,
     164 + yStart: image.Point{0, 0},
     165 + cvsAr: image.Rect(0, 0, 1, 3),
     166 + wantErr: true,
     167 + },
     168 + {
     169 + desc: "fails when cvsAr isn't tall enough",
     170 + numPoints: 1,
     171 + yStart: image.Point{0, 0},
     172 + cvsAr: image.Rect(0, 0, 3, 2),
     173 + wantErr: true,
     174 + },
     175 + {
     176 + desc: "works with no data points",
     177 + numPoints: 0,
     178 + yStart: image.Point{0, 0},
     179 + cvsAr: image.Rect(0, 0, 2, 3),
     180 + want: &XDetails{
     181 + Start: image.Point{0, 1},
     182 + End: image.Point{1, 1},
     183 + Scale: mustNewXScale(0, 1, nonZeroDecimals),
     184 + Labels: []*Label{
     185 + {
     186 + Value: NewValue(0, nonZeroDecimals),
     187 + Pos: image.Point{1, 2},
     188 + },
     189 + },
     190 + },
     191 + },
     192 + {
     193 + desc: "accounts for non-zero yStart",
     194 + numPoints: 0,
     195 + yStart: image.Point{2, 0},
     196 + cvsAr: image.Rect(0, 0, 4, 5),
     197 + want: &XDetails{
     198 + Start: image.Point{2, 3},
     199 + End: image.Point{3, 3},
     200 + Scale: mustNewXScale(0, 1, nonZeroDecimals),
     201 + Labels: []*Label{
     202 + {
     203 + Value: NewValue(0, nonZeroDecimals),
     204 + Pos: image.Point{3, 4},
     205 + },
     206 + },
     207 + },
     208 + },
     209 + }
     210 + 
     211 + for _, tc := range tests {
     212 + t.Run(tc.desc, func(t *testing.T) {
     213 + got, err := NewXDetails(tc.numPoints, tc.yStart, tc.cvsAr, tc.customLabels)
     214 + if (err != nil) != tc.wantErr {
     215 + t.Errorf("NewXDetails => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     216 + }
     217 + if err != nil {
     218 + return
     219 + }
     220 + 
     221 + if diff := pretty.Compare(tc.want, got); diff != "" {
     222 + t.Errorf("NewXDetails => unexpected diff (-want, +got):\n%s", diff)
     223 + }
     224 + })
     225 + }
     226 +}
     227 + 
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/axes/label.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 axes
     16 + 
     17 +// label.go contains code that calculates the positions of labels on the axes.
     18 + 
     19 +import (
     20 + "fmt"
     21 + "image"
     22 + 
     23 + "github.com/mum4k/termdash/align"
     24 +)
     25 + 
     26 +// Label is one value label on an axis.
     27 +type Label struct {
     28 + // Value if the value to be displayed.
     29 + Value *Value
     30 + 
     31 + // Position of the label within the canvas.
     32 + Pos image.Point
     33 +}
     34 + 
     35 +// yLabels returns labels that should be placed next to the Y axis.
     36 +// The labelWidth is the width of the area from the left-most side of the
     37 +// canvas until the Y axis (not including the Y axis). This is the area where
     38 +// the labels will be placed and aligned.
     39 +// Labels are returned in an increasing value order.
     40 +// Label value is not trimmed to the provided labelWidth, the label width is
     41 +// only used to align the labels. Alignment is done with the assumption that
     42 +// longer labels will be trimmed.
     43 +func yLabels(scale *YScale, labelWidth int) ([]*Label, error) {
     44 + if min := 2; scale.GraphHeight < min {
     45 + return nil, fmt.Errorf("cannot place labels on a canvas with height %d, minimum is %d", scale.GraphHeight, min)
     46 + }
     47 + if min := 1; labelWidth < min {
     48 + return nil, fmt.Errorf("cannot place labels in label area width %d, minimum is %d", labelWidth, min)
     49 + }
     50 + 
     51 + var labels []*Label
     52 + const labelSpacing = 4
     53 + seen := map[string]bool{}
     54 + for y := scale.GraphHeight - 1; y >= 0; y -= labelSpacing {
     55 + label, err := rowLabel(scale, y, labelWidth)
     56 + if err != nil {
     57 + return nil, err
     58 + }
     59 + if !seen[label.Value.Text()] {
     60 + labels = append(labels, label)
     61 + seen[label.Value.Text()] = true
     62 + }
     63 + }
     64 + 
     65 + // If we have data, place at least two labels, first and last.
     66 + haveData := scale.Min.Rounded != 0 || scale.Max.Rounded != 0
     67 + if len(labels) < 2 && haveData {
     68 + const maxRow = 0
     69 + label, err := rowLabel(scale, maxRow, labelWidth)
     70 + if err != nil {
     71 + return nil, err
     72 + }
     73 + labels = append(labels, label)
     74 + }
     75 + return labels, nil
     76 +}
     77 + 
     78 +// rowLabelArea determines the area available for labels on the specified row.
     79 +// The row is the Y coordinate of the row, Y coordinates grow down.
     80 +func rowLabelArea(row int, labelWidth int) image.Rectangle {
     81 + return image.Rect(0, row, labelWidth, row+1)
     82 +}
     83 + 
     84 +// rowLabel returns label for the specified row.
     85 +func rowLabel(scale *YScale, y int, labelWidth int) (*Label, error) {
     86 + v, err := scale.CellLabel(y)
     87 + if err != nil {
     88 + return nil, fmt.Errorf("unable to determine label value for row %d: %v", y, err)
     89 + }
     90 + 
     91 + ar := rowLabelArea(y, labelWidth)
     92 + pos, err := align.Text(ar, v.Text(), align.HorizontalRight, align.VerticalMiddle)
     93 + return &Label{
     94 + Value: v,
     95 + Pos: pos,
     96 + }, nil
     97 +}
     98 + 
     99 +// xSpace represents an available space among the X axis.
     100 +type xSpace struct {
     101 + // min is the current relative coordinate.
     102 + // These are zero based, i.e. not adjusted to axisStart.
     103 + cur int
     104 + // max is the maximum relative coordinate.
     105 + // These are zero based, i.e. not adjusted to axisStart.
     106 + // The xSpace instance contains points 0 <= x < max
     107 + max int
     108 + 
     109 + // graphZero is the (0, 0) point on the graph.
     110 + graphZero image.Point
     111 +}
     112 + 
     113 +// newXSpace returns a new xSpace instance initialized for the provided width.
     114 +func newXSpace(graphZero image.Point, graphWidth int) *xSpace {
     115 + return &xSpace{
     116 + cur: 0,
     117 + max: graphWidth,
     118 + graphZero: graphZero,
     119 + }
     120 +}
     121 + 
     122 +// Implements fmt.Stringer.
     123 +func (xs *xSpace) String() string {
     124 + return fmt.Sprintf("xSpace(size:%d)-cur:%v-max:%v", xs.Remaining(), image.Point{xs.cur, xs.graphZero.Y}, image.Point{xs.max, xs.graphZero.Y})
     125 +}
     126 + 
     127 +// Remaining returns the remaining size on the X axis.
     128 +func (xs *xSpace) Remaining() int {
     129 + return xs.max - xs.cur
     130 +}
     131 + 
     132 +// Relative returns the relative coordinate within the space, these are zero
     133 +// based.
     134 +func (xs *xSpace) Relative() image.Point {
     135 + return image.Point{xs.cur, xs.graphZero.Y + 1}
     136 +}
     137 + 
     138 +// LabelPos returns the absolute coordinate on the canvas where a label should
     139 +// be placed. The is the coordinate that represents the current relative
     140 +// coordinate of the space.
     141 +func (xs *xSpace) LabelPos() image.Point {
     142 + return image.Point{xs.cur + xs.graphZero.X, xs.graphZero.Y + 2} // First down is the axis, second the label.
     143 +}
     144 + 
     145 +// Sub subtracts the specified size from the beginning of the available
     146 +// space.
     147 +func (xs *xSpace) Sub(size int) error {
     148 + if xs.Remaining() < size {
     149 + return fmt.Errorf("unable to subtract %d from the start, not enough size in %v", size, xs)
     150 + }
     151 + xs.cur += size
     152 + return nil
     153 +}
     154 + 
     155 +// xLabels returns labels that should be placed under the X axis.
     156 +// The graphZero is the (0, 0) point of the graph area on the canvas.
     157 +// Labels are returned in an increasing value order.
     158 +// Returned labels shouldn't be trimmed, their count is adjusted so that they
     159 +// fit under the width of the axis.
     160 +// The customLabels map value positions in the series to the desired custom
     161 +// label. These are preferred if present.
     162 +func xLabels(scale *XScale, graphZero image.Point, customLabels map[int]string) ([]*Label, error) {
     163 + space := newXSpace(graphZero, scale.GraphWidth)
     164 + const minSpacing = 3
     165 + var res []*Label
     166 + 
     167 + next := 0
     168 + for haveLabels := 0; haveLabels <= int(scale.Max.Value); haveLabels = len(res) {
     169 + label, err := colLabel(scale, space, next, customLabels)
     170 + if err != nil {
     171 + return nil, err
     172 + }
     173 + if label == nil {
     174 + break
     175 + }
     176 + res = append(res, label)
     177 + 
     178 + next++
     179 + if next > int(scale.Max.Value) {
     180 + break
     181 + }
     182 + nextCell, err := scale.ValueToCell(next)
     183 + if err != nil {
     184 + return nil, err
     185 + }
     186 + 
     187 + skip := nextCell - space.Relative().X
     188 + if skip < minSpacing {
     189 + skip = minSpacing
     190 + }
     191 + 
     192 + if space.Remaining() <= skip {
     193 + break
     194 + }
     195 + if err := space.Sub(skip); err != nil {
     196 + return nil, err
     197 + }
     198 + }
     199 + return res, nil
     200 +}
     201 + 
     202 +// colLabel returns a label placed either at the beginning of the space.
     203 +// The space is adjusted according to how much space was taken by the label.
     204 +// Returns nil, nil if the label doesn't fit in the space.
     205 +func colLabel(scale *XScale, space *xSpace, labelNum int, customLabels map[int]string) (*Label, error) {
     206 + var val *Value
     207 + if custom, ok := customLabels[labelNum]; ok {
     208 + val = NewTextValue(custom)
     209 + } else {
     210 + pos := space.Relative()
     211 + v, err := scale.CellLabel(pos.X)
     212 + if err != nil {
     213 + return nil, fmt.Errorf("unable to determine label value for column %d: %v", pos.X, err)
     214 + }
     215 + val = v
     216 + }
     217 + 
     218 + labelLen := len(val.Text())
     219 + if labelLen > space.Remaining() {
     220 + return nil, nil
     221 + }
     222 + 
     223 + abs := space.LabelPos()
     224 + if err := space.Sub(labelLen); err != nil {
     225 + return nil, err
     226 + }
     227 + 
     228 + return &Label{
     229 + Value: val,
     230 + Pos: abs,
     231 + }, nil
     232 +}
     233 + 
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/axes/label_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 axes
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 +)
     23 + 
     24 +func TestYLabels(t *testing.T) {
     25 + const nonZeroDecimals = 2
     26 + tests := []struct {
     27 + desc string
     28 + min float64
     29 + max float64
     30 + graphHeight int
     31 + labelWidth int
     32 + want []*Label
     33 + wantErr bool
     34 + }{
     35 + {
     36 + desc: "fails when canvas is too small",
     37 + min: 0,
     38 + max: 1,
     39 + graphHeight: 1,
     40 + labelWidth: 4,
     41 + wantErr: true,
     42 + },
     43 + {
     44 + desc: "fails when labelWidth is too small",
     45 + min: 0,
     46 + max: 1,
     47 + graphHeight: 2,
     48 + labelWidth: 0,
     49 + wantErr: true,
     50 + },
     51 + {
     52 + desc: "works when there are no data points",
     53 + min: 0,
     54 + max: 0,
     55 + graphHeight: 2,
     56 + labelWidth: 1,
     57 + want: []*Label{
     58 + {NewValue(0, nonZeroDecimals), image.Point{0, 1}},
     59 + },
     60 + },
     61 + {
     62 + desc: "only one label on tall canvas without data points",
     63 + min: 0,
     64 + max: 0,
     65 + graphHeight: 25,
     66 + labelWidth: 1,
     67 + want: []*Label{
     68 + {NewValue(0, nonZeroDecimals), image.Point{0, 24}},
     69 + },
     70 + },
     71 + {
     72 + desc: "works when min equals max",
     73 + min: 5,
     74 + max: 5,
     75 + graphHeight: 2,
     76 + labelWidth: 1,
     77 + want: []*Label{
     78 + {NewValue(0, nonZeroDecimals), image.Point{0, 1}},
     79 + {NewValue(2.88, nonZeroDecimals), image.Point{0, 0}},
     80 + },
     81 + },
     82 + {
     83 + desc: "only two rows on the canvas, labels min and max",
     84 + min: 0,
     85 + max: 5,
     86 + graphHeight: 2,
     87 + labelWidth: 1,
     88 + want: []*Label{
     89 + {NewValue(0, nonZeroDecimals), image.Point{0, 1}},
     90 + {NewValue(2.88, nonZeroDecimals), image.Point{0, 0}},
     91 + },
     92 + },
     93 + {
     94 + desc: "aligns labels to the right",
     95 + min: 0,
     96 + max: 5,
     97 + graphHeight: 2,
     98 + labelWidth: 5,
     99 + want: []*Label{
     100 + {NewValue(0, nonZeroDecimals), image.Point{4, 1}},
     101 + {NewValue(2.88, nonZeroDecimals), image.Point{1, 0}},
     102 + },
     103 + },
     104 + {
     105 + desc: "multiple labels, last on the top",
     106 + min: 0,
     107 + max: 5,
     108 + graphHeight: 9,
     109 + labelWidth: 1,
     110 + want: []*Label{
     111 + {NewValue(0, nonZeroDecimals), image.Point{0, 8}},
     112 + {NewValue(2.4, nonZeroDecimals), image.Point{0, 4}},
     113 + {NewValue(4.8, nonZeroDecimals), image.Point{0, 0}},
     114 + },
     115 + },
     116 + {
     117 + desc: "multiple labels, last on top-1",
     118 + min: 0,
     119 + max: 5,
     120 + graphHeight: 10,
     121 + labelWidth: 1,
     122 + want: []*Label{
     123 + {NewValue(0, nonZeroDecimals), image.Point{0, 9}},
     124 + {NewValue(2.08, nonZeroDecimals), image.Point{0, 5}},
     125 + {NewValue(4.16, nonZeroDecimals), image.Point{0, 1}},
     126 + },
     127 + },
     128 + }
     129 + 
     130 + for _, tc := range tests {
     131 + t.Run(tc.desc, func(t *testing.T) {
     132 + scale, err := NewYScale(tc.min, tc.max, tc.graphHeight, nonZeroDecimals)
     133 + if err != nil {
     134 + t.Fatalf("NewYScale => unexpected error: %v", err)
     135 + }
     136 + t.Logf("scale step: %v", scale.Step.Rounded)
     137 + got, err := yLabels(scale, tc.labelWidth)
     138 + if (err != nil) != tc.wantErr {
     139 + t.Errorf("yLabels => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     140 + }
     141 + if err != nil {
     142 + return
     143 + }
     144 + if diff := pretty.Compare(tc.want, got); diff != "" {
     145 + t.Errorf("yLabels => unexpected diff (-want, +got):\n%s", diff)
     146 + }
     147 + })
     148 + }
     149 +}
     150 + 
     151 +func TestXLabels(t *testing.T) {
     152 + const nonZeroDecimals = 2
     153 + tests := []struct {
     154 + desc string
     155 + numPoints int
     156 + graphWidth int
     157 + graphZero image.Point
     158 + customLabels map[int]string
     159 + want []*Label
     160 + wantErr bool
     161 + }{
     162 + {
     163 + desc: "only one point",
     164 + numPoints: 1,
     165 + graphWidth: 1,
     166 + graphZero: image.Point{0, 1},
     167 + want: []*Label{
     168 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     169 + },
     170 + },
     171 + {
     172 + desc: "two points, only one label fits",
     173 + numPoints: 2,
     174 + graphWidth: 1,
     175 + graphZero: image.Point{0, 1},
     176 + want: []*Label{
     177 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     178 + },
     179 + },
     180 + {
     181 + desc: "two points, two labels fit exactly",
     182 + numPoints: 2,
     183 + graphWidth: 5,
     184 + graphZero: image.Point{0, 1},
     185 + want: []*Label{
     186 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     187 + {NewValue(1, nonZeroDecimals), image.Point{4, 3}},
     188 + },
     189 + },
     190 + {
     191 + desc: "labels are placed according to graphZero",
     192 + numPoints: 2,
     193 + graphWidth: 5,
     194 + graphZero: image.Point{3, 5},
     195 + want: []*Label{
     196 + {NewValue(0, nonZeroDecimals), image.Point{3, 7}},
     197 + {NewValue(1, nonZeroDecimals), image.Point{7, 7}},
     198 + },
     199 + },
     200 + {
     201 + desc: "skip to next value exhausts the space completely",
     202 + numPoints: 11,
     203 + graphWidth: 4,
     204 + graphZero: image.Point{0, 1},
     205 + want: []*Label{
     206 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     207 + },
     208 + },
     209 + {
     210 + desc: "second label doesn't fit due to its length",
     211 + numPoints: 100,
     212 + graphWidth: 5,
     213 + graphZero: image.Point{0, 1},
     214 + want: []*Label{
     215 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     216 + },
     217 + },
     218 + {
     219 + desc: "two points, two labels, more space than minSpacing so end label adjusted",
     220 + numPoints: 2,
     221 + graphWidth: 6,
     222 + graphZero: image.Point{0, 1},
     223 + want: []*Label{
     224 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     225 + {NewValue(1, nonZeroDecimals), image.Point{5, 3}},
     226 + },
     227 + },
     228 + {
     229 + desc: "at most as many labels as there are points",
     230 + numPoints: 2,
     231 + graphWidth: 100,
     232 + graphZero: image.Point{0, 1},
     233 + want: []*Label{
     234 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     235 + {NewValue(1, nonZeroDecimals), image.Point{98, 3}},
     236 + },
     237 + },
     238 + {
     239 + desc: "some labels in the middle",
     240 + numPoints: 4,
     241 + graphWidth: 100,
     242 + graphZero: image.Point{0, 1},
     243 + want: []*Label{
     244 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     245 + {NewValue(1, nonZeroDecimals), image.Point{31, 3}},
     246 + {NewValue(2, nonZeroDecimals), image.Point{62, 3}},
     247 + {NewValue(3, nonZeroDecimals), image.Point{94, 3}},
     248 + },
     249 + },
     250 + {
     251 + desc: "custom labels provided",
     252 + numPoints: 4,
     253 + graphWidth: 100,
     254 + graphZero: image.Point{0, 1},
     255 + customLabels: map[int]string{
     256 + 0: "a",
     257 + 1: "b",
     258 + 2: "c",
     259 + 3: "d",
     260 + },
     261 + want: []*Label{
     262 + {NewTextValue("a"), image.Point{0, 3}},
     263 + {NewTextValue("b"), image.Point{31, 3}},
     264 + {NewTextValue("c"), image.Point{62, 3}},
     265 + {NewTextValue("d"), image.Point{94, 3}},
     266 + },
     267 + },
     268 + {
     269 + desc: "only some custom labels provided",
     270 + numPoints: 4,
     271 + graphWidth: 100,
     272 + graphZero: image.Point{0, 1},
     273 + customLabels: map[int]string{
     274 + 0: "a",
     275 + 3: "d",
     276 + },
     277 + want: []*Label{
     278 + {NewTextValue("a"), image.Point{0, 3}},
     279 + {NewValue(1, nonZeroDecimals), image.Point{31, 3}},
     280 + {NewValue(2, nonZeroDecimals), image.Point{62, 3}},
     281 + {NewTextValue("d"), image.Point{94, 3}},
     282 + },
     283 + },
     284 + {
     285 + desc: "not displayed if custom labels don't fit",
     286 + numPoints: 2,
     287 + graphWidth: 6,
     288 + graphZero: image.Point{0, 1},
     289 + customLabels: map[int]string{
     290 + 0: "a very very long custom label",
     291 + },
     292 + want: []*Label{},
     293 + },
     294 + {
     295 + desc: "more points than pixels",
     296 + numPoints: 100,
     297 + graphWidth: 6,
     298 + graphZero: image.Point{0, 1},
     299 + want: []*Label{
     300 + {NewValue(0, nonZeroDecimals), image.Point{0, 3}},
     301 + {NewValue(72, nonZeroDecimals), image.Point{4, 3}},
     302 + },
     303 + },
     304 + }
     305 + 
     306 + for _, tc := range tests {
     307 + t.Run(tc.desc, func(t *testing.T) {
     308 + scale, err := NewXScale(tc.numPoints, tc.graphWidth, nonZeroDecimals)
     309 + if err != nil {
     310 + t.Fatalf("NewXScale => unexpected error: %v", err)
     311 + }
     312 + t.Logf("scale step: %v", scale.Step.Rounded)
     313 + got, err := xLabels(scale, tc.graphZero, tc.customLabels)
     314 + if (err != nil) != tc.wantErr {
     315 + t.Errorf("xLabels => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     316 + }
     317 + if err != nil {
     318 + return
     319 + }
     320 + if diff := pretty.Compare(tc.want, got); diff != "" {
     321 + t.Errorf("xLabels => unexpected diff (-want, +got):\n%s", diff)
     322 + }
     323 + })
     324 + }
     325 +}
     326 + 
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/axes/scale.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 axes
     16 + 
     17 +// scale.go calculates the scale of the Y axis.
     18 + 
     19 +import (
     20 + "fmt"
     21 + 
     22 + "github.com/mum4k/termdash/canvas/braille"
     23 + "github.com/mum4k/termdash/numbers"
     24 +)
     25 + 
     26 +// YScale is the scale of the Y axis.
     27 +type YScale struct {
     28 + // Min is the minimum value on the axis.
     29 + Min *Value
     30 + // Max is the maximum value on the axis.
     31 + Max *Value
     32 + // Step is the step in the value between pixels.
     33 + Step *Value
     34 + 
     35 + // GraphHeight is the height in cells of the area on the canvas that is
     36 + // dedicated to the graph itself.
     37 + GraphHeight int
     38 + // brailleHeight is the height of the braille canvas based on the GraphHeight.
     39 + brailleHeight int
     40 +}
     41 + 
     42 +// NewYScale calculates the scale of the Y axis, given the boundary values and
     43 +// the height of the graph. The nonZeroDecimals dictates rounding of the
     44 +// calculated scale, see NewValue for details.
     45 +// Max must be greater or equal to min. The graphHeight must be a positive
     46 +// number.
     47 +func NewYScale(min, max float64, graphHeight, nonZeroDecimals int) (*YScale, error) {
     48 + if max < min {
     49 + return nil, fmt.Errorf("max(%v) cannot be less than min(%v)", max, min)
     50 + }
     51 + if min := 1; graphHeight < min {
     52 + return nil, fmt.Errorf("graphHeight cannot be less than %d, got %d", min, graphHeight)
     53 + }
     54 + 
     55 + brailleHeight := graphHeight * braille.RowMult
     56 + usablePixels := brailleHeight - 1 // One pixel reserved for value zero.
     57 + 
     58 + if min > 0 { // If we only have positive data points, make the scale zero based (min).
     59 + min = 0
     60 + }
     61 + if max < 0 { // If we only have negative data points, make the scale zero based (max).
     62 + max = 0
     63 + }
     64 + diff := max - min
     65 + step := NewValue(diff/float64(usablePixels), nonZeroDecimals)
     66 + return &YScale{
     67 + Min: NewValue(min, nonZeroDecimals),
     68 + Max: NewValue(max, nonZeroDecimals),
     69 + Step: step,
     70 + GraphHeight: graphHeight,
     71 + brailleHeight: brailleHeight,
     72 + }, nil
     73 +}
     74 + 
     75 +// PixelToValue given a Y coordinate of the pixel, returns its value according
     76 +// to the scale. The coordinate must be within bounds of the graph height
     77 +// provided to NewYScale. Y coordinates grow down.
     78 +func (ys *YScale) PixelToValue(y int) (float64, error) {
     79 + pos, err := yToPosition(y, ys.brailleHeight)
     80 + if err != nil {
     81 + return 0, err
     82 + }
     83 + 
     84 + switch {
     85 + case pos == 0:
     86 + return ys.Min.Rounded, nil
     87 + case pos == ys.brailleHeight-1:
     88 + return ys.Max.Rounded, nil
     89 + default:
     90 + v := float64(pos) * ys.Step.Rounded
     91 + if ys.Min.Value < 0 {
     92 + diff := -1 * ys.Min.Value
     93 + v -= diff
     94 + }
     95 + return v, nil
     96 + }
     97 +}
     98 + 
     99 +// ValueToPixel given a value, determines the Y coordinate of the pixel that
     100 +// most closely represents the value on the line chart according to the scale.
     101 +// The value must be within the bounds provided to NewYScale. Y coordinates
     102 +// grow down.
     103 +func (ys *YScale) ValueToPixel(v float64) (int, error) {
     104 + if ys.Step.Rounded == 0 {
     105 + return 0, nil
     106 + }
     107 + 
     108 + if ys.Min.Value < 0 {
     109 + diff := -1 * ys.Min.Value
     110 + v += diff
     111 + }
     112 + pos := int(numbers.Round(v / ys.Step.Rounded))
     113 + return positionToY(pos, ys.brailleHeight)
     114 +}
     115 + 
     116 +// CellLabel given a Y coordinate of a cell on the canvas, determines value of
     117 +// the label that should be next to it. The Y coordinate must be within the
     118 +// graphHeight provided to NewYScale. Y coordinates grow down.
     119 +func (ys *YScale) CellLabel(y int) (*Value, error) {
     120 + pos, err := yToPosition(y, ys.GraphHeight)
     121 + if err != nil {
     122 + return nil, err
     123 + }
     124 + 
     125 + pixelY, err := positionToY(pos*braille.RowMult, ys.brailleHeight)
     126 + if err != nil {
     127 + return nil, err
     128 + }
     129 + 
     130 + v, err := ys.PixelToValue(pixelY)
     131 + if err != nil {
     132 + return nil, err
     133 + }
     134 + return NewValue(v, ys.Min.NonZeroDecimals), nil
     135 +}
     136 + 
     137 +// XScale is the scale of the X axis.
     138 +type XScale struct {
     139 + // Min is the minimum value on the axis.
     140 + Min *Value
     141 + // Max is the maximum value on the axis.
     142 + Max *Value
     143 + // Step is the step in the value between pixels.
     144 + Step *Value
     145 + 
     146 + // GraphWidth is the width in cells of the area on the canvas that is
     147 + // dedicated to the graph.
     148 + GraphWidth int
     149 + // brailleWidth is the width of the braille canvas based on the GraphWidth.
     150 + brailleWidth int
     151 +}
     152 + 
     153 +// NewXScale calculates the scale of the X axis, given the number of data
     154 +// points in the series and the width on the canvas that is available to the X
     155 +// axis. The nonZeroDecimals dictates rounding of the calculated scale, see
     156 +// NewValue for details.
     157 +// The numPoints must be zero or positive number. The graphWidth must be a
     158 +// positive number.
     159 +func NewXScale(numPoints int, graphWidth, nonZeroDecimals int) (*XScale, error) {
     160 + if numPoints < 0 {
     161 + return nil, fmt.Errorf("numPoints cannot be negative, got %d", numPoints)
     162 + }
     163 + if min := 1; graphWidth < min {
     164 + return nil, fmt.Errorf("graphWidth must be at least %d, got %d", min, graphWidth)
     165 + }
     166 + 
     167 + brailleWidth := graphWidth * braille.ColMult
     168 + usablePixels := brailleWidth - 1 // One pixel reserved for value zero.
     169 + 
     170 + const min float64 = 0
     171 + max := float64(numPoints - 1)
     172 + if max < 0 {
     173 + max = 0
     174 + }
     175 + diff := max - min
     176 + step := NewValue(diff/float64(usablePixels), nonZeroDecimals)
     177 + return &XScale{
     178 + Min: NewValue(min, nonZeroDecimals),
     179 + Max: NewValue(max, nonZeroDecimals),
     180 + Step: step,
     181 + GraphWidth: graphWidth,
     182 + brailleWidth: brailleWidth,
     183 + }, nil
     184 +}
     185 + 
     186 +// PixelToValue given a X coordinate of the pixel, returns its value according
     187 +// to the scale. The coordinate must be within bounds of the canvas width
     188 +// provided to NewXScale. X coordinates grow right.
     189 +func (xs *XScale) PixelToValue(x int) (float64, error) {
     190 + if min, max := 0, xs.brailleWidth; x < min || x >= max {
     191 + return 0, fmt.Errorf("invalid x coordinate %d, must be in range %v < x < %v", x, min, max)
     192 + }
     193 + 
     194 + switch {
     195 + case x == 0:
     196 + return xs.Min.Rounded, nil
     197 + case x == xs.brailleWidth-1:
     198 + return xs.Max.Rounded, nil
     199 + default:
     200 + return float64(x) * xs.Step.Rounded, nil
     201 + }
     202 +}
     203 + 
     204 +// ValueToPixel given a value, determines the X coordinate of the pixel that
     205 +// most closely represents the value on the line chart according to the scale.
     206 +// The value must be within the bounds provided to NewXScale. X coordinates
     207 +// grow right.
     208 +func (xs *XScale) ValueToPixel(v int) (int, error) {
     209 + fv := float64(v)
     210 + if min, max := xs.Min.Value, xs.Max.Rounded; fv < min || fv > max {
     211 + return 0, fmt.Errorf("invalid value %v, must be in range %v <= v <= %v", v, min, max)
     212 + }
     213 + if xs.Step.Rounded == 0 {
     214 + return 0, nil
     215 + }
     216 + return int(numbers.Round(fv / xs.Step.Rounded)), nil
     217 +}
     218 + 
     219 +// ValueToCell given a value, determines the X coordinate of the cell that
     220 +// most closely represents the value on the line chart according to the scale.
     221 +// The value must be within the bounds provided to NewXScale. X coordinates
     222 +// grow right.
     223 +func (xs *XScale) ValueToCell(v int) (int, error) {
     224 + p, err := xs.ValueToPixel(v)
     225 + if err != nil {
     226 + return 0, err
     227 + }
     228 + return p / braille.ColMult, nil
     229 +}
     230 + 
     231 +// CellLabel given an X coordinate of a cell on the canvas, determines value of the
     232 +// label that should be next to it. The X coordinate must be within the
     233 +// graphWidth provided to NewXScale. X coordinates grow right.
     234 +// The returned value is rounded to the nearest int, rounding half away from zero.
     235 +func (xs *XScale) CellLabel(x int) (*Value, error) {
     236 + v, err := xs.PixelToValue(x * braille.ColMult)
     237 + if err != nil {
     238 + return nil, err
     239 + }
     240 + return NewValue(numbers.Round(v), xs.Min.NonZeroDecimals), nil
     241 +}
     242 + 
     243 +// positionToY, given a position within the height, returns the Y coordinate of
     244 +// the position. Positions grow up, coordinates grow down.
     245 +//
     246 +// Positions Y Coordinates
     247 +// 2 | 0
     248 +// 1 | 1
     249 +// 0 | 2
     250 +func positionToY(pos int, height int) (int, error) {
     251 + max := height - 1
     252 + if min := 0; pos < min || pos > max {
     253 + return 0, fmt.Errorf("position %d out of bounds %d <= pos <= %d", pos, min, max)
     254 + }
     255 + return max - pos, nil
     256 +}
     257 + 
     258 +// yToPosition is the reverse of positionToY.
     259 +func yToPosition(y int, height int) (int, error) {
     260 + max := height - 1
     261 + if min := 0; y < min || y > max {
     262 + return 0, fmt.Errorf("Y coordinate %d out of bounds %d <= Y <= %d", y, min, max)
     263 + }
     264 + return -1*y + max, nil
     265 +}
     266 + 
Please wait...
Page is in error, reload to recover