Projects STRLCPY termdash Commits e6af456c
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
Showing first 60 files as there are too many
  • ■ ■ ■ ■
    .travis.yml
    1 1  language: go
    2 2  go:
    3  - - 1.9.x
    4 3   - 1.10.x
    5 4   - 1.11.x
     5 + - 1.12.x
    6 6   - stable
    7 7  script:
    8 8   - go get -t ./...
    skipped 10 lines
  • ■ ■ ■ ■ ■
    CHANGELOG.md
    skipped 6 lines
    7 7   
    8 8  ## [Unreleased]
    9 9   
     10 +## [0.9.0] - 28-Apr-2019
     11 + 
     12 +### Added
     13 + 
     14 +- The `TextInput` widget, an input field allowing interactive text input.
     15 +- The `Donut` widget can now display an optional text label under the donut.
     16 + 
     17 +### Changed
     18 + 
     19 +- Widgets now get information whether their container is focused when Draw is
     20 + executed.
     21 +- The SegmentDisplay widget now has a method that returns the observed character
     22 + capacity the last time Draw was called.
     23 +- The grid.Builder API now allows users to specify options for intermediate
     24 + containers, i.e. containers that don't have widgets, but represent rows and
     25 + columns.
     26 +- Line chart widget now allows `math.NaN` values to represent "no value" (values
     27 + that will not be rendered) in the values slice.
     28 + 
     29 +#### Breaking API changes
     30 + 
     31 +- The widgetapi.Widget.Draw method now accepts a second argument which provides
     32 + widgets with additional metadata. This affects all implemented widgets.
     33 +- Termdash now requires at least Go version 1.10, which allows us to utilize
     34 + `math.Round` instead of our own implementation and `strings.Builder` instead
     35 + of `bytes.Buffer`.
     36 +- Terminal shortcuts like `Ctrl-A` no longer come as two separate events,
     37 + Termdash now mirrors termbox-go and sends these as one event.
     38 + 
    10 39  ## [0.8.0] - 30-Mar-2019
    11 40   
    12 41  ### Added
    skipped 57 lines
    70 99  - The draw.LineStyle enum was refactored into its own package
    71 100   linestyle.LineStyle. Users will have to replace:
    72 101   
    73  - - draw.LineStyleNone -> linestyle.None
    74  - - draw.LineStyleLight -> linestyle.Light
    75  - - draw.LineStyleDouble -> linestyle.Double
    76  - - draw.LineStyleRound -> linestyle.Round
     102 + - draw.LineStyleNone -> linestyle.None
     103 + - draw.LineStyleLight -> linestyle.Light
     104 + - draw.LineStyleDouble -> linestyle.Double
     105 + - draw.LineStyleRound -> linestyle.Round
    77 106   
    78 107  ## [0.7.0] - 24-Feb-2019
    79 108   
    skipped 26 lines
    106 135   
    107 136  - The Text widget now has a Write option that atomically replaces the entire
    108 137   text content.
    109  - 
    110 138   
    111 139  #### Improvements to the infrastructure
    112 140   
    skipped 123 lines
    236 264  - The Gauge widget.
    237 265  - The Text widget.
    238 266   
    239  -[Unreleased]: https://github.com/mum4k/termdash/compare/v0.8.0...devel
     267 +[unreleased]: https://github.com/mum4k/termdash/compare/v0.9.0...devel
     268 +[0.9.0]: https://github.com/mum4k/termdash/compare/v0.8.0...v0.9.0
    240 269  [0.8.0]: https://github.com/mum4k/termdash/compare/v0.7.2...v0.8.0
    241 270  [0.7.2]: https://github.com/mum4k/termdash/compare/v0.7.1...v0.7.2
    242 271  [0.7.1]: https://github.com/mum4k/termdash/compare/v0.7.0...v0.7.1
    skipped 8 lines
  • ■ ■ ■ ■ ■ ■
    README.md
    skipped 9 lines
    10 10   
    11 11  Termdash is a cross-platform customizable terminal based dashboard.
    12 12   
    13  -[<img src="./doc/images/termdashdemo_0_8_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
     13 +[<img src="./doc/images/termdashdemo_0_9_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
    14 14   
    15 15  The feature set is inspired by the
    16 16  [gizak/termui](http://github.com/gizak/termui) project, which in turn was
    skipped 68 lines
    85 85  ```
    86 86   
    87 87  [<img src="./doc/images/buttondemo.gif" alt="buttondemo" type="image/gif" width="50%">](widgets/button/buttondemo/buttondemo.go)
     88 + 
     89 +## The TextInput
     90 + 
     91 +Allows users to interact with the application by entering, editing and
     92 +submitting text data. Run the
     93 +[textinputdemo](widgets/textinput/textinputdemo/textinputdemo.go).
     94 + 
     95 +```go
     96 +go run github.com/mum4k/termdash/widgets/textinput/textinputdemo/textinputdemo.go
     97 +```
     98 + 
     99 +[<img src="./doc/images/textinputdemo.gif" alt="textinputdemo" type="image/gif" width="80%">](widgets/textinput/textinputdemo/textinputdemo.go)
    88 100   
    89 101  ## The Gauge
    90 102   
    skipped 99 lines
  • ■ ■ ■ ■ ■
    container/container_test.go
    skipped 906 lines
    907 907   want: func(size image.Point) *faketerm.Terminal {
    908 908   ft := faketerm.MustNew(size)
    909 909   cvs := testcanvas.MustNew(ft.Area())
    910  - testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 10))
    911  - testdraw.MustText(cvs, "(10,10)", image.Point{1, 1})
    912  - testcanvas.MustApply(cvs, ft)
     910 + fakewidget.MustDraw(
     911 + ft,
     912 + cvs,
     913 + &widgetapi.Meta{Focused: true},
     914 + widgetapi.Options{},
     915 + )
    913 916   return ft
    914 917   },
    915 918   },
    skipped 117 lines
    1033 1036   fakewidget.MustDraw(
    1034 1037   ft,
    1035 1038   testcanvas.MustNew(image.Rect(0, 0, 20, 20)),
     1039 + &widgetapi.Meta{},
    1036 1040   widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
    1037 1041   )
    1038 1042   fakewidget.MustDraw(
    1039 1043   ft,
    1040 1044   testcanvas.MustNew(image.Rect(20, 0, 40, 10)),
     1045 + &widgetapi.Meta{},
    1041 1046   widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
    1042 1047   )
    1043 1048   
    skipped 1 lines
    1045 1050   fakewidget.MustDraw(
    1046 1051   ft,
    1047 1052   testcanvas.MustNew(image.Rect(20, 10, 40, 20)),
     1053 + &widgetapi.Meta{Focused: true},
    1048 1054   widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
    1049 1055   &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    1050 1056   )
    skipped 38 lines
    1089 1095   fakewidget.MustDraw(
    1090 1096   ft,
    1091 1097   testcanvas.MustNew(image.Rect(0, 0, 20, 20)),
     1098 + &widgetapi.Meta{},
    1092 1099   widgetapi.Options{WantKeyboard: widgetapi.KeyScopeGlobal},
    1093 1100   &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    1094 1101   )
    skipped 2 lines
    1097 1104   fakewidget.MustDraw(
    1098 1105   ft,
    1099 1106   testcanvas.MustNew(image.Rect(20, 0, 40, 10)),
     1107 + &widgetapi.Meta{},
    1100 1108   widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
    1101 1109   )
    1102 1110   
    skipped 1 lines
    1104 1112   fakewidget.MustDraw(
    1105 1113   ft,
    1106 1114   testcanvas.MustNew(image.Rect(20, 10, 40, 20)),
     1115 + &widgetapi.Meta{Focused: true},
    1107 1116   widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
    1108 1117   &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    1109 1118   )
    skipped 18 lines
    1128 1137   fakewidget.MustDraw(
    1129 1138   ft,
    1130 1139   testcanvas.MustNew(ft.Area()),
     1140 + &widgetapi.Meta{Focused: true},
    1131 1141   widgetapi.Options{},
    1132 1142   )
    1133 1143   return ft
    skipped 18 lines
    1152 1162   fakewidget.MustDraw(
    1153 1163   ft,
    1154 1164   testcanvas.MustNew(ft.Area()),
     1165 + &widgetapi.Meta{Focused: true},
    1155 1166   widgetapi.Options{},
    1156 1167   )
    1157 1168   return ft
    skipped 91 lines
    1249 1260   fakewidget.MustDraw(
    1250 1261   ft,
    1251 1262   testcanvas.MustNew(ft.Area()),
     1263 + &widgetapi.Meta{Focused: true},
    1252 1264   widgetapi.Options{},
    1253 1265   )
    1254 1266   return ft
    skipped 46 lines
    1301 1313   fakewidget.MustDraw(
    1302 1314   ft,
    1303 1315   testcanvas.MustNew(image.Rect(0, 0, 25, 20)),
     1316 + &widgetapi.Meta{},
    1304 1317   widgetapi.Options{},
    1305 1318   )
    1306 1319   fakewidget.MustDraw(
    1307 1320   ft,
    1308 1321   testcanvas.MustNew(image.Rect(25, 10, 50, 20)),
     1322 + &widgetapi.Meta{},
    1309 1323   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1310 1324   &terminalapi.Keyboard{},
    1311 1325   )
    skipped 2 lines
    1314 1328   fakewidget.MustDraw(
    1315 1329   ft,
    1316 1330   testcanvas.MustNew(image.Rect(25, 0, 50, 10)),
     1331 + &widgetapi.Meta{Focused: true},
    1317 1332   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1318 1333   &terminalapi.Mouse{Position: image.Point{24, 9}, Button: mouse.ButtonLeft},
    1319 1334   &terminalapi.Mouse{Position: image.Point{24, 9}, Button: mouse.ButtonRelease},
    skipped 61 lines
    1381 1396   fakewidget.MustDraw(
    1382 1397   ft,
    1383 1398   testcanvas.MustNew(image.Rect(0, 0, 25, 20)),
     1399 + &widgetapi.Meta{},
    1384 1400   widgetapi.Options{},
    1385 1401   )
    1386 1402   fakewidget.MustDraw(
    1387 1403   ft,
    1388 1404   testcanvas.MustNew(image.Rect(25, 10, 50, 20)),
     1405 + &widgetapi.Meta{},
    1389 1406   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1390 1407   &terminalapi.Keyboard{},
    1391 1408   )
    skipped 2 lines
    1394 1411   fakewidget.MustDraw(
    1395 1412   ft,
    1396 1413   testcanvas.MustNew(image.Rect(26, 1, 49, 9)),
     1414 + &widgetapi.Meta{Focused: true},
    1397 1415   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1398 1416   &terminalapi.Mouse{Position: image.Point{22, 7}, Button: mouse.ButtonLeft},
    1399 1417   &terminalapi.Mouse{Position: image.Point{22, 7}, Button: mouse.ButtonRelease},
    skipped 19 lines
    1419 1437   fakewidget.MustDraw(
    1420 1438   ft,
    1421 1439   testcanvas.MustNew(ft.Area()),
     1440 + &widgetapi.Meta{Focused: true},
    1422 1441   widgetapi.Options{},
    1423 1442   )
    1424 1443   return ft
    skipped 28 lines
    1453 1472   fakewidget.MustDraw(
    1454 1473   ft,
    1455 1474   testcanvas.MustNew(image.Rect(1, 1, 19, 19)),
     1475 + &widgetapi.Meta{Focused: true},
    1456 1476   widgetapi.Options{},
    1457 1477   )
    1458 1478   return ft
    skipped 28 lines
    1487 1507   fakewidget.MustDraw(
    1488 1508   ft,
    1489 1509   testcanvas.MustNew(image.Rect(1, 1, 20, 19)),
     1510 + &widgetapi.Meta{Focused: true},
    1490 1511   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1491 1512   &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
    1492 1513   )
    skipped 29 lines
    1522 1543   fakewidget.MustDraw(
    1523 1544   ft,
    1524 1545   testcanvas.MustNew(image.Rect(1, 1, 20, 19)),
     1546 + &widgetapi.Meta{Focused: true},
    1525 1547   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1526 1548   &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
    1527 1549   )
    skipped 25 lines
    1553 1575   fakewidget.MustDraw(
    1554 1576   ft,
    1555 1577   testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
     1578 + &widgetapi.Meta{Focused: true},
    1556 1579   widgetapi.Options{},
    1557 1580   )
    1558 1581   return ft
    skipped 24 lines
    1583 1606   fakewidget.MustDraw(
    1584 1607   ft,
    1585 1608   testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
     1609 + &widgetapi.Meta{Focused: true},
    1586 1610   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1587 1611   &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
    1588 1612   )
    skipped 25 lines
    1614 1638   fakewidget.MustDraw(
    1615 1639   ft,
    1616 1640   testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
     1641 + &widgetapi.Meta{Focused: true},
    1617 1642   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1618 1643   &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
    1619 1644   )
    skipped 30 lines
    1650 1675   fakewidget.MustDraw(
    1651 1676   ft,
    1652 1677   testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
     1678 + &widgetapi.Meta{},
    1653 1679   widgetapi.Options{},
    1654 1680   )
    1655 1681   return ft
    skipped 29 lines
    1685 1711   fakewidget.MustDraw(
    1686 1712   ft,
    1687 1713   testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
     1714 + &widgetapi.Meta{},
    1688 1715   widgetapi.Options{},
    1689 1716   )
    1690 1717   return ft
    skipped 29 lines
    1720 1747   fakewidget.MustDraw(
    1721 1748   ft,
    1722 1749   testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
     1750 + &widgetapi.Meta{},
    1723 1751   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1724 1752   &terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
    1725 1753   )
    skipped 25 lines
    1751 1779   fakewidget.MustDraw(
    1752 1780   ft,
    1753 1781   testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
     1782 + &widgetapi.Meta{Focused: true},
    1754 1783   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1755 1784   &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
    1756 1785   )
    skipped 25 lines
    1782 1811   fakewidget.MustDraw(
    1783 1812   ft,
    1784 1813   testcanvas.MustNew(image.Rect(6, 0, 24, 20)),
     1814 + &widgetapi.Meta{Focused: true},
    1785 1815   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    1786 1816   &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
    1787 1817   )
    skipped 19 lines
    1807 1837   fakewidget.MustDraw(
    1808 1838   ft,
    1809 1839   testcanvas.MustNew(ft.Area()),
     1840 + &widgetapi.Meta{Focused: true},
    1810 1841   widgetapi.Options{},
    1811 1842   )
    1812 1843   return ft
    skipped 196 lines
    2009 2040   cvs := testcanvas.MustNew(ft.Area())
    2010 2041   wAr := image.Rect(10, 0, 20, 10)
    2011 2042   wCvs := testcanvas.MustNew(wAr)
    2012  - fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
     2043 + fakewidget.MustDraw(ft, wCvs, &widgetapi.Meta{}, widgetapi.Options{})
    2013 2044   testcanvas.MustCopyTo(wCvs, cvs)
    2014 2045   testcanvas.MustApply(cvs, ft)
    2015 2046   return ft
    skipped 25 lines
    2041 2072   want: func(size image.Point) *faketerm.Terminal {
    2042 2073   ft := faketerm.MustNew(size)
    2043 2074   cvs := testcanvas.MustNew(ft.Area())
    2044  - fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
     2075 + fakewidget.MustDraw(ft, cvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    2045 2076   testcanvas.MustApply(cvs, ft)
    2046 2077   return ft
    2047 2078   },
    skipped 164 lines
    2212 2243   fakewidget.MustDraw(
    2213 2244   ft,
    2214 2245   cvs,
     2246 + &widgetapi.Meta{Focused: true},
    2215 2247   widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
    2216 2248   &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    2217 2249   )
    skipped 25 lines
    2243 2275   fakewidget.MustDraw(
    2244 2276   ft,
    2245 2277   cvs,
     2278 + &widgetapi.Meta{Focused: true},
    2246 2279   widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
    2247 2280   &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
    2248 2281   )
    skipped 89 lines
  • ■ ■ ■ ■ ■
    container/draw.go
    skipped 24 lines
    25 25   "github.com/mum4k/termdash/internal/area"
    26 26   "github.com/mum4k/termdash/internal/canvas"
    27 27   "github.com/mum4k/termdash/internal/draw"
     28 + "github.com/mum4k/termdash/widgetapi"
    28 29  )
    29 30   
    30 31  // drawTree draws this container and all of its sub containers.
    skipped 99 lines
    130 131   return err
    131 132   }
    132 133   
    133  - if err := c.opts.widget.Draw(cvs); err != nil {
     134 + meta := &widgetapi.Meta{
     135 + Focused: c.focusTracker.isActive(c),
     136 + }
     137 + 
     138 + if err := c.opts.widget.Draw(cvs, meta); err != nil {
    134 139   return err
    135 140   }
    136 141   return cvs.Apply(c.term)
    skipped 35 lines
  • ■ ■ ■ ■ ■
    container/draw_test.go
    skipped 244 lines
    245 245   wAr := image.Rect(5, 2, 17, 6)
    246 246   wCvs := testcanvas.MustNew(wAr)
    247 247   // Fake widget border.
    248  - fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
     248 + fakewidget.MustDraw(ft, wCvs, &widgetapi.Meta{}, widgetapi.Options{})
    249 249   testcanvas.MustCopyTo(wCvs, cvs)
    250 250   testcanvas.MustApply(cvs, ft)
    251 251   return ft
    skipped 26 lines
    278 278   wAr := image.Rect(4, 2, 14, 16)
    279 279   wCvs := testcanvas.MustNew(wAr)
    280 280   // Fake widget border.
    281  - fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
     281 + fakewidget.MustDraw(ft, wCvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    282 282   testcanvas.MustCopyTo(wCvs, cvs)
    283 283   testcanvas.MustApply(cvs, ft)
    284 284   return ft
    skipped 72 lines
    357 357   wAr := image.Rect(9, 3, 25, 13)
    358 358   wCvs := testcanvas.MustNew(wAr)
    359 359   // Fake widget border.
    360  - fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
     360 + fakewidget.MustDraw(ft, wCvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    361 361   testcanvas.MustCopyTo(wCvs, cvs)
    362 362   testcanvas.MustApply(cvs, ft)
    363 363   return ft
    skipped 236 lines
    600 600   
    601 601   // Fake widget.
    602 602   cvs := testcanvas.MustNew(image.Rect(1, 1, 11, 11))
    603  - fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
     603 + fakewidget.MustDraw(ft, cvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    604 604   testcanvas.MustApply(cvs, ft)
    605 605   return ft
    606 606   },
    skipped 25 lines
    632 632   
    633 633   // Fake widget.
    634 634   cvs := testcanvas.MustNew(image.Rect(1, 1, 11, 21))
    635  - fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
     635 + fakewidget.MustDraw(ft, cvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    636 636   testcanvas.MustApply(cvs, ft)
    637 637   return ft
    638 638   },
    skipped 25 lines
    664 664   
    665 665   // Fake widget.
    666 666   cvs := testcanvas.MustNew(image.Rect(1, 1, 21, 11))
    667  - fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
     667 + fakewidget.MustDraw(ft, cvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    668 668   testcanvas.MustApply(cvs, ft)
    669 669   return ft
    670 670   },
    skipped 23 lines
    694 694   )
    695 695   
    696 696   // Fake widget border.
    697  - testdraw.MustBorder(cvs, image.Rect(1, 1, 11, 21))
    698  - testdraw.MustText(cvs, "(10,20)", image.Point{2, 2})
     697 + wCvs := testcanvas.MustNew(image.Rect(1, 1, 11, 21))
     698 + fakewidget.MustDraw(
     699 + ft,
     700 + wCvs,
     701 + &widgetapi.Meta{Focused: true},
     702 + widgetapi.Options{},
     703 + )
    699 704   
     705 + testcanvas.MustCopyTo(wCvs, cvs)
    700 706   testcanvas.MustApply(cvs, ft)
    701 707   return ft
    702 708   },
    skipped 24 lines
    727 733   
    728 734   // Fake widget.
    729 735   cvs := testcanvas.MustNew(image.Rect(1, 1, 20, 20))
    730  - fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
     736 + fakewidget.MustDraw(ft, cvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    731 737   testcanvas.MustApply(cvs, ft)
    732 738   return ft
    733 739   },
    skipped 23 lines
    757 763   )
    758 764   
    759 765   // Fake widget border.
    760  - testdraw.MustBorder(cvs, image.Rect(1, 1, 11, 21))
    761  - testdraw.MustText(cvs, "(10,20)", image.Point{2, 2})
     766 + wCvs := testcanvas.MustNew(image.Rect(1, 1, 11, 21))
     767 + fakewidget.MustDraw(
     768 + ft,
     769 + wCvs,
     770 + &widgetapi.Meta{Focused: true},
     771 + widgetapi.Options{},
     772 + )
    762 773   
     774 + testcanvas.MustCopyTo(wCvs, cvs)
    763 775   testcanvas.MustApply(cvs, ft)
    764 776   return ft
    765 777   },
    skipped 22 lines
    788 800   )
    789 801   
    790 802   // Fake widget border.
    791  - testdraw.MustBorder(cvs, image.Rect(6, 1, 16, 21))
    792  - testdraw.MustText(cvs, "(10,20)", image.Point{7, 2})
     803 + wCvs := testcanvas.MustNew(image.Rect(6, 1, 16, 21))
     804 + fakewidget.MustDraw(
     805 + ft,
     806 + wCvs,
     807 + &widgetapi.Meta{Focused: true},
     808 + widgetapi.Options{},
     809 + )
    793 810   
     811 + testcanvas.MustCopyTo(wCvs, cvs)
    794 812   testcanvas.MustApply(cvs, ft)
    795 813   return ft
    796 814   },
    skipped 22 lines
    819 837   )
    820 838   
    821 839   // Fake widget border.
    822  - testdraw.MustBorder(cvs, image.Rect(11, 1, 21, 21))
    823  - testdraw.MustText(cvs, "(10,20)", image.Point{12, 2})
     840 + wCvs := testcanvas.MustNew(image.Rect(11, 1, 21, 21))
     841 + fakewidget.MustDraw(
     842 + ft,
     843 + wCvs,
     844 + &widgetapi.Meta{Focused: true},
     845 + widgetapi.Options{},
     846 + )
    824 847   
     848 + testcanvas.MustCopyTo(wCvs, cvs)
    825 849   testcanvas.MustApply(cvs, ft)
    826 850   return ft
    827 851   },
    skipped 22 lines
    850 874   )
    851 875   
    852 876   // Fake widget border.
    853  - testdraw.MustBorder(cvs, image.Rect(1, 1, 21, 11))
    854  - testdraw.MustText(cvs, "(20,10)", image.Point{2, 2})
     877 + wCvs := testcanvas.MustNew(image.Rect(1, 1, 21, 11))
     878 + fakewidget.MustDraw(
     879 + ft,
     880 + wCvs,
     881 + &widgetapi.Meta{Focused: true},
     882 + widgetapi.Options{},
     883 + )
    855 884   
     885 + testcanvas.MustCopyTo(wCvs, cvs)
    856 886   testcanvas.MustApply(cvs, ft)
    857 887   return ft
    858 888   },
    skipped 22 lines
    881 911   )
    882 912   
    883 913   // Fake widget border.
    884  - testdraw.MustBorder(cvs, image.Rect(1, 6, 21, 16))
    885  - testdraw.MustText(cvs, "(20,10)", image.Point{2, 7})
     914 + wCvs := testcanvas.MustNew(image.Rect(1, 6, 21, 16))
     915 + fakewidget.MustDraw(
     916 + ft,
     917 + wCvs,
     918 + &widgetapi.Meta{Focused: true},
     919 + widgetapi.Options{},
     920 + )
    886 921   
     922 + testcanvas.MustCopyTo(wCvs, cvs)
    887 923   testcanvas.MustApply(cvs, ft)
    888 924   return ft
    889 925   },
    skipped 22 lines
    912 948   )
    913 949   
    914 950   // Fake widget border.
    915  - testdraw.MustBorder(cvs, image.Rect(1, 11, 21, 21))
    916  - testdraw.MustText(cvs, "(20,10)", image.Point{2, 12})
     951 + wCvs := testcanvas.MustNew(image.Rect(1, 11, 21, 21))
     952 + fakewidget.MustDraw(
     953 + ft,
     954 + wCvs,
     955 + &widgetapi.Meta{Focused: true},
     956 + widgetapi.Options{},
     957 + )
    917 958   
     959 + testcanvas.MustCopyTo(wCvs, cvs)
    918 960   testcanvas.MustApply(cvs, ft)
    919 961   return ft
    920 962   },
    skipped 73 lines
    994 1036   fakewidget.MustDraw(
    995 1037   ft,
    996 1038   testcanvas.MustNew(image.Rect(0, 0, 30, 5)),
     1039 + &widgetapi.Meta{},
    997 1040   widgetapi.Options{},
    998 1041   )
    999 1042   fakewidget.MustDraw(
    1000 1043   ft,
    1001 1044   testcanvas.MustNew(image.Rect(0, 5, 30, 10)),
     1045 + &widgetapi.Meta{},
    1002 1046   widgetapi.Options{},
    1003 1047   )
    1004 1048   fakewidget.MustDraw(
    1005 1049   ft,
    1006 1050   testcanvas.MustNew(image.Rect(30, 0, 45, 10)),
     1051 + &widgetapi.Meta{},
    1007 1052   widgetapi.Options{},
    1008 1053   )
    1009 1054   fakewidget.MustDraw(
    1010 1055   ft,
    1011 1056   testcanvas.MustNew(image.Rect(45, 0, 60, 10)),
     1057 + &widgetapi.Meta{},
    1012 1058   widgetapi.Options{},
    1013 1059   )
    1014 1060   return ft
    skipped 8 lines
    1023 1069   fakewidget.MustDraw(
    1024 1070   ft,
    1025 1071   testcanvas.MustNew(image.Rect(0, 0, 40, 5)),
     1072 + &widgetapi.Meta{},
    1026 1073   widgetapi.Options{},
    1027 1074   )
    1028 1075   fakewidget.MustDraw(
    1029 1076   ft,
    1030 1077   testcanvas.MustNew(image.Rect(0, 5, 40, 10)),
     1078 + &widgetapi.Meta{},
    1031 1079   widgetapi.Options{},
    1032 1080   )
    1033 1081   fakewidget.MustDraw(
    1034 1082   ft,
    1035 1083   testcanvas.MustNew(image.Rect(40, 0, 60, 10)),
     1084 + &widgetapi.Meta{},
    1036 1085   widgetapi.Options{},
    1037 1086   )
    1038 1087   fakewidget.MustDraw(
    1039 1088   ft,
    1040 1089   testcanvas.MustNew(image.Rect(60, 0, 80, 10)),
     1090 + &widgetapi.Meta{},
    1041 1091   widgetapi.Options{},
    1042 1092   )
    1043 1093   return ft
    skipped 8 lines
    1052 1102   fakewidget.MustDraw(
    1053 1103   ft,
    1054 1104   testcanvas.MustNew(image.Rect(0, 0, 25, 5)),
     1105 + &widgetapi.Meta{},
    1055 1106   widgetapi.Options{},
    1056 1107   )
    1057 1108   fakewidget.MustDraw(
    1058 1109   ft,
    1059 1110   testcanvas.MustNew(image.Rect(0, 5, 25, 10)),
     1111 + &widgetapi.Meta{},
    1060 1112   widgetapi.Options{},
    1061 1113   )
    1062 1114   fakewidget.MustDraw(
    1063 1115   ft,
    1064 1116   testcanvas.MustNew(image.Rect(25, 0, 37, 10)),
     1117 + &widgetapi.Meta{},
    1065 1118   widgetapi.Options{},
    1066 1119   )
    1067 1120   fakewidget.MustDraw(
    1068 1121   ft,
    1069 1122   testcanvas.MustNew(image.Rect(37, 0, 50, 10)),
     1123 + &widgetapi.Meta{},
    1070 1124   widgetapi.Options{},
    1071 1125   )
    1072 1126   return ft
    skipped 22 lines
  • ■ ■ ■ ■ ■
    container/grid/grid.go
    skipped 108 lines
    109 109   
    110 110   switch e := elem.(type) {
    111 111   case *row:
     112 + 
    112 113   if len(elems) > 0 {
    113 114   perc := innerPerc(e.heightPerc, parentHeightPerc)
    114 115   childHeightPerc := parentHeightPerc - e.heightPerc
    115 116   return []container.Option{
    116 117   container.SplitHorizontal(
    117  - container.Top(build(e.subElem, 100, parentWidthPerc)...),
     118 + container.Top(append(e.cOpts, build(e.subElem, 100, parentWidthPerc)...)...),
    118 119   container.Bottom(build(elems, childHeightPerc, parentWidthPerc)...),
    119 120   container.SplitPercent(perc),
    120 121   ),
    121 122   }
    122 123   }
    123  - return build(e.subElem, 100, parentWidthPerc)
     124 + return append(e.cOpts, build(e.subElem, 100, parentWidthPerc)...)
    124 125   
    125 126   case *col:
    126 127   if len(elems) > 0 {
    skipped 1 lines
    128 129   childWidthPerc := parentWidthPerc - e.widthPerc
    129 130   return []container.Option{
    130 131   container.SplitVertical(
    131  - container.Left(build(e.subElem, parentHeightPerc, 100)...),
     132 + container.Left(append(e.cOpts, build(e.subElem, parentHeightPerc, 100)...)...),
    132 133   container.Right(build(elems, parentHeightPerc, childWidthPerc)...),
    133 134   container.SplitPercent(perc),
    134 135   ),
    135 136   }
    136 137   }
    137  - return build(e.subElem, parentHeightPerc, 100)
     138 + return append(e.cOpts, build(e.subElem, parentHeightPerc, 100)...)
    138 139   
    139 140   case *widget:
    140 141   opts := e.cOpts
    skipped 45 lines
    186 187   
    187 188   // subElem are the sub Rows or Columns or a single widget.
    188 189   subElem []Element
     190 + 
     191 + // cOpts are the options for the row's container.
     192 + cOpts []container.Option
    189 193  }
    190 194   
    191 195  // isElement implements Element.isElement.
    skipped 12 lines
    204 208   
    205 209   // subElem are the sub Rows or Columns or a single widget.
    206 210   subElem []Element
     211 + 
     212 + // cOpts are the options for the column's container.
     213 + cOpts []container.Option
    207 214  }
    208 215   
    209 216  // isElement implements Element.isElement.
    skipped 34 lines
    244 251   }
    245 252  }
    246 253   
     254 +// RowHeightPercWithOpts is like RowHeightPerc, but also allows to apply
     255 +// additional options to the container that represents the row.
     256 +func RowHeightPercWithOpts(heightPerc int, cOpts []container.Option, subElements ...Element) Element {
     257 + return &row{
     258 + heightPerc: heightPerc,
     259 + subElem: subElements,
     260 + cOpts: cOpts,
     261 + }
     262 +}
     263 + 
    247 264  // ColWidthPerc creates a column of the specified width.
    248 265  // The width is supplied as width percentage of the parent element.
    249 266  // The sum of all widths at the same level cannot be larger than 100%. If it
    skipped 4 lines
    254 271   return &col{
    255 272   widthPerc: widthPerc,
    256 273   subElem: subElements,
     274 + }
     275 +}
     276 + 
     277 +// ColWidthPercWithOpts is like ColWidthPerc, but also allows to apply
     278 +// additional options to the container that represents the column.
     279 +func ColWidthPercWithOpts(widthPerc int, cOpts []container.Option, subElements ...Element) Element {
     280 + return &col{
     281 + widthPerc: widthPerc,
     282 + subElem: subElements,
     283 + cOpts: cOpts,
    257 284   }
    258 285  }
    259 286   
    skipped 10 lines
  • ■ ■ ■ ■ ■ ■
    container/grid/grid_test.go
    skipped 367 lines
    368 368   want: func(size image.Point) *faketerm.Terminal {
    369 369   ft := faketerm.MustNew(size)
    370 370   cvs := testcanvas.MustNew(ft.Area())
    371  - fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
     371 + fakewidget.MustDraw(ft, cvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    372 372   return ft
    373 373   },
    374 374   },
    skipped 9 lines
    384 384   want: func(size image.Point) *faketerm.Terminal {
    385 385   ft := faketerm.MustNew(size)
    386 386   top, bot := mustHSplit(ft.Area(), 50)
    387  - fakewidget.MustDraw(ft, testcanvas.MustNew(top), widgetapi.Options{})
    388  - fakewidget.MustDraw(ft, testcanvas.MustNew(bot), widgetapi.Options{})
     387 + fakewidget.MustDraw(ft, testcanvas.MustNew(top), &widgetapi.Meta{}, widgetapi.Options{})
     388 + fakewidget.MustDraw(ft, testcanvas.MustNew(bot), &widgetapi.Meta{}, widgetapi.Options{})
     389 + return ft
     390 + },
     391 + },
     392 + {
     393 + desc: "two equal rows with options",
     394 + termSize: image.Point{10, 10},
     395 + builder: func() *Builder {
     396 + b := New()
     397 + b.Add(RowHeightPercWithOpts(
     398 + 50,
     399 + []container.Option{
     400 + container.Border(linestyle.Double),
     401 + },
     402 + Widget(mirror()),
     403 + ))
     404 + b.Add(RowHeightPercWithOpts(
     405 + 50,
     406 + []container.Option{
     407 + container.Border(linestyle.Double),
     408 + },
     409 + Widget(mirror()),
     410 + ))
     411 + return b
     412 + }(),
     413 + want: func(size image.Point) *faketerm.Terminal {
     414 + ft := faketerm.MustNew(size)
     415 + 
     416 + top, bot := mustHSplit(ft.Area(), 50)
     417 + topCvs := testcanvas.MustNew(top)
     418 + botCvs := testcanvas.MustNew(bot)
     419 + testdraw.MustBorder(topCvs, topCvs.Area(), draw.BorderLineStyle(linestyle.Double))
     420 + testdraw.MustBorder(botCvs, botCvs.Area(), draw.BorderLineStyle(linestyle.Double))
     421 + testcanvas.MustApply(topCvs, ft)
     422 + testcanvas.MustApply(botCvs, ft)
     423 + 
     424 + topWidget := testcanvas.MustNew(area.ExcludeBorder(top))
     425 + botWidget := testcanvas.MustNew(area.ExcludeBorder(bot))
     426 + fakewidget.MustDraw(ft, topWidget, &widgetapi.Meta{}, widgetapi.Options{})
     427 + fakewidget.MustDraw(ft, botWidget, &widgetapi.Meta{}, widgetapi.Options{})
    389 428   return ft
    390 429   },
    391 430   },
    skipped 9 lines
    401 440   want: func(size image.Point) *faketerm.Terminal {
    402 441   ft := faketerm.MustNew(size)
    403 442   top, bot := mustHSplit(ft.Area(), 20)
    404  - fakewidget.MustDraw(ft, testcanvas.MustNew(top), widgetapi.Options{})
    405  - fakewidget.MustDraw(ft, testcanvas.MustNew(bot), widgetapi.Options{})
     443 + fakewidget.MustDraw(ft, testcanvas.MustNew(top), &widgetapi.Meta{}, widgetapi.Options{})
     444 + fakewidget.MustDraw(ft, testcanvas.MustNew(bot), &widgetapi.Meta{}, widgetapi.Options{})
     445 + return ft
     446 + },
     447 + },
     448 + {
     449 + desc: "two equal columns with options",
     450 + termSize: image.Point{20, 10},
     451 + builder: func() *Builder {
     452 + b := New()
     453 + b.Add(ColWidthPercWithOpts(
     454 + 50,
     455 + []container.Option{
     456 + container.Border(linestyle.Double),
     457 + },
     458 + Widget(mirror()),
     459 + ))
     460 + b.Add(ColWidthPercWithOpts(
     461 + 50,
     462 + []container.Option{
     463 + container.Border(linestyle.Double),
     464 + },
     465 + Widget(mirror()),
     466 + ))
     467 + return b
     468 + }(),
     469 + want: func(size image.Point) *faketerm.Terminal {
     470 + ft := faketerm.MustNew(size)
     471 + 
     472 + left, right := mustVSplit(ft.Area(), 50)
     473 + leftCvs := testcanvas.MustNew(left)
     474 + rightCvs := testcanvas.MustNew(right)
     475 + testdraw.MustBorder(leftCvs, leftCvs.Area(), draw.BorderLineStyle(linestyle.Double))
     476 + testdraw.MustBorder(rightCvs, rightCvs.Area(), draw.BorderLineStyle(linestyle.Double))
     477 + testcanvas.MustApply(leftCvs, ft)
     478 + testcanvas.MustApply(rightCvs, ft)
     479 + 
     480 + leftWidget := testcanvas.MustNew(area.ExcludeBorder(left))
     481 + rightWidget := testcanvas.MustNew(area.ExcludeBorder(right))
     482 + fakewidget.MustDraw(ft, leftWidget, &widgetapi.Meta{}, widgetapi.Options{})
     483 + fakewidget.MustDraw(ft, rightWidget, &widgetapi.Meta{}, widgetapi.Options{})
    406 484   return ft
    407 485   },
    408 486   },
    skipped 9 lines
    418 496   want: func(size image.Point) *faketerm.Terminal {
    419 497   ft := faketerm.MustNew(size)
    420 498   left, right := mustVSplit(ft.Area(), 50)
    421  - fakewidget.MustDraw(ft, testcanvas.MustNew(left), widgetapi.Options{})
    422  - fakewidget.MustDraw(ft, testcanvas.MustNew(right), widgetapi.Options{})
     499 + fakewidget.MustDraw(ft, testcanvas.MustNew(left), &widgetapi.Meta{}, widgetapi.Options{})
     500 + fakewidget.MustDraw(ft, testcanvas.MustNew(right), &widgetapi.Meta{}, widgetapi.Options{})
    423 501   return ft
    424 502   },
    425 503   },
    skipped 9 lines
    435 513   want: func(size image.Point) *faketerm.Terminal {
    436 514   ft := faketerm.MustNew(size)
    437 515   left, right := mustVSplit(ft.Area(), 20)
    438  - fakewidget.MustDraw(ft, testcanvas.MustNew(left), widgetapi.Options{})
    439  - fakewidget.MustDraw(ft, testcanvas.MustNew(right), widgetapi.Options{})
     516 + fakewidget.MustDraw(ft, testcanvas.MustNew(left), &widgetapi.Meta{}, widgetapi.Options{})
     517 + fakewidget.MustDraw(ft, testcanvas.MustNew(right), &widgetapi.Meta{}, widgetapi.Options{})
    440 518   return ft
    441 519   },
    442 520   },
    skipped 22 lines
    465 543   
    466 544   topLeft, topRight := mustVSplit(top, 50)
    467 545   botLeft, botRight := mustVSplit(bot, 50)
    468  - fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), widgetapi.Options{})
    469  - fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
    470  - fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), widgetapi.Options{})
    471  - fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
     546 + fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), &widgetapi.Meta{}, widgetapi.Options{})
     547 + fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), &widgetapi.Meta{}, widgetapi.Options{})
     548 + fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), &widgetapi.Meta{}, widgetapi.Options{})
     549 + fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), &widgetapi.Meta{}, widgetapi.Options{})
    472 550   return ft
    473 551   },
    474 552   },
    skipped 22 lines
    497 575   
    498 576   topLeft, topRight := mustVSplit(top, 20)
    499 577   botLeft, botRight := mustVSplit(bot, 80)
    500  - fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), widgetapi.Options{})
    501  - fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
    502  - fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), widgetapi.Options{})
    503  - fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
     578 + fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), &widgetapi.Meta{}, widgetapi.Options{})
     579 + fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), &widgetapi.Meta{}, widgetapi.Options{})
     580 + fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), &widgetapi.Meta{}, widgetapi.Options{})
     581 + fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), &widgetapi.Meta{}, widgetapi.Options{})
    504 582   return ft
    505 583   },
    506 584   },
    skipped 22 lines
    529 607   
    530 608   topLeft, topRight := mustVSplit(top, 50)
    531 609   botLeft, botRight := mustVSplit(bot, 50)
    532  - fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), widgetapi.Options{})
    533  - fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
    534  - fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), widgetapi.Options{})
    535  - fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
     610 + fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), &widgetapi.Meta{}, widgetapi.Options{})
     611 + fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), &widgetapi.Meta{}, widgetapi.Options{})
     612 + fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), &widgetapi.Meta{}, widgetapi.Options{})
     613 + fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), &widgetapi.Meta{}, widgetapi.Options{})
    536 614   return ft
    537 615   },
    538 616   },
    skipped 22 lines
    561 639   
    562 640   topLeft, topRight := mustHSplit(left, 20)
    563 641   botLeft, botRight := mustHSplit(right, 80)
    564  - fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), widgetapi.Options{})
    565  - fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
    566  - fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), widgetapi.Options{})
    567  - fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
     642 + fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), &widgetapi.Meta{}, widgetapi.Options{})
     643 + fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), &widgetapi.Meta{}, widgetapi.Options{})
     644 + fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), &widgetapi.Meta{}, widgetapi.Options{})
     645 + fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), &widgetapi.Meta{}, widgetapi.Options{})
    568 646   return ft
    569 647   },
    570 648   },
    skipped 42 lines
    613 691   topBotLeft, topBotRight := mustVSplit(topBot, 50)
    614 692   botTopLeft, botTopRight := mustVSplit(botTop, 50)
    615 693   botBotLeft, botBotRight := mustVSplit(botBot, 50)
    616  - fakewidget.MustDraw(ft, testcanvas.MustNew(topTopLeft), widgetapi.Options{})
    617  - fakewidget.MustDraw(ft, testcanvas.MustNew(topTopRight), widgetapi.Options{})
    618  - fakewidget.MustDraw(ft, testcanvas.MustNew(topBotLeft), widgetapi.Options{})
    619  - fakewidget.MustDraw(ft, testcanvas.MustNew(topBotRight), widgetapi.Options{})
    620  - fakewidget.MustDraw(ft, testcanvas.MustNew(botTopLeft), widgetapi.Options{})
    621  - fakewidget.MustDraw(ft, testcanvas.MustNew(botTopRight), widgetapi.Options{})
    622  - fakewidget.MustDraw(ft, testcanvas.MustNew(botBotLeft), widgetapi.Options{})
    623  - fakewidget.MustDraw(ft, testcanvas.MustNew(botBotRight), widgetapi.Options{})
     694 + fakewidget.MustDraw(ft, testcanvas.MustNew(topTopLeft), &widgetapi.Meta{}, widgetapi.Options{})
     695 + fakewidget.MustDraw(ft, testcanvas.MustNew(topTopRight), &widgetapi.Meta{}, widgetapi.Options{})
     696 + fakewidget.MustDraw(ft, testcanvas.MustNew(topBotLeft), &widgetapi.Meta{}, widgetapi.Options{})
     697 + fakewidget.MustDraw(ft, testcanvas.MustNew(topBotRight), &widgetapi.Meta{}, widgetapi.Options{})
     698 + fakewidget.MustDraw(ft, testcanvas.MustNew(botTopLeft), &widgetapi.Meta{}, widgetapi.Options{})
     699 + fakewidget.MustDraw(ft, testcanvas.MustNew(botTopRight), &widgetapi.Meta{}, widgetapi.Options{})
     700 + fakewidget.MustDraw(ft, testcanvas.MustNew(botBotLeft), &widgetapi.Meta{}, widgetapi.Options{})
     701 + fakewidget.MustDraw(ft, testcanvas.MustNew(botBotRight), &widgetapi.Meta{}, widgetapi.Options{})
    624 702   return ft
    625 703   },
    626 704   },
    skipped 16 lines
    643 721   
    644 722   left, right := mustVSplit(bot, 20)
    645 723   topRight, botRight := mustHSplit(right, 25)
    646  - fakewidget.MustDraw(ft, testcanvas.MustNew(top), widgetapi.Options{})
    647  - fakewidget.MustDraw(ft, testcanvas.MustNew(left), widgetapi.Options{})
    648  - fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
    649  - fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
     724 + fakewidget.MustDraw(ft, testcanvas.MustNew(top), &widgetapi.Meta{}, widgetapi.Options{})
     725 + fakewidget.MustDraw(ft, testcanvas.MustNew(left), &widgetapi.Meta{}, widgetapi.Options{})
     726 + fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), &widgetapi.Meta{}, widgetapi.Options{})
     727 + fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), &widgetapi.Meta{}, widgetapi.Options{})
    650 728   return ft
    651 729   },
    652 730   },
    skipped 17 lines
    670 748   want: func(size image.Point) *faketerm.Terminal {
    671 749   ft := faketerm.MustNew(size)
    672 750   top, bot := mustHSplit(ft.Area(), 50)
    673  - fakewidget.MustDraw(ft, testcanvas.MustNew(bot), widgetapi.Options{})
     751 + fakewidget.MustDraw(ft, testcanvas.MustNew(bot), &widgetapi.Meta{}, widgetapi.Options{})
    674 752   
    675 753   topTop, topBot := mustHSplit(top, 20)
    676 754   left, right := mustVSplit(topBot, 20)
    677 755   topRight, botRight := mustHSplit(right, 25)
    678  - fakewidget.MustDraw(ft, testcanvas.MustNew(topTop), widgetapi.Options{})
    679  - fakewidget.MustDraw(ft, testcanvas.MustNew(left), widgetapi.Options{})
    680  - fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
    681  - fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
     756 + fakewidget.MustDraw(ft, testcanvas.MustNew(topTop), &widgetapi.Meta{}, widgetapi.Options{})
     757 + fakewidget.MustDraw(ft, testcanvas.MustNew(left), &widgetapi.Meta{}, widgetapi.Options{})
     758 + fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), &widgetapi.Meta{}, widgetapi.Options{})
     759 + fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), &widgetapi.Meta{}, widgetapi.Options{})
    682 760   return ft
    683 761   },
    684 762   },
    skipped 20 lines
    705 783   draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
    706 784   )
    707 785   wCvs := testcanvas.MustNew(area.ExcludeBorder(cvs.Area()))
    708  - fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
     786 + fakewidget.MustDraw(ft, wCvs, &widgetapi.Meta{Focused: true}, widgetapi.Options{})
    709 787   testcanvas.MustCopyTo(wCvs, cvs)
    710 788   testcanvas.MustApply(cvs, ft)
    711 789   return ft
    skipped 44 lines
  • doc/images/termdashdemo_0_7_0.gif
  • doc/images/termdashdemo_0_9_0.gif
  • doc/images/textinputdemo.gif
  • ■ ■ ■ ■ ■ ■
    doc/widget_development.md
    skipped 47 lines
    48 48  A widget can draw a character indicating that a resize is needed in such cases:
    49 49   
    50 50  ```go
    51  -func (w *Widget) Draw(cvs *canvas.Canvas) error {
     51 +func (w *Widget) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    52 52   min := w.minSize() // Output depends on the current state.
    53 53   needAr, err := area.FromSize(min)
    54 54   if err != nil {
    skipped 29 lines
    84 84   tests := []struct {
    85 85   desc string
    86 86   canvas image.Rectangle
     87 + meta *widgetapi.Meta
    87 88   opts []Option
    88 89   want func(size image.Point) *faketerm.Terminal
    89 90   wantErr bool
    90 91   }{
    91 92   {
    92 93   desc: "a test case",
     94 + // The metadata widget receives when drawn.
     95 + meta: &widgetapi.Meta{},
    93 96   // canvas determines the size of the allocated canvas in the test case.
    94 97   canvas: image.Rect(0,0,10,10),
    95 98   // want creates the expected content on the fake terminal.
    skipped 16 lines
    112 115   }
    113 116   
    114 117   widget := New()
    115  - err = widget.Draw(c)
     118 + err = widget.Draw(c, tc.meta)
    116 119   if (err != nil) != tc.wantErr {
    117 120   t.Errorf("Draw => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    118 121   }
    skipped 27 lines
  • ■ ■ ■ ■ ■ ■
    internal/area/area.go
    skipped 77 lines
    78 78   return left, right, nil
    79 79  }
    80 80   
     81 +// VSplitCells returns two new areas created by splitting the provided area
     82 +// after the specified amount of cells of its width. The number of cells must
     83 +// be a zero or a positive integer. Providing a zero returns left=image.ZR,
     84 +// right=area. Providing a number equal or larger to area's width returns
     85 +// left=area, right=image.ZR.
     86 +func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) {
     87 + if min := 0; cells < min {
     88 + return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
     89 + }
     90 + if cells == 0 {
     91 + return image.ZR, area, nil
     92 + }
     93 + 
     94 + width := area.Dx()
     95 + if cells >= width {
     96 + return area, image.ZR, nil
     97 + }
     98 + 
     99 + left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+cells, area.Max.Y)
     100 + right = image.Rect(area.Min.X+cells, area.Min.Y, area.Max.X, area.Max.Y)
     101 + return left, right, nil
     102 +}
     103 + 
     104 +// HSplitCells returns two new areas created by splitting the provided area
     105 +// after the specified amount of cells of its height. The number of cells must
     106 +// be a zero or a positive integer. Providing a zero returns top=image.ZR,
     107 +// bottom=area. Providing a number equal or larger to area's height returns
     108 +// top=area, bottom=image.ZR.
     109 +func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) {
     110 + if min := 0; cells < min {
     111 + return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
     112 + }
     113 + if cells == 0 {
     114 + return image.ZR, area, nil
     115 + }
     116 + 
     117 + height := area.Dy()
     118 + if cells >= height {
     119 + return area, image.ZR, nil
     120 + }
     121 + 
     122 + top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+cells)
     123 + bottom = image.Rect(area.Min.X, area.Min.Y+cells, area.Max.X, area.Max.Y)
     124 + return top, bottom, nil
     125 +}
     126 + 
    81 127  // ExcludeBorder returns a new area created by subtracting a border around the
    82 128  // provided area. Return the zero area if there isn't enough space to exclude
    83 129  // the border.
    skipped 98 lines
  • ■ ■ ■ ■ ■ ■
    internal/area/area_test.go
    skipped 281 lines
    282 282   }
    283 283  }
    284 284   
     285 +func TestVSplitCells(t *testing.T) {
     286 + tests := []struct {
     287 + desc string
     288 + area image.Rectangle
     289 + cells int
     290 + wantLeft image.Rectangle
     291 + wantRight image.Rectangle
     292 + wantErr bool
     293 + }{
     294 + {
     295 + desc: "fails on negative cells",
     296 + area: image.Rect(1, 1, 2, 2),
     297 + cells: -1,
     298 + wantErr: true,
     299 + },
     300 + {
     301 + desc: "returns area as left on cells too large",
     302 + area: image.Rect(1, 1, 2, 2),
     303 + cells: 2,
     304 + wantLeft: image.Rect(1, 1, 2, 2),
     305 + wantRight: image.ZR,
     306 + },
     307 + {
     308 + desc: "returns area as left on cells equal area width",
     309 + area: image.Rect(1, 1, 2, 2),
     310 + cells: 1,
     311 + wantLeft: image.Rect(1, 1, 2, 2),
     312 + wantRight: image.ZR,
     313 + },
     314 + {
     315 + desc: "returns area as right on zero cells",
     316 + area: image.Rect(1, 1, 2, 2),
     317 + cells: 0,
     318 + wantRight: image.Rect(1, 1, 2, 2),
     319 + wantLeft: image.ZR,
     320 + },
     321 + {
     322 + desc: "zero area to begin with",
     323 + area: image.ZR,
     324 + cells: 0,
     325 + wantLeft: image.ZR,
     326 + wantRight: image.ZR,
     327 + },
     328 + {
     329 + desc: "splits area with even width",
     330 + area: image.Rect(1, 1, 3, 3),
     331 + cells: 1,
     332 + wantLeft: image.Rect(1, 1, 2, 3),
     333 + wantRight: image.Rect(2, 1, 3, 3),
     334 + },
     335 + {
     336 + desc: "splits area with odd width",
     337 + area: image.Rect(1, 1, 4, 4),
     338 + cells: 1,
     339 + wantLeft: image.Rect(1, 1, 2, 4),
     340 + wantRight: image.Rect(2, 1, 4, 4),
     341 + },
     342 + {
     343 + desc: "splits to unequal areas",
     344 + area: image.Rect(0, 0, 4, 4),
     345 + cells: 3,
     346 + wantLeft: image.Rect(0, 0, 3, 4),
     347 + wantRight: image.Rect(3, 0, 4, 4),
     348 + },
     349 + }
     350 + 
     351 + for _, tc := range tests {
     352 + t.Run(tc.desc, func(t *testing.T) {
     353 + gotLeft, gotRight, err := VSplitCells(tc.area, tc.cells)
     354 + if (err != nil) != tc.wantErr {
     355 + t.Errorf("VSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr)
     356 + }
     357 + if err != nil {
     358 + return
     359 + }
     360 + if diff := pretty.Compare(tc.wantLeft, gotLeft); diff != "" {
     361 + t.Errorf("VSplitCells => left value unexpected diff (-want, +got):\n%s", diff)
     362 + }
     363 + if diff := pretty.Compare(tc.wantRight, gotRight); diff != "" {
     364 + t.Errorf("VSplitCells => right value unexpected diff (-want, +got):\n%s", diff)
     365 + }
     366 + })
     367 + }
     368 +}
     369 + 
     370 +func TestHSplitCells(t *testing.T) {
     371 + tests := []struct {
     372 + desc string
     373 + area image.Rectangle
     374 + cells int
     375 + wantTop image.Rectangle
     376 + wantBottom image.Rectangle
     377 + wantErr bool
     378 + }{
     379 + {
     380 + desc: "fails on negative cells",
     381 + area: image.Rect(1, 1, 2, 2),
     382 + cells: -1,
     383 + wantErr: true,
     384 + },
     385 + {
     386 + desc: "returns area as top on cells too large",
     387 + area: image.Rect(1, 1, 2, 2),
     388 + cells: 2,
     389 + wantTop: image.Rect(1, 1, 2, 2),
     390 + wantBottom: image.ZR,
     391 + },
     392 + {
     393 + desc: "returns area as top on cells equal area width",
     394 + area: image.Rect(1, 1, 2, 2),
     395 + cells: 1,
     396 + wantTop: image.Rect(1, 1, 2, 2),
     397 + wantBottom: image.ZR,
     398 + },
     399 + {
     400 + desc: "returns area as bottom on zero cells",
     401 + area: image.Rect(1, 1, 2, 2),
     402 + cells: 0,
     403 + wantBottom: image.Rect(1, 1, 2, 2),
     404 + wantTop: image.ZR,
     405 + },
     406 + {
     407 + desc: "zero area to begin with",
     408 + area: image.ZR,
     409 + cells: 0,
     410 + wantTop: image.ZR,
     411 + wantBottom: image.ZR,
     412 + },
     413 + {
     414 + desc: "splits area with even height",
     415 + area: image.Rect(1, 1, 3, 3),
     416 + cells: 1,
     417 + wantTop: image.Rect(1, 1, 3, 2),
     418 + wantBottom: image.Rect(1, 2, 3, 3),
     419 + },
     420 + {
     421 + desc: "splits area with odd width",
     422 + area: image.Rect(1, 1, 4, 4),
     423 + cells: 1,
     424 + wantTop: image.Rect(1, 1, 4, 2),
     425 + wantBottom: image.Rect(1, 2, 4, 4),
     426 + },
     427 + {
     428 + desc: "splits to unequal areas",
     429 + area: image.Rect(0, 0, 4, 4),
     430 + cells: 3,
     431 + wantTop: image.Rect(0, 0, 4, 3),
     432 + wantBottom: image.Rect(0, 3, 4, 4),
     433 + },
     434 + }
     435 + 
     436 + for _, tc := range tests {
     437 + t.Run(tc.desc, func(t *testing.T) {
     438 + gotTop, gotBottom, err := HSplitCells(tc.area, tc.cells)
     439 + if (err != nil) != tc.wantErr {
     440 + t.Errorf("HSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr)
     441 + }
     442 + if err != nil {
     443 + return
     444 + }
     445 + if diff := pretty.Compare(tc.wantTop, gotTop); diff != "" {
     446 + t.Errorf("HSplitCells => left value unexpected diff (-want, +got):\n%s", diff)
     447 + }
     448 + if diff := pretty.Compare(tc.wantBottom, gotBottom); diff != "" {
     449 + t.Errorf("HSplitCells => right value unexpected diff (-want, +got):\n%s", diff)
     450 + }
     451 + })
     452 + }
     453 +}
     454 + 
    285 455  func TestExcludeBorder(t *testing.T) {
    286 456   tests := []struct {
    287 457   desc string
    skipped 417 lines
  • ■ ■ ■ ■ ■
    internal/canvas/buffer/buffer.go
    skipped 114 lines
    115 115   return -1, err
    116 116   }
    117 117   rw := runewidth.RuneWidth(r)
     118 + if rw == 0 {
     119 + // Even if the rune is invisible, like the zero-value rune, it still
     120 + // occupies at least the target cell.
     121 + rw = 1
     122 + }
    118 123   if rw > remW {
    119 124   return -1, fmt.Errorf("cannot set rune %q of width %d at point %v, only have %d remaining cells at this line", r, rw, p, remW)
    120 125   }
    skipped 64 lines
  • ■ ■ ■ ■ ■ ■
    internal/canvas/buffer/buffer_test.go
    skipped 411 lines
    412 412   }(),
    413 413   },
    414 414   {
     415 + desc: "sets zero-value rune in a cell",
     416 + buffer: mustNew(image.Point{3, 3}),
     417 + point: image.Point{1, 2},
     418 + r: 0,
     419 + wantCells: 1,
     420 + want: func() Buffer {
     421 + b := mustNew(size)
     422 + b[1][2].Rune = 0
     423 + return b
     424 + }(),
     425 + },
     426 + {
    415 427   desc: "sets cell options",
    416 428   buffer: mustNew(image.Point{3, 3}),
    417 429   point: image.Point{1, 2},
    skipped 211 lines
  • ■ ■ ■ ■ ■ ■
    internal/draw/text.go
    skipped 16 lines
    17 17  // text.go contains code that prints UTF-8 encoded strings on the canvas.
    18 18   
    19 19  import (
    20  - "bytes"
    21 20   "fmt"
    22 21   "image"
     22 + "strings"
    23 23   
    24 24   "github.com/mum4k/termdash/cell"
    25 25   "github.com/mum4k/termdash/internal/canvas"
    skipped 98 lines
    124 124   return "", fmt.Errorf("unsupported overrun mode %d", om)
    125 125   }
    126 126   
    127  - var b bytes.Buffer
     127 + var b strings.Builder
    128 128   cur := 0
    129 129   for _, r := range text {
    130 130   rw := runewidth.RuneWidth(r)
    skipped 66 lines
  • ■ ■ ■ ■ ■ ■
    internal/faketerm/diff.go
    skipped 16 lines
    17 17  // diff.go provides functions that highlight differences between fake terminals.
    18 18   
    19 19  import (
    20  - "bytes"
    21 20   "fmt"
    22 21   "image"
    23 22   "reflect"
     23 + "strings"
    24 24   
    25 25   "github.com/kylelemons/godebug/pretty"
    26 26   "github.com/mum4k/termdash/cell"
    skipped 16 lines
    43 43   return ""
    44 44   }
    45 45   
    46  - var b bytes.Buffer
     46 + var b strings.Builder
    47 47   b.WriteString("found differences between the two fake terminals.\n")
    48 48   b.WriteString(" got:\n")
    49 49   b.WriteString(got.String())
    skipped 60 lines
  • ■ ■ ■ ■ ■ ■
    internal/faketerm/faketerm.go
    skipped 15 lines
    16 16  package faketerm
    17 17   
    18 18  import (
    19  - "bytes"
    20 19   "context"
    21 20   "fmt"
    22 21   "image"
    23 22   "log"
     23 + "strings"
    24 24   "sync"
    25 25   
    26 26   "github.com/mum4k/termdash/cell"
    skipped 91 lines
    118 118  // Implements fmt.Stringer.
    119 119  func (t *Terminal) String() string {
    120 120   size := t.Size()
    121  - var b bytes.Buffer
     121 + var b strings.Builder
    122 122   for row := 0; row < size.Y; row++ {
    123 123   for col := 0; col < size.X; col++ {
    124 124   r := t.buffer[col][row].Rune
    skipped 88 lines
  • ■ ■ ■ ■ ■ ■
    internal/fakewidget/fakewidget.go
    skipped 30 lines
    31 31  )
    32 32   
    33 33  // outputLines are the number of lines written by this plugin.
    34  -const outputLines = 3
     34 +const outputLines = 4
    35 35   
    36 36  const (
    37 37   sizeLine = iota
    38 38   keyboardLine
    39 39   mouseLine
     40 + focusLine
    40 41  )
    41 42   
    42 43  // MinimumSize is the minimum size required to draw this widget.
    skipped 4 lines
    47 48  // canvas. It writes the last received keyboard event onto the second line. It
    48 49  // writes the last received mouse event onto the third line. If a non-empty
    49 50  // string is provided via the Text() method, that text will be written right
    50  -// after the canvas size on the first line.
     51 +// after the canvas size on the first line. If the widget's container is
     52 +// focused it writes "focus" onto the fourth line.
    51 53  //
    52 54  // The widget requests the same options that are provided to the constructor.
    53  -// If the options or canvas size don't allow for the three lines mentioned
    54  -// above, the widget skips the ones it has no space for.
     55 +// If the options or canvas size don't allow for the lines mentioned above, the
     56 +// widget skips the ones it has no space for.
    55 57  //
    56 58  // This is thread-safe and must not be copied.
    57 59  // Implements widgetapi.Widget.
    58 60  type Mirror struct {
    59  - // lines are the three lines that will be drawn on the canvas.
     61 + // lines are the lines that will be drawn on the canvas.
    60 62   lines []string
    61 63   
    62 64   // text is the text provided by the last call to Text().
    skipped 20 lines
    83 85  // 2x2 border on it, or of any of the text lines end up being longer than the
    84 86  // width of the canvas.
    85 87  // Draw implements widgetapi.Widget.Draw.
    86  -func (mi *Mirror) Draw(cvs *canvas.Canvas) error {
     88 +func (mi *Mirror) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    87 89   mi.mu.Lock()
    88 90   defer mi.mu.Unlock()
     91 + if meta.Focused {
     92 + mi.lines[focusLine] = "focus"
     93 + }
     94 + 
    89 95   if err := cvs.Clear(); err != nil {
    90 96   return err
    91 97   }
    skipped 64 lines
    156 162   
    157 163  // Draw draws the content that would be expected after placing the Mirror
    158 164  // widget onto the provided canvas and forwarding the given events.
    159  -func Draw(t terminalapi.Terminal, cvs *canvas.Canvas, opts widgetapi.Options, events ...terminalapi.Event) error {
     165 +func Draw(t terminalapi.Terminal, cvs *canvas.Canvas, meta *widgetapi.Meta, opts widgetapi.Options, events ...terminalapi.Event) error {
    160 166   mirror := New(opts)
    161  - return DrawWithMirror(mirror, t, cvs, events...)
     167 + return DrawWithMirror(mirror, t, cvs, meta, events...)
    162 168  }
    163 169   
    164 170  // MustDraw is like Draw, but panics on all errors.
    165  -func MustDraw(t terminalapi.Terminal, cvs *canvas.Canvas, opts widgetapi.Options, events ...terminalapi.Event) {
    166  - if err := Draw(t, cvs, opts, events...); err != nil {
     171 +func MustDraw(t terminalapi.Terminal, cvs *canvas.Canvas, meta *widgetapi.Meta, opts widgetapi.Options, events ...terminalapi.Event) {
     172 + if err := Draw(t, cvs, meta, opts, events...); err != nil {
    167 173   panic(fmt.Sprintf("Draw => %v", err))
    168 174   }
    169 175  }
    170 176   
    171 177  // DrawWithMirror is like Draw, but uses the provided Mirror instead of creating one.
    172  -func DrawWithMirror(mirror *Mirror, t terminalapi.Terminal, cvs *canvas.Canvas, events ...terminalapi.Event) error {
     178 +func DrawWithMirror(mirror *Mirror, t terminalapi.Terminal, cvs *canvas.Canvas, meta *widgetapi.Meta, events ...terminalapi.Event) error {
    173 179   for _, ev := range events {
    174 180   switch e := ev.(type) {
    175 181   case *terminalapi.Mouse:
    skipped 15 lines
    191 197   }
    192 198   }
    193 199   
    194  - if err := mirror.Draw(cvs); err != nil {
     200 + if err := mirror.Draw(cvs, meta); err != nil {
    195 201   return err
    196 202   }
    197 203   return cvs.Apply(t)
    198 204  }
    199 205   
    200 206  // MustDrawWithMirror is like DrawWithMirror, but panics on all errors.
    201  -func MustDrawWithMirror(mirror *Mirror, t terminalapi.Terminal, cvs *canvas.Canvas, events ...terminalapi.Event) {
    202  - if err := DrawWithMirror(mirror, t, cvs, events...); err != nil {
     207 +func MustDrawWithMirror(mirror *Mirror, t terminalapi.Terminal, cvs *canvas.Canvas, meta *widgetapi.Meta, events ...terminalapi.Event) {
     208 + if err := DrawWithMirror(mirror, t, cvs, meta, events...); err != nil {
    203 209   panic(fmt.Sprintf("DrawWithMirror => %v", err))
    204 210   }
    205 211  }
    skipped 1 lines
  • ■ ■ ■ ■ ■
    internal/fakewidget/fakewidget_test.go
    skipped 47 lines
    48 48   mouseEvents []mouseEvents // Mouse events to send before calling Draw().
    49 49   apiEvents func(*Mirror) // External events via the widget's API.
    50 50   cvs *canvas.Canvas
     51 + meta *widgetapi.Meta
    51 52   want func(size image.Point) *faketerm.Terminal
    52 53   wantErr bool
    53 54   }{
    54 55   {
    55 56   desc: "canvas too small to draw a box",
    56 57   cvs: testcanvas.MustNew(image.Rect(0, 0, 1, 1)),
     58 + meta: &widgetapi.Meta{},
    57 59   want: func(size image.Point) *faketerm.Terminal {
    58 60   return faketerm.MustNew(size)
    59 61   },
    skipped 2 lines
    62 64   {
    63 65   desc: "the canvas size text doesn't fit onto the line",
    64 66   cvs: testcanvas.MustNew(image.Rect(0, 0, 3, 3)),
     67 + meta: &widgetapi.Meta{},
    65 68   want: func(size image.Point) *faketerm.Terminal {
    66 69   return faketerm.MustNew(size)
    67 70   },
    skipped 2 lines
    70 73   {
    71 74   desc: "draws the box and canvas size",
    72 75   cvs: testcanvas.MustNew(image.Rect(0, 0, 7, 3)),
     76 + meta: &widgetapi.Meta{},
    73 77   want: func(size image.Point) *faketerm.Terminal {
    74 78   ft := faketerm.MustNew(size)
    75 79   cvs := testcanvas.MustNew(ft.Area())
    skipped 4 lines
    80 84   },
    81 85   },
    82 86   {
     87 + desc: "indicates that it is focused",
     88 + cvs: testcanvas.MustNew(image.Rect(0, 0, 7, 6)),
     89 + meta: &widgetapi.Meta{
     90 + Focused: true,
     91 + },
     92 + want: func(size image.Point) *faketerm.Terminal {
     93 + ft := faketerm.MustNew(size)
     94 + cvs := testcanvas.MustNew(ft.Area())
     95 + testdraw.MustBorder(cvs, cvs.Area())
     96 + testdraw.MustText(cvs, "(7,6)", image.Point{1, 1})
     97 + testdraw.MustText(cvs, "focus", image.Point{1, 4})
     98 + testcanvas.MustApply(cvs, ft)
     99 + return ft
     100 + },
     101 + },
     102 + {
    83 103   desc: "draws the box, canvas size and custom text",
    84 104   apiEvents: func(mi *Mirror) {
    85 105   mi.Text("hi")
    86 106   },
    87  - cvs: testcanvas.MustNew(image.Rect(0, 0, 9, 3)),
     107 + cvs: testcanvas.MustNew(image.Rect(0, 0, 9, 3)),
     108 + meta: &widgetapi.Meta{},
    88 109   want: func(size image.Point) *faketerm.Terminal {
    89 110   ft := faketerm.MustNew(size)
    90 111   cvs := testcanvas.MustNew(ft.Area())
    skipped 6 lines
    97 118   {
    98 119   desc: "skips canvas size if there isn't a line for it",
    99 120   cvs: testcanvas.MustNew(image.Rect(0, 0, 3, 2)),
     121 + meta: &widgetapi.Meta{},
    100 122   want: func(size image.Point) *faketerm.Terminal {
    101 123   ft := faketerm.MustNew(size)
    102 124   cvs := testcanvas.MustNew(ft.Area())
    skipped 12 lines
    115 137   k: &terminalapi.Keyboard{Key: keyboard.KeyEnd},
    116 138   },
    117 139   },
    118  - cvs: testcanvas.MustNew(image.Rect(0, 0, 8, 4)),
     140 + cvs: testcanvas.MustNew(image.Rect(0, 0, 8, 4)),
     141 + meta: &widgetapi.Meta{},
    119 142   want: func(size image.Point) *faketerm.Terminal {
    120 143   ft := faketerm.MustNew(size)
    121 144   cvs := testcanvas.MustNew(ft.Area())
    skipped 11 lines
    133 156   k: &terminalapi.Keyboard{Key: keyboard.KeyEnd},
    134 157   },
    135 158   },
    136  - cvs: testcanvas.MustNew(image.Rect(0, 0, 8, 3)),
     159 + cvs: testcanvas.MustNew(image.Rect(0, 0, 8, 3)),
     160 + meta: &widgetapi.Meta{},
    137 161   want: func(size image.Point) *faketerm.Terminal {
    138 162   ft := faketerm.MustNew(size)
    139 163   cvs := testcanvas.MustNew(ft.Area())
    skipped 15 lines
    155 179   Button: mouse.ButtonMiddle},
    156 180   },
    157 181   },
    158  - cvs: testcanvas.MustNew(image.Rect(0, 0, 19, 5)),
     182 + cvs: testcanvas.MustNew(image.Rect(0, 0, 19, 5)),
     183 + meta: &widgetapi.Meta{},
    159 184   want: func(size image.Point) *faketerm.Terminal {
    160 185   ft := faketerm.MustNew(size)
    161 186   cvs := testcanvas.MustNew(ft.Area())
    skipped 11 lines
    173 198   m: &terminalapi.Mouse{Button: mouse.ButtonLeft},
    174 199   },
    175 200   },
    176  - cvs: testcanvas.MustNew(image.Rect(0, 0, 13, 4)),
     201 + cvs: testcanvas.MustNew(image.Rect(0, 0, 13, 4)),
     202 + meta: &widgetapi.Meta{},
    177 203   want: func(size image.Point) *faketerm.Terminal {
    178 204   ft := faketerm.MustNew(size)
    179 205   cvs := testcanvas.MustNew(ft.Area())
    skipped 15 lines
    195 221   m: &terminalapi.Mouse{Button: mouse.ButtonLeft},
    196 222   },
    197 223   },
    198  - cvs: testcanvas.MustNew(image.Rect(0, 0, 17, 5)),
     224 + cvs: testcanvas.MustNew(image.Rect(0, 0, 17, 5)),
     225 + meta: &widgetapi.Meta{},
    199 226   want: func(size image.Point) *faketerm.Terminal {
    200 227   ft := faketerm.MustNew(size)
    201 228   cvs := testcanvas.MustNew(ft.Area())
    skipped 25 lines
    227 254   wantErr: true,
    228 255   },
    229 256   },
    230  - cvs: testcanvas.MustNew(image.Rect(0, 0, 12, 5)),
     257 + cvs: testcanvas.MustNew(image.Rect(0, 0, 12, 5)),
     258 + meta: &widgetapi.Meta{},
    231 259   want: func(size image.Point) *faketerm.Terminal {
    232 260   ft := faketerm.MustNew(size)
    233 261   cvs := testcanvas.MustNew(ft.Area())
    skipped 27 lines
    261 289   }
    262 290   }
    263 291   
    264  - err := w.Draw(tc.cvs)
     292 + err := w.Draw(tc.cvs, tc.meta)
    265 293   if (err != nil) != tc.wantErr {
    266 294   t.Errorf("Draw => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    267 295   }
    skipped 28 lines
    296 324   desc string
    297 325   opts widgetapi.Options
    298 326   cvs *canvas.Canvas
     327 + meta *widgetapi.Meta
    299 328   events []terminalapi.Event
    300 329   want func(size image.Point) *faketerm.Terminal
    301 330   wantErr bool
    skipped 1 lines
    303 332   {
    304 333   desc: "canvas too small to draw a box",
    305 334   cvs: testcanvas.MustNew(image.Rect(0, 0, 1, 1)),
     335 + meta: &widgetapi.Meta{},
    306 336   want: func(size image.Point) *faketerm.Terminal {
    307 337   return faketerm.MustNew(size)
    308 338   },
    skipped 2 lines
    311 341   {
    312 342   desc: "draws the box and canvas size",
    313 343   cvs: testcanvas.MustNew(image.Rect(0, 0, 9, 3)),
     344 + meta: &widgetapi.Meta{},
    314 345   want: func(size image.Point) *faketerm.Terminal {
    315 346   ft := faketerm.MustNew(size)
    316 347   cvs := testcanvas.MustNew(ft.Area())
    skipped 9 lines
    326 357   WantKeyboard: widgetapi.KeyScopeFocused,
    327 358   WantMouse: widgetapi.MouseScopeWidget,
    328 359   },
    329  - cvs: testcanvas.MustNew(image.Rect(0, 0, 17, 5)),
     360 + cvs: testcanvas.MustNew(image.Rect(0, 0, 17, 5)),
     361 + meta: &widgetapi.Meta{},
    330 362   events: []terminalapi.Event{
    331 363   &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    332 364   &terminalapi.Mouse{Button: mouse.ButtonLeft},
    skipped 14 lines
    347 379   for _, tc := range tests {
    348 380   t.Run(tc.desc, func(t *testing.T) {
    349 381   got := faketerm.MustNew(tc.cvs.Size())
    350  - err := Draw(got, tc.cvs, tc.opts, tc.events...)
     382 + err := Draw(got, tc.cvs, tc.meta, tc.opts, tc.events...)
    351 383   if (err != nil) != tc.wantErr {
    352 384   t.Errorf("Draw => got error:%v, wantErr: %v", err, tc.wantErr)
    353 385   }
    skipped 8 lines
  • ■ ■ ■ ■ ■ ■
    internal/numbers/numbers.go
    skipped 95 lines
    96 96   return (f - floor) * sign
    97 97  }
    98 98   
    99  -// Round returns the nearest integer, rounding half away from zero.
    100  -// Copied from the math package of Go 1.10 for backwards compatibility with Go
    101  -// 1.8 where the math.Round function doesn't exist yet.
    102  -func Round(x float64) float64 {
    103  - t := math.Trunc(x)
    104  - if math.Abs(x-t) >= 0.5 {
    105  - return t + math.Copysign(1, x)
    106  - }
    107  - return t
    108  -}
    109  - 
    110 99  // MinMax returns the smallest and the largest value among the provided values.
    111 100  // Returns (0, 0) if there are no values.
     101 +// Ignores NaN values. Allowing NaN values could lead to a corner case where all
     102 +// values can be NaN, in this case the function will return NaN as min and max.
    112 103  func MinMax(values []float64) (min, max float64) {
    113 104   if len(values) == 0 {
    114 105   return 0, 0
    115 106   }
    116 107   min = math.MaxFloat64
    117 108   max = -1 * math.MaxFloat64
     109 + allNaN := true
     110 + for _, v := range values {
     111 + if math.IsNaN(v) {
     112 + continue
     113 + }
     114 + allNaN = false
    118 115   
    119  - for _, v := range values {
    120 116   if v < min {
    121 117   min = v
    122 118   }
    skipped 1 lines
    124 120   max = v
    125 121   }
    126 122   }
     123 + 
     124 + if allNaN {
     125 + return math.NaN(), math.NaN()
     126 + }
     127 + 
    127 128   return min, max
    128 129  }
    129 130   
    skipped 27 lines
    157 158   
    158 159  // RadiansToDegrees converts radians to the equivalent in degrees.
    159 160  func RadiansToDegrees(radians float64) int {
    160  - d := int(Round(radians * 180 / math.Pi))
     161 + d := int(math.Round(radians * 180 / math.Pi))
    161 162   if d < 0 {
    162 163   d += 360
    163 164   }
    skipped 51 lines
    215 216   sum := float64(sr.X + sr.Y)
    216 217   fact := fn / sum
    217 218   return image.Point{
    218  - int(Round(fact * float64(sr.X))),
    219  - int(Round(fact * float64(sr.Y))),
     219 + int(math.Round(fact * float64(sr.X))),
     220 + int(math.Round(fact * float64(sr.Y))),
    220 221   }
    221 222  }
    222 223   
  • ■ ■ ■ ■ ■
    internal/numbers/numbers_test.go
    skipped 119 lines
    120 120   return false
    121 121  }
    122 122   
    123  -var round = []float64{
    124  - 5,
    125  - 8,
    126  - math.Copysign(0, -1),
    127  - -5,
    128  - 10,
    129  - 3,
    130  - 5,
    131  - 3,
    132  - 2,
    133  - -9,
    134  -}
    135  - 
    136  -var vf = []float64{
    137  - 4.9790119248836735e+00,
    138  - 7.7388724745781045e+00,
    139  - -2.7688005719200159e-01,
    140  - -5.0106036182710749e+00,
    141  - 9.6362937071984173e+00,
    142  - 2.9263772392439646e+00,
    143  - 5.2290834314593066e+00,
    144  - 2.7279399104360102e+00,
    145  - 1.8253080916808550e+00,
    146  - -8.6859247685756013e+00,
    147  -}
    148  - 
    149  -var vfroundSC = [][2]float64{
    150  - {0, 0},
    151  - {1.390671161567e-309, 0}, // denormal
    152  - {0.49999999999999994, 0}, // 0.5-epsilon
    153  - {0.5, 1},
    154  - {0.5000000000000001, 1}, // 0.5+epsilon
    155  - {-1.5, -2},
    156  - {-2.5, -3},
    157  - {math.NaN(), math.NaN()},
    158  - {math.Inf(1), math.Inf(1)},
    159  - {2251799813685249.5, 2251799813685250}, // 1 bit fraction
    160  - {2251799813685250.5, 2251799813685251},
    161  - {4503599627370495.5, 4503599627370496}, // 1 bit fraction, rounding to 0 bit fraction
    162  - {4503599627370497, 4503599627370497}, // large integer
    163  -}
    164  - 
    165  -func TestRound(t *testing.T) {
    166  - for i := 0; i < len(vf); i++ {
    167  - if f := Round(vf[i]); !alike(round[i], f) {
    168  - t.Errorf("Round(%g) = %g, want %g", vf[i], f, round[i])
    169  - }
    170  - }
    171  - for i := 0; i < len(vfroundSC); i++ {
    172  - if f := Round(vfroundSC[i][0]); !alike(vfroundSC[i][1], f) {
    173  - t.Errorf("Round(%g) = %g, want %g", vfroundSC[i][0], f, vfroundSC[i][1])
    174  - }
    175  - }
    176  -}
    177  - 
    178 123  func TestMinMax(t *testing.T) {
    179 124   tests := []struct {
    180 125   desc string
    skipped 34 lines
    215 160   wantMin: -11.3,
    216 161   wantMax: 22.5,
    217 162   },
     163 + {
     164 + desc: "min and max among negative, positive, zero and NaN values",
     165 + values: []float64{1.1, 0, 1.3, math.NaN(), -11.3, 22.5},
     166 + wantMin: -11.3,
     167 + wantMax: 22.5,
     168 + },
     169 + {
     170 + desc: "all NaN values",
     171 + values: []float64{math.NaN(), math.NaN(), math.NaN(), math.NaN()},
     172 + wantMin: math.NaN(),
     173 + wantMax: math.NaN(),
     174 + },
    218 175   }
    219 176   
    220 177   for _, tc := range tests {
    221 178   t.Run(tc.desc, func(t *testing.T) {
    222 179   gotMin, gotMax := MinMax(tc.values)
    223  - if gotMin != tc.wantMin || gotMax != tc.wantMax {
    224  - t.Errorf("MinMax => (%v, %v), want (%v, %v)", gotMin, gotMax, tc.wantMin, tc.wantMax)
     180 + if diff := pretty.Compare(tc.wantMin, gotMin); diff != "" {
     181 + t.Errorf("MinMax => unexpected min, diff (-want, +got):\n %s", diff)
     182 + }
     183 + if diff := pretty.Compare(tc.wantMax, gotMax); diff != "" {
     184 + t.Errorf("MinMax => unexpected max, diff (-want, +got):\n %s", diff)
    225 185   }
    226 186   })
    227 187   }
    skipped 263 lines
  • ■ ■ ■ ■ ■ ■
    internal/numbers/trig/trig.go
    skipped 29 lines
    30 30  func CirclePointAtAngle(degrees int, mid image.Point, radius int) image.Point {
    31 31   angle := numbers.DegreesToRadians(degrees)
    32 32   r := float64(radius)
    33  - x := mid.X + int(numbers.Round(r*math.Cos(angle)))
     33 + x := mid.X + int(math.Round(r*math.Cos(angle)))
    34 34   // Y coordinates grow down on the canvas.
    35  - y := mid.Y - int(numbers.Round(r*math.Sin(angle)))
     35 + y := mid.Y - int(math.Round(r*math.Sin(angle)))
    36 36   return image.Point{x, y}
    37 37  }
    38 38   
    skipped 187 lines
  • ■ ■ ■ ■ ■ ■
    internal/runewidth/runewidth_test.go
    skipped 65 lines
    66 66   },
    67 67   {
    68 68   desc: "termdash special runes",
    69  - runes: []rune{'⇄', '…', '⇧', '⇩'},
     69 + runes: []rune{'⇄', '…', '⇧', '⇩', '', ''},
    70 70   want: 1,
    71 71   },
    72 72   {
    73 73   desc: "termdash special runes in eastAsian",
    74  - runes: []rune{'⇄', '…', '⇧', '⇩'},
     74 + runes: []rune{'⇄', '…', '⇧', '⇩', '', ''},
    75 75   eastAsian: true,
    76 76   want: 1,
    77 77   },
    skipped 90 lines
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/sixteen/attributes.go
    skipped 55 lines
    56 56  func segmentSize(ar image.Rectangle) int {
    57 57   // widthPerc is the relative width of a segment to the width of the canvas.
    58 58   const widthPerc = 9
    59  - s := int(numbers.Round(float64(ar.Dx()) * widthPerc / 100))
     59 + s := int(math.Round(float64(ar.Dx()) * widthPerc / 100))
    60 60   if s > 3 && s%2 == 0 {
    61 61   // Segments with odd number of pixels in their width/height look
    62 62   // better, since the spike at the top of their slopes has only one
    skipped 83 lines
    146 146   twoSegLeg := twoSegHypo / math.Sqrt2
    147 147   edgeSegGap := twoSegLeg - segPeakDist
    148 148   
    149  - spaces := int(numbers.Round(2*edgeSegGap + peakToPeak))
     149 + spaces := int(math.Round(2*edgeSegGap + peakToPeak))
    150 150   shortLen := (bcAr.Dx()-spaces)/2 - 1
    151 151   longLen := (bcAr.Dy()-spaces)/2 - 1
    152 152   
    153  - ptp := int(numbers.Round(peakToPeak))
    154  - horizLeftX := int(numbers.Round(edgeSegGap))
     153 + ptp := int(math.Round(peakToPeak))
     154 + horizLeftX := int(math.Round(edgeSegGap))
    155 155   
    156 156   // Refer to doc/segment_placement.svg.
    157 157   // Diagram labeled "A mid point".
    158  - offset := int(numbers.Round(diaLeg - segPeakDist))
     158 + offset := int(math.Round(diaLeg - segPeakDist))
    159 159   horizMidX := horizLeftX + shortLen + offset
    160 160   horizRightX := horizLeftX + shortLen + ptp + shortLen + offset
    161 161   
    skipped 130 lines
    292 292   const hvToDiaGapPerc = 30
    293 293   hvToDiaGap := a.diaGap * hvToDiaGapPerc / 100
    294 294   
    295  - startX := int(numbers.Round(float64(topAr.Min.X) + a.segPeakDist - a.diaLeg + hvToDiaGap))
    296  - startY := int(numbers.Round(float64(leftAr.Min.Y) + a.segPeakDist - a.diaLeg + hvToDiaGap))
    297  - endX := int(numbers.Round(float64(bottomAr.Max.X) - a.segPeakDist + a.diaLeg - hvToDiaGap))
    298  - endY := int(numbers.Round(float64(rightAr.Max.Y) - a.segPeakDist + a.diaLeg - hvToDiaGap))
     295 + startX := int(math.Round(float64(topAr.Min.X) + a.segPeakDist - a.diaLeg + hvToDiaGap))
     296 + startY := int(math.Round(float64(leftAr.Min.Y) + a.segPeakDist - a.diaLeg + hvToDiaGap))
     297 + endX := int(math.Round(float64(bottomAr.Max.X) - a.segPeakDist + a.diaLeg - hvToDiaGap))
     298 + endY := int(math.Round(float64(rightAr.Max.Y) - a.segPeakDist + a.diaLeg - hvToDiaGap))
    299 299   return image.Rect(startX, startY, endX, endY)
    300 300  }
    301 301   
  • ■ ■ ■ ■ ■ ■
    internal/segdisp/sixteen/sixteen.go
    skipped 39 lines
    40 40  package sixteen
    41 41   
    42 42  import (
    43  - "bytes"
    44 43   "fmt"
    45 44   "image"
    46 45   "math"
     46 + "strings"
    47 47   
    48 48   "github.com/mum4k/termdash/cell"
    49 49   "github.com/mum4k/termdash/internal/area"
    skipped 200 lines
    250 250  // Sanitize returns a copy of the string, replacing all unsupported characters
    251 251  // with a space character.
    252 252  func Sanitize(s string) string {
    253  - var b bytes.Buffer
     253 + var b strings.Builder
    254 254   for _, r := range s {
    255 255   if _, ok := characterSegments[r]; !ok {
    256 256   b.WriteRune(' ')
    skipped 234 lines
  • ■ ■ ■ ■ ■ ■
    internal/wrap/wrap.go
    skipped 15 lines
    16 16  package wrap
    17 17   
    18 18  import (
    19  - "bytes"
    20 19   "errors"
    21 20   "fmt"
     21 + "strings"
    22 22   "unicode"
    23 23   
    24 24   "github.com/mum4k/termdash/internal/canvas/buffer"
    skipped 58 lines
    83 83  // ValidCells validates the provided cells for wrapping.
    84 84  // The text in the cells must follow the same rules as described for ValidText.
    85 85  func ValidCells(cells []*buffer.Cell) error {
    86  - var b bytes.Buffer
     86 + var b strings.Builder
    87 87   for _, c := range cells {
    88 88   b.WriteRune(c.Rune)
    89 89   }
    skipped 118 lines
    208 208  // wordWidth returns the width of the current word in cells when printed on the
    209 209  // terminal.
    210 210  func (cs *cellScanner) wordWidth() int {
    211  - var b bytes.Buffer
     211 + var b strings.Builder
    212 212   for _, wc := range cs.wordCells() {
    213 213   b.WriteRune(wc.Rune)
    214 214   }
    skipped 196 lines
  • ■ ■ ■ ■ ■ ■
    internal/wrap/wrap_test.go
    skipped 14 lines
    15 15  package wrap
    16 16   
    17 17  import (
    18  - "bytes"
    19 18   "fmt"
     19 + "strings"
    20 20   "testing"
    21 21   "unicode"
    22 22   
    skipped 19 lines
    42 42   {
    43 43   desc: "all printable ASCII characters are allowed",
    44 44   text: func() string {
    45  - var b bytes.Buffer
     45 + var b strings.Builder
    46 46   for i := 0; i < unicode.MaxASCII; i++ {
    47 47   r := rune(i)
    48 48   if unicode.IsPrint(r) {
    skipped 6 lines
    55 55   {
    56 56   desc: "all printable Unicode characters in the Latin-1 space are allowed",
    57 57   text: func() string {
    58  - var b bytes.Buffer
     58 + var b strings.Builder
    59 59   for i := 0; i < unicode.MaxLatin1; i++ {
    60 60   r := rune(i)
    61 61   if unicode.IsPrint(r) {
    skipped 683 lines
  • ■ ■ ■ ■ ■ ■
    keyboard/keyboard.go
    skipped 54 lines
    55 55   KeyArrowDown: "KeyArrowDown",
    56 56   KeyArrowLeft: "KeyArrowLeft",
    57 57   KeyArrowRight: "KeyArrowRight",
     58 + KeyCtrlTilde: "KeyCtrlTilde",
     59 + KeyCtrlA: "KeyCtrlA",
     60 + KeyCtrlB: "KeyCtrlB",
     61 + KeyCtrlC: "KeyCtrlC",
     62 + KeyCtrlD: "KeyCtrlD",
     63 + KeyCtrlE: "KeyCtrlE",
     64 + KeyCtrlF: "KeyCtrlF",
     65 + KeyCtrlG: "KeyCtrlG",
    58 66   KeyBackspace: "KeyBackspace",
    59 67   KeyTab: "KeyTab",
     68 + KeyCtrlJ: "KeyCtrlJ",
     69 + KeyCtrlK: "KeyCtrlK",
     70 + KeyCtrlL: "KeyCtrlL",
    60 71   KeyEnter: "KeyEnter",
     72 + KeyCtrlN: "KeyCtrlN",
     73 + KeyCtrlO: "KeyCtrlO",
     74 + KeyCtrlP: "KeyCtrlP",
     75 + KeyCtrlQ: "KeyCtrlQ",
     76 + KeyCtrlR: "KeyCtrlR",
     77 + KeyCtrlS: "KeyCtrlS",
     78 + KeyCtrlT: "KeyCtrlT",
     79 + KeyCtrlU: "KeyCtrlU",
     80 + KeyCtrlV: "KeyCtrlV",
     81 + KeyCtrlW: "KeyCtrlW",
     82 + KeyCtrlX: "KeyCtrlX",
     83 + KeyCtrlY: "KeyCtrlY",
     84 + KeyCtrlZ: "KeyCtrlZ",
    61 85   KeyEsc: "KeyEsc",
    62  - KeyCtrl: "KeyCtrl",
     86 + KeyCtrl4: "KeyCtrl4",
     87 + KeyCtrl5: "KeyCtrl5",
     88 + KeyCtrl6: "KeyCtrl6",
     89 + KeyCtrl7: "KeyCtrl7",
     90 + KeySpace: "KeySpace",
     91 + KeyBackspace2: "KeyBackspace2",
    63 92  }
    64 93   
    65 94  // Printable characters, but worth having constants for them.
    skipped 25 lines
    91 120   KeyArrowDown
    92 121   KeyArrowLeft
    93 122   KeyArrowRight
     123 + KeyCtrlTilde
     124 + KeyCtrlA
     125 + KeyCtrlB
     126 + KeyCtrlC
     127 + KeyCtrlD
     128 + KeyCtrlE
     129 + KeyCtrlF
     130 + KeyCtrlG
    94 131   KeyBackspace
    95 132   KeyTab
     133 + KeyCtrlJ
     134 + KeyCtrlK
     135 + KeyCtrlL
    96 136   KeyEnter
     137 + KeyCtrlN
     138 + KeyCtrlO
     139 + KeyCtrlP
     140 + KeyCtrlQ
     141 + KeyCtrlR
     142 + KeyCtrlS
     143 + KeyCtrlT
     144 + KeyCtrlU
     145 + KeyCtrlV
     146 + KeyCtrlW
     147 + KeyCtrlX
     148 + KeyCtrlY
     149 + KeyCtrlZ
    97 150   KeyEsc
    98  - KeyCtrl
     151 + KeyCtrl4
     152 + KeyCtrl5
     153 + KeyCtrl6
     154 + KeyCtrl7
     155 + KeyBackspace2
     156 +)
     157 + 
     158 +// Keys declared as duplicates by termbox.
     159 +const (
     160 + KeyCtrl2 Key = KeyCtrlTilde
     161 + KeyCtrlSpace Key = KeyCtrlTilde
     162 + KeyCtrlH Key = KeyBackspace
     163 + KeyCtrlI Key = KeyTab
     164 + KeyCtrlM Key = KeyEnter
     165 + KeyCtrlLsqBracket Key = KeyEsc
     166 + KeyCtrl3 Key = KeyEsc
     167 + KeyCtrlBackslash Key = KeyCtrl4
     168 + KeyCtrlRsqBracket Key = KeyCtrl5
     169 + KeyCtrlSlash Key = KeyCtrl7
     170 + KeyCtrlUnderscore Key = KeyCtrl7
     171 + KeyCtrl8 Key = KeyBackspace2
    99 172  )
    100 173   
  • ■ ■ ■ ■ ■ ■
    termdash_test.go
    skipped 210 lines
    211 211   fakewidget.MustDraw(
    212 212   ft,
    213 213   testcanvas.MustNew(ft.Area()),
     214 + &widgetapi.Meta{Focused: true},
    214 215   widgetapi.Options{},
    215 216   )
    216 217   return ft
    skipped 31 lines
    248 249   fakewidget.MustDraw(
    249 250   ft,
    250 251   testcanvas.MustNew(ft.Area()),
     252 + &widgetapi.Meta{Focused: true},
    251 253   widgetapi.Options{
    252 254   WantMouse: widgetapi.MouseScopeWidget,
    253 255   },
    skipped 20 lines
    274 276   fakewidget.MustDraw(
    275 277   ft,
    276 278   testcanvas.MustNew(ft.Area()),
     279 + &widgetapi.Meta{Focused: true},
    277 280   widgetapi.Options{
    278 281   WantKeyboard: widgetapi.KeyScopeFocused,
    279 282   WantMouse: widgetapi.MouseScopeWidget,
    skipped 28 lines
    308 311   fakewidget.MustDraw(
    309 312   ft,
    310 313   testcanvas.MustNew(ft.Area()),
     314 + &widgetapi.Meta{Focused: true},
    311 315   widgetapi.Options{},
    312 316   )
    313 317   return ft
    skipped 25 lines
    339 343   fakewidget.MustDraw(
    340 344   ft,
    341 345   testcanvas.MustNew(ft.Area()),
     346 + &widgetapi.Meta{Focused: true},
    342 347   widgetapi.Options{
    343 348   WantKeyboard: widgetapi.KeyScopeFocused,
    344 349   },
    skipped 28 lines
    373 378   fakewidget.MustDraw(
    374 379   ft,
    375 380   testcanvas.MustNew(ft.Area()),
     381 + &widgetapi.Meta{Focused: true},
    376 382   widgetapi.Options{
    377 383   WantMouse: widgetapi.MouseScopeWidget,
    378 384   },
    skipped 101 lines
    480 486   fakewidget.MustDraw(
    481 487   ft,
    482 488   testcanvas.MustNew(ft.Area()),
     489 + &widgetapi.Meta{Focused: true},
    483 490   widgetapi.Options{
    484 491   WantKeyboard: widgetapi.KeyScopeFocused,
    485 492   WantMouse: widgetapi.MouseScopeWidget,
    skipped 22 lines
    508 515   mirror,
    509 516   ft,
    510 517   testcanvas.MustNew(ft.Area()),
     518 + &widgetapi.Meta{Focused: true},
    511 519   )
    512 520   return ft
    513 521   },
    skipped 16 lines
    530 538   fakewidget.MustDraw(
    531 539   ft,
    532 540   testcanvas.MustNew(ft.Area()),
     541 + &widgetapi.Meta{Focused: true},
    533 542   widgetapi.Options{},
    534 543   )
    535 544   return ft
    skipped 14 lines
    550 559   fakewidget.MustDraw(
    551 560   ft,
    552 561   testcanvas.MustNew(ft.Area()),
     562 + &widgetapi.Meta{Focused: true},
    553 563   widgetapi.Options{},
    554 564   )
    555 565   return ft
    skipped 23 lines
    579 589   fakewidget.MustDraw(
    580 590   ft,
    581 591   testcanvas.MustNew(ft.Area()),
     592 + &widgetapi.Meta{Focused: true},
    582 593   widgetapi.Options{},
    583 594   )
    584 595   return ft
    skipped 69 lines
  • ■ ■ ■ ■ ■ ■
    termdashdemo/termdashdemo.go
    skipped 28 lines
    29 29   "github.com/mum4k/termdash/cell"
    30 30   "github.com/mum4k/termdash/container"
    31 31   "github.com/mum4k/termdash/container/grid"
     32 + "github.com/mum4k/termdash/keyboard"
    32 33   "github.com/mum4k/termdash/linestyle"
    33 34   "github.com/mum4k/termdash/terminal/termbox"
    34 35   "github.com/mum4k/termdash/terminal/terminalapi"
    skipped 5 lines
    40 41   "github.com/mum4k/termdash/widgets/segmentdisplay"
    41 42   "github.com/mum4k/termdash/widgets/sparkline"
    42 43   "github.com/mum4k/termdash/widgets/text"
     44 + "github.com/mum4k/termdash/widgets/textinput"
    43 45  )
    44 46   
    45 47  // redrawInterval is how often termdash redraws the screen.
    skipped 2 lines
    48 50  // widgets holds the widgets used by this demo.
    49 51  type widgets struct {
    50 52   segDist *segmentdisplay.SegmentDisplay
     53 + input *textinput.TextInput
    51 54   rollT *text.Text
    52 55   spGreen *sparkline.SparkLine
    53 56   spRed *sparkline.SparkLine
    skipped 10 lines
    64 67   
    65 68  // newWidgets creates all widgets used by this demo.
    66 69  func newWidgets(ctx context.Context, c *container.Container) (*widgets, error) {
    67  - sd, err := newSegmentDisplay(ctx)
     70 + updateText := make(chan string)
     71 + sd, err := newSegmentDisplay(ctx, updateText)
    68 72   if err != nil {
    69 73   return nil, err
    70 74   }
     75 + 
     76 + input, err := newTextInput(updateText)
     77 + if err != nil {
     78 + return nil, err
     79 + }
     80 + 
    71 81   rollT, err := newRollText(ctx)
    72 82   if err != nil {
    73 83   return nil, err
    skipped 28 lines
    102 112   }
    103 113   return &widgets{
    104 114   segDist: sd,
     115 + input: input,
    105 116   rollT: rollT,
    106 117   spGreen: spGreen,
    107 118   spRed: spRed,
    skipped 30 lines
    138 149   grid.RowHeightPerc(25,
    139 150   grid.Widget(w.segDist,
    140 151   container.Border(linestyle.Light),
    141  - container.BorderTitle("Press Q to quit"),
     152 + container.BorderTitle("Press Esc to quit"),
    142 153   ),
    143 154   ),
     155 + grid.RowHeightPerc(5,
     156 + grid.Widget(w.input),
     157 + ),
     158 + 
    144 159   grid.RowHeightPerc(5,
    145 160   grid.ColWidthPerc(25,
    146 161   grid.Widget(w.buttons.allB),
    skipped 12 lines
    159 174   switch lt {
    160 175   case layoutAll:
    161 176   leftRows = append(leftRows,
    162  - grid.RowHeightPerc(23,
     177 + grid.RowHeightPerc(20,
    163 178   grid.ColWidthPerc(50,
    164 179   grid.Widget(w.rollT,
    165 180   container.Border(linestyle.Light),
    166 181   container.BorderTitle("A rolling text"),
    167 182   ),
    168 183   ),
    169  - grid.RowHeightPerc(50,
    170  - grid.Widget(w.spGreen,
    171  - container.Border(linestyle.Light),
    172  - container.BorderTitle("Green SparkLine"),
     184 + grid.ColWidthPerc(50,
     185 + grid.RowHeightPerc(50,
     186 + grid.Widget(w.spGreen,
     187 + container.Border(linestyle.Light),
     188 + container.BorderTitle("Green SparkLine"),
     189 + ),
    173 190   ),
    174  - ),
    175  - grid.RowHeightPerc(50,
    176  - grid.Widget(w.spRed,
    177  - container.Border(linestyle.Light),
    178  - container.BorderTitle("Red SparkLine"),
     191 + grid.RowHeightPerc(50,
     192 + grid.Widget(w.spRed,
     193 + container.Border(linestyle.Light),
     194 + container.BorderTitle("Red SparkLine"),
     195 + ),
    179 196   ),
    180 197   ),
    181 198   ),
    skipped 4 lines
    186 203   container.BorderColor(cell.ColorNumber(39)),
    187 204   ),
    188 205   ),
    189  - grid.RowHeightPerc(35,
     206 + grid.RowHeightPerc(38,
    190 207   grid.Widget(w.heartLC,
    191 208   container.Border(linestyle.Light),
    192 209   container.BorderTitle("A LineChart"),
    skipped 120 lines
    313 330   ),
    314 331   }
    315 332   
    316  - segmentTextSpark := []container.Option{
     333 + textAndSparks := []container.Option{
     334 + container.SplitVertical(
     335 + container.Left(
     336 + container.Border(linestyle.Light),
     337 + container.BorderTitle("A rolling text"),
     338 + container.PlaceWidget(w.rollT),
     339 + ),
     340 + container.Right(
     341 + container.SplitHorizontal(
     342 + container.Top(
     343 + container.Border(linestyle.Light),
     344 + container.BorderTitle("Green SparkLine"),
     345 + container.PlaceWidget(w.spGreen),
     346 + ),
     347 + container.Bottom(
     348 + container.Border(linestyle.Light),
     349 + container.BorderTitle("Red SparkLine"),
     350 + container.PlaceWidget(w.spRed),
     351 + ),
     352 + ),
     353 + ),
     354 + ),
     355 + }
     356 + 
     357 + segmentTextInputSparks := []container.Option{
    317 358   container.SplitHorizontal(
    318 359   container.Top(
    319 360   container.Border(linestyle.Light),
    320  - container.BorderTitle("Press Q to quit"),
     361 + container.BorderTitle("Press Esc to quit"),
    321 362   container.PlaceWidget(w.segDist),
    322 363   ),
    323 364   container.Bottom(
    324 365   container.SplitHorizontal(
    325  - container.Top(buttonRow...),
    326  - container.Bottom(
    327  - container.SplitVertical(
    328  - container.Left(
    329  - container.Border(linestyle.Light),
    330  - container.BorderTitle("A rolling text"),
    331  - container.PlaceWidget(w.rollT),
     366 + container.Top(
     367 + container.SplitHorizontal(
     368 + container.Top(
     369 + container.PlaceWidget(w.input),
    332 370   ),
    333  - container.Right(
    334  - container.SplitHorizontal(
    335  - container.Top(
    336  - container.Border(linestyle.Light),
    337  - container.BorderTitle("Green SparkLine"),
    338  - container.PlaceWidget(w.spGreen),
    339  - ),
    340  - container.Bottom(
    341  - container.Border(linestyle.Light),
    342  - container.BorderTitle("Red SparkLine"),
    343  - container.PlaceWidget(w.spRed),
    344  - ),
    345  - ),
    346  - ),
     371 + container.Bottom(buttonRow...),
    347 372   ),
    348 373   ),
    349  - container.SplitPercent(20),
     374 + container.Bottom(textAndSparks...),
     375 + container.SplitPercent(40),
    350 376   ),
    351 377   ),
    352 378   container.SplitPercent(50),
    skipped 19 lines
    372 398   
    373 399   leftSide := []container.Option{
    374 400   container.SplitHorizontal(
    375  - container.Top(segmentTextSpark...),
     401 + container.Top(segmentTextInputSparks...),
    376 402   container.Bottom(gaugeAndHeartbeat...),
    377 403   container.SplitPercent(50),
    378 404   ),
    skipped 94 lines
    473 499   }
    474 500   
    475 501   quitter := func(k *terminalapi.Keyboard) {
    476  - if k.Key == 'q' || k.Key == 'Q' {
     502 + if k.Key == keyboard.KeyEsc || k.Key == keyboard.KeyCtrlC {
    477 503   cancel()
    478 504   }
    479 505   }
    skipped 19 lines
    499 525   }
    500 526  }
    501 527   
    502  -// newSegmentDisplay creates a new SegmentDisplay that shows the Termdash name.
    503  -func newSegmentDisplay(ctx context.Context) (*segmentdisplay.SegmentDisplay, error) {
     528 +// textState creates a rotated state for the text we are displaying.
     529 +func textState(text string, capacity, step int) []rune {
     530 + if capacity == 0 {
     531 + return nil
     532 + }
     533 + 
     534 + var state []rune
     535 + for i := 0; i < capacity; i++ {
     536 + state = append(state, ' ')
     537 + }
     538 + state = append(state, []rune(text)...)
     539 + step = step % len(state)
     540 + return rotateRunes(state, step)
     541 +}
     542 + 
     543 +// newTextInput creates a new TextInput field that changes the text on the
     544 +// SegmentDisplay.
     545 +func newTextInput(updateText chan<- string) (*textinput.TextInput, error) {
     546 + input, err := textinput.New(
     547 + textinput.Label("Change text to: ", cell.FgColor(cell.ColorBlue)),
     548 + textinput.MaxWidthCells(20),
     549 + textinput.PlaceHolder("enter any text"),
     550 + textinput.OnSubmit(func(text string) error {
     551 + updateText <- text
     552 + return nil
     553 + }),
     554 + textinput.ClearOnSubmit(),
     555 + )
     556 + if err != nil {
     557 + return nil, err
     558 + }
     559 + return input, err
     560 +}
     561 + 
     562 +// newSegmentDisplay creates a new SegmentDisplay that initially shows the
     563 +// Termdash name. Shows any text that is sent over the channel.
     564 +func newSegmentDisplay(ctx context.Context, updateText <-chan string) (*segmentdisplay.SegmentDisplay, error) {
    504 565   sd, err := segmentdisplay.New()
    505 566   if err != nil {
    506 567   return nil, err
    507 568   }
    508 569   
    509  - const text = "Termdash"
    510  - colors := map[rune]cell.Color{
    511  - 'T': cell.ColorBlue,
    512  - 'e': cell.ColorRed,
    513  - 'r': cell.ColorYellow,
    514  - 'm': cell.ColorBlue,
    515  - 'd': cell.ColorGreen,
    516  - 'a': cell.ColorRed,
    517  - 's': cell.ColorGreen,
    518  - 'h': cell.ColorRed,
     570 + colors := []cell.Color{
     571 + cell.ColorBlue,
     572 + cell.ColorRed,
     573 + cell.ColorYellow,
     574 + cell.ColorBlue,
     575 + cell.ColorGreen,
     576 + cell.ColorRed,
     577 + cell.ColorGreen,
     578 + cell.ColorRed,
    519 579   }
    520 580   
    521  - var state []rune
    522  - for i := 0; i < len(text); i++ {
    523  - state = append(state, ' ')
    524  - }
    525  - state = append(state, []rune(text)...)
    526  - go periodic(ctx, 500*time.Millisecond, func() error {
    527  - var chunks []*segmentdisplay.TextChunk
    528  - for i := 0; i < len(text); i++ {
    529  - chunks = append(chunks, segmentdisplay.NewChunk(
    530  - string(state[i]),
    531  - segmentdisplay.WriteCellOpts(cell.FgColor(colors[state[i]])),
    532  - ))
    533  - }
    534  - if err := sd.Write(chunks); err != nil {
    535  - return err
     581 + text := "Termdash"
     582 + step := 0
     583 + 
     584 + go func() {
     585 + ticker := time.NewTicker(500 * time.Millisecond)
     586 + defer ticker.Stop()
     587 + for {
     588 + select {
     589 + case <-ticker.C:
     590 + state := textState(text, sd.Capacity(), step)
     591 + var chunks []*segmentdisplay.TextChunk
     592 + for i := 0; i < sd.Capacity(); i++ {
     593 + if i >= len(state) {
     594 + break
     595 + }
     596 + 
     597 + color := colors[i%len(colors)]
     598 + chunks = append(chunks, segmentdisplay.NewChunk(
     599 + string(state[i]),
     600 + segmentdisplay.WriteCellOpts(cell.FgColor(color)),
     601 + ))
     602 + }
     603 + if len(chunks) == 0 {
     604 + continue
     605 + }
     606 + if err := sd.Write(chunks); err != nil {
     607 + panic(err)
     608 + }
     609 + step++
     610 + 
     611 + case t := <-updateText:
     612 + text = t
     613 + sd.Reset()
     614 + step = 0
     615 + 
     616 + case <-ctx.Done():
     617 + return
     618 + }
    536 619   }
    537  - state = rotateRunes(state, 1)
    538  - return nil
    539  - })
     620 + }()
    540 621   return sd, nil
    541 622  }
    542 623   
    skipped 324 lines
  • ■ ■ ■ ■ ■ ■
    terminal/termbox/event.go
    skipped 24 lines
    25 25   tbx "github.com/nsf/termbox-go"
    26 26  )
    27 27   
    28  -// newKeyboard creates a new termdash keyboard events with the provided keys.
    29  -func newKeyboard(keys ...keyboard.Key) []terminalapi.Event {
    30  - var evs []terminalapi.Event
    31  - for _, k := range keys {
    32  - evs = append(evs, &terminalapi.Keyboard{Key: k})
    33  - }
    34  - return evs
     28 +// tbxToTd maps termbox key values to the termdash format.
     29 +var tbxToTd = map[tbx.Key]keyboard.Key{
     30 + tbx.KeySpace: keyboard.KeySpace,
     31 + tbx.KeyF1: keyboard.KeyF1,
     32 + tbx.KeyF2: keyboard.KeyF2,
     33 + tbx.KeyF3: keyboard.KeyF3,
     34 + tbx.KeyF4: keyboard.KeyF4,
     35 + tbx.KeyF5: keyboard.KeyF5,
     36 + tbx.KeyF6: keyboard.KeyF6,
     37 + tbx.KeyF7: keyboard.KeyF7,
     38 + tbx.KeyF8: keyboard.KeyF8,
     39 + tbx.KeyF9: keyboard.KeyF9,
     40 + tbx.KeyF10: keyboard.KeyF10,
     41 + tbx.KeyF11: keyboard.KeyF11,
     42 + tbx.KeyF12: keyboard.KeyF12,
     43 + tbx.KeyInsert: keyboard.KeyInsert,
     44 + tbx.KeyDelete: keyboard.KeyDelete,
     45 + tbx.KeyHome: keyboard.KeyHome,
     46 + tbx.KeyEnd: keyboard.KeyEnd,
     47 + tbx.KeyPgup: keyboard.KeyPgUp,
     48 + tbx.KeyPgdn: keyboard.KeyPgDn,
     49 + tbx.KeyArrowUp: keyboard.KeyArrowUp,
     50 + tbx.KeyArrowDown: keyboard.KeyArrowDown,
     51 + tbx.KeyArrowLeft: keyboard.KeyArrowLeft,
     52 + tbx.KeyArrowRight: keyboard.KeyArrowRight,
     53 + tbx.KeyCtrlTilde: keyboard.KeyCtrlTilde,
     54 + tbx.KeyCtrlA: keyboard.KeyCtrlA,
     55 + tbx.KeyCtrlB: keyboard.KeyCtrlB,
     56 + tbx.KeyCtrlC: keyboard.KeyCtrlC,
     57 + tbx.KeyCtrlD: keyboard.KeyCtrlD,
     58 + tbx.KeyCtrlE: keyboard.KeyCtrlE,
     59 + tbx.KeyCtrlF: keyboard.KeyCtrlF,
     60 + tbx.KeyCtrlG: keyboard.KeyCtrlG,
     61 + tbx.KeyBackspace: keyboard.KeyBackspace,
     62 + tbx.KeyTab: keyboard.KeyTab,
     63 + tbx.KeyCtrlJ: keyboard.KeyCtrlJ,
     64 + tbx.KeyCtrlK: keyboard.KeyCtrlK,
     65 + tbx.KeyCtrlL: keyboard.KeyCtrlL,
     66 + tbx.KeyEnter: keyboard.KeyEnter,
     67 + tbx.KeyCtrlN: keyboard.KeyCtrlN,
     68 + tbx.KeyCtrlO: keyboard.KeyCtrlO,
     69 + tbx.KeyCtrlP: keyboard.KeyCtrlP,
     70 + tbx.KeyCtrlQ: keyboard.KeyCtrlQ,
     71 + tbx.KeyCtrlR: keyboard.KeyCtrlR,
     72 + tbx.KeyCtrlS: keyboard.KeyCtrlS,
     73 + tbx.KeyCtrlT: keyboard.KeyCtrlT,
     74 + tbx.KeyCtrlU: keyboard.KeyCtrlU,
     75 + tbx.KeyCtrlV: keyboard.KeyCtrlV,
     76 + tbx.KeyCtrlW: keyboard.KeyCtrlW,
     77 + tbx.KeyCtrlX: keyboard.KeyCtrlX,
     78 + tbx.KeyCtrlY: keyboard.KeyCtrlY,
     79 + tbx.KeyCtrlZ: keyboard.KeyCtrlZ,
     80 + tbx.KeyEsc: keyboard.KeyEsc,
     81 + tbx.KeyCtrl4: keyboard.KeyCtrl4,
     82 + tbx.KeyCtrl5: keyboard.KeyCtrl5,
     83 + tbx.KeyCtrl6: keyboard.KeyCtrl6,
     84 + tbx.KeyCtrl7: keyboard.KeyCtrl7,
     85 + tbx.KeyBackspace2: keyboard.KeyBackspace2,
    35 86  }
    36 87   
    37 88  // convKey converts a termbox keyboard event to the termdash format.
    38  -func convKey(tbxEv tbx.Event) []terminalapi.Event {
     89 +func convKey(tbxEv tbx.Event) terminalapi.Event {
    39 90   if tbxEv.Key != 0 && tbxEv.Ch != 0 {
    40  - return []terminalapi.Event{
    41  - terminalapi.NewErrorf("the key event contain both a key(%v) and a character(%v)", tbxEv.Key, tbxEv.Ch),
    42  - }
     91 + return terminalapi.NewErrorf("the key event contain both a key(%v) and a character(%v)", tbxEv.Key, tbxEv.Ch)
    43 92   }
    44 93   
    45 94   if tbxEv.Ch != 0 {
    46  - return []terminalapi.Event{&terminalapi.Keyboard{
     95 + return &terminalapi.Keyboard{
    47 96   Key: keyboard.Key(tbxEv.Ch),
    48  - }}
     97 + }
    49 98   }
    50 99   
    51  - switch k := tbxEv.Key; k {
    52  - case tbx.KeySpace:
    53  - return newKeyboard(keyboard.KeySpace)
    54  - case tbx.KeyF1:
    55  - return newKeyboard(keyboard.KeyF1)
    56  - case tbx.KeyF2:
    57  - return newKeyboard(keyboard.KeyF2)
    58  - case tbx.KeyF3:
    59  - return newKeyboard(keyboard.KeyF3)
    60  - case tbx.KeyF4:
    61  - return newKeyboard(keyboard.KeyF4)
    62  - case tbx.KeyF5:
    63  - return newKeyboard(keyboard.KeyF5)
    64  - case tbx.KeyF6:
    65  - return newKeyboard(keyboard.KeyF6)
    66  - case tbx.KeyF7:
    67  - return newKeyboard(keyboard.KeyF7)
    68  - case tbx.KeyF8:
    69  - return newKeyboard(keyboard.KeyF8)
    70  - case tbx.KeyF9:
    71  - return newKeyboard(keyboard.KeyF9)
    72  - case tbx.KeyF10:
    73  - return newKeyboard(keyboard.KeyF10)
    74  - case tbx.KeyF11:
    75  - return newKeyboard(keyboard.KeyF11)
    76  - case tbx.KeyF12:
    77  - return newKeyboard(keyboard.KeyF12)
    78  - case tbx.KeyInsert:
    79  - return newKeyboard(keyboard.KeyInsert)
    80  - case tbx.KeyDelete:
    81  - return newKeyboard(keyboard.KeyDelete)
    82  - case tbx.KeyHome:
    83  - return newKeyboard(keyboard.KeyHome)
    84  - case tbx.KeyEnd:
    85  - return newKeyboard(keyboard.KeyEnd)
    86  - case tbx.KeyPgup:
    87  - return newKeyboard(keyboard.KeyPgUp)
    88  - case tbx.KeyPgdn:
    89  - return newKeyboard(keyboard.KeyPgDn)
    90  - case tbx.KeyArrowUp:
    91  - return newKeyboard(keyboard.KeyArrowUp)
    92  - case tbx.KeyArrowDown:
    93  - return newKeyboard(keyboard.KeyArrowDown)
    94  - case tbx.KeyArrowLeft:
    95  - return newKeyboard(keyboard.KeyArrowLeft)
    96  - case tbx.KeyArrowRight:
    97  - return newKeyboard(keyboard.KeyArrowRight)
    98  - case tbx.KeyBackspace /*, tbx.KeyCtrlH */ :
    99  - return newKeyboard(keyboard.KeyBackspace)
    100  - case tbx.KeyTab /*, tbx.KeyCtrlI */ :
    101  - return newKeyboard(keyboard.KeyTab)
    102  - case tbx.KeyEnter /*, tbx.KeyCtrlM*/ :
    103  - return newKeyboard(keyboard.KeyEnter)
    104  - case tbx.KeyEsc /*, tbx.KeyCtrlLsqBracket, tbx.KeyCtrl3 */ :
    105  - return newKeyboard(keyboard.KeyEsc)
    106  - case tbx.KeyCtrl2 /*, tbx.KeyCtrlTilde, tbx.KeyCtrlSpace */ :
    107  - return newKeyboard(keyboard.KeyCtrl, '2')
    108  - case tbx.KeyCtrl4 /*, tbx.KeyCtrlBackslash */ :
    109  - return newKeyboard(keyboard.KeyCtrl, '4')
    110  - case tbx.KeyCtrl5 /*, tbx.KeyCtrlRsqBracket */ :
    111  - return newKeyboard(keyboard.KeyCtrl, '5')
    112  - case tbx.KeyCtrl6:
    113  - return newKeyboard(keyboard.KeyCtrl, '6')
    114  - case tbx.KeyCtrl7 /*, tbx.KeyCtrlSlash, tbx.KeyCtrlUnderscore */ :
    115  - return newKeyboard(keyboard.KeyCtrl, '7')
    116  - case tbx.KeyCtrl8:
    117  - return newKeyboard(keyboard.KeyCtrl, '8')
    118  - case tbx.KeyCtrlA:
    119  - return newKeyboard(keyboard.KeyCtrl, 'a')
    120  - case tbx.KeyCtrlB:
    121  - return newKeyboard(keyboard.KeyCtrl, 'b')
    122  - case tbx.KeyCtrlC:
    123  - return newKeyboard(keyboard.KeyCtrl, 'c')
    124  - case tbx.KeyCtrlD:
    125  - return newKeyboard(keyboard.KeyCtrl, 'd')
    126  - case tbx.KeyCtrlE:
    127  - return newKeyboard(keyboard.KeyCtrl, 'e')
    128  - case tbx.KeyCtrlF:
    129  - return newKeyboard(keyboard.KeyCtrl, 'f')
    130  - case tbx.KeyCtrlG:
    131  - return newKeyboard(keyboard.KeyCtrl, 'g')
    132  - case tbx.KeyCtrlJ:
    133  - return newKeyboard(keyboard.KeyCtrl, 'j')
    134  - case tbx.KeyCtrlK:
    135  - return newKeyboard(keyboard.KeyCtrl, 'k')
    136  - case tbx.KeyCtrlL:
    137  - return newKeyboard(keyboard.KeyCtrl, 'l')
    138  - case tbx.KeyCtrlN:
    139  - return newKeyboard(keyboard.KeyCtrl, 'n')
    140  - case tbx.KeyCtrlO:
    141  - return newKeyboard(keyboard.KeyCtrl, 'o')
    142  - case tbx.KeyCtrlP:
    143  - return newKeyboard(keyboard.KeyCtrl, 'p')
    144  - case tbx.KeyCtrlQ:
    145  - return newKeyboard(keyboard.KeyCtrl, 'q')
    146  - case tbx.KeyCtrlR:
    147  - return newKeyboard(keyboard.KeyCtrl, 'r')
    148  - case tbx.KeyCtrlS:
    149  - return newKeyboard(keyboard.KeyCtrl, 's')
    150  - case tbx.KeyCtrlT:
    151  - return newKeyboard(keyboard.KeyCtrl, 't')
    152  - case tbx.KeyCtrlU:
    153  - return newKeyboard(keyboard.KeyCtrl, 'u')
    154  - case tbx.KeyCtrlV:
    155  - return newKeyboard(keyboard.KeyCtrl, 'v')
    156  - case tbx.KeyCtrlW:
    157  - return newKeyboard(keyboard.KeyCtrl, 'w')
    158  - case tbx.KeyCtrlX:
    159  - return newKeyboard(keyboard.KeyCtrl, 'x')
    160  - case tbx.KeyCtrlY:
    161  - return newKeyboard(keyboard.KeyCtrl, 'y')
    162  - case tbx.KeyCtrlZ:
    163  - return newKeyboard(keyboard.KeyCtrl, 'z')
    164  - default:
    165  - return []terminalapi.Event{
    166  - terminalapi.NewErrorf("unknown keyboard key %v in a keyboard event", k),
    167  - }
     100 + k, ok := tbxToTd[tbxEv.Key]
     101 + if !ok {
     102 + return terminalapi.NewErrorf("unknown keyboard key '%v' in a keyboard event", k)
     103 + }
     104 + return &terminalapi.Keyboard{
     105 + Key: k,
    168 106   }
    169 107  }
    170 108   
    skipped 59 lines
    230 168   case tbx.EventMouse:
    231 169   return []terminalapi.Event{convMouse(tbxEv)}
    232 170   case tbx.EventKey:
    233  - return convKey(tbxEv)
     171 + return []terminalapi.Event{
     172 + convKey(tbxEv),
     173 + }
    234 174   default:
    235 175   return []terminalapi.Event{
    236 176   terminalapi.NewErrorf("unknown termbox event type: %v", t),
    skipped 4 lines
  • ■ ■ ■ ■ ■ ■
    terminal/termbox/event_test.go
    skipped 192 lines
    193 193   tests := []struct {
    194 194   key tbx.Key
    195 195   ch rune
    196  - want []keyboard.Key
     196 + want keyboard.Key
    197 197   wantErr bool
    198 198   }{
    199 199   {key: tbx.KeyF1, ch: 'a', wantErr: true},
    200 200   {key: 2000, wantErr: true},
    201  - {ch: 'a', want: []keyboard.Key{'a'}},
    202  - {ch: 'A', want: []keyboard.Key{'A'}},
    203  - {ch: 'z', want: []keyboard.Key{'z'}},
    204  - {ch: 'Z', want: []keyboard.Key{'Z'}},
    205  - {ch: '0', want: []keyboard.Key{'0'}},
    206  - {ch: '9', want: []keyboard.Key{'9'}},
    207  - {ch: '!', want: []keyboard.Key{'!'}},
    208  - {ch: ')', want: []keyboard.Key{')'}},
    209  - {key: tbx.KeySpace, want: []keyboard.Key{keyboard.KeySpace}},
    210  - {key: tbx.KeyF1, want: []keyboard.Key{keyboard.KeyF1}},
    211  - {key: tbx.KeyF2, want: []keyboard.Key{keyboard.KeyF2}},
    212  - {key: tbx.KeyF3, want: []keyboard.Key{keyboard.KeyF3}},
    213  - {key: tbx.KeyF4, want: []keyboard.Key{keyboard.KeyF4}},
    214  - {key: tbx.KeyF5, want: []keyboard.Key{keyboard.KeyF5}},
    215  - {key: tbx.KeyF6, want: []keyboard.Key{keyboard.KeyF6}},
    216  - {key: tbx.KeyF7, want: []keyboard.Key{keyboard.KeyF7}},
    217  - {key: tbx.KeyF8, want: []keyboard.Key{keyboard.KeyF8}},
    218  - {key: tbx.KeyF9, want: []keyboard.Key{keyboard.KeyF9}},
    219  - {key: tbx.KeyF10, want: []keyboard.Key{keyboard.KeyF10}},
    220  - {key: tbx.KeyF11, want: []keyboard.Key{keyboard.KeyF11}},
    221  - {key: tbx.KeyF12, want: []keyboard.Key{keyboard.KeyF12}},
    222  - {key: tbx.KeyInsert, want: []keyboard.Key{keyboard.KeyInsert}},
    223  - {key: tbx.KeyDelete, want: []keyboard.Key{keyboard.KeyDelete}},
    224  - {key: tbx.KeyHome, want: []keyboard.Key{keyboard.KeyHome}},
    225  - {key: tbx.KeyEnd, want: []keyboard.Key{keyboard.KeyEnd}},
    226  - {key: tbx.KeyPgup, want: []keyboard.Key{keyboard.KeyPgUp}},
    227  - {key: tbx.KeyPgdn, want: []keyboard.Key{keyboard.KeyPgDn}},
    228  - {key: tbx.KeyArrowUp, want: []keyboard.Key{keyboard.KeyArrowUp}},
    229  - {key: tbx.KeyArrowDown, want: []keyboard.Key{keyboard.KeyArrowDown}},
    230  - {key: tbx.KeyArrowLeft, want: []keyboard.Key{keyboard.KeyArrowLeft}},
    231  - {key: tbx.KeyArrowRight, want: []keyboard.Key{keyboard.KeyArrowRight}},
    232  - {key: tbx.KeyBackspace, want: []keyboard.Key{keyboard.KeyBackspace}},
    233  - {key: tbx.KeyCtrlH, want: []keyboard.Key{keyboard.KeyBackspace}},
    234  - {key: tbx.KeyTab, want: []keyboard.Key{keyboard.KeyTab}},
    235  - {key: tbx.KeyCtrlI, want: []keyboard.Key{keyboard.KeyTab}},
    236  - {key: tbx.KeyEnter, want: []keyboard.Key{keyboard.KeyEnter}},
    237  - {key: tbx.KeyCtrlM, want: []keyboard.Key{keyboard.KeyEnter}},
    238  - {key: tbx.KeyEsc, want: []keyboard.Key{keyboard.KeyEsc}},
    239  - {key: tbx.KeyCtrlLsqBracket, want: []keyboard.Key{keyboard.KeyEsc}},
    240  - {key: tbx.KeyCtrl3, want: []keyboard.Key{keyboard.KeyEsc}},
    241  - {key: tbx.KeyCtrl2, want: []keyboard.Key{keyboard.KeyCtrl, '2'}},
    242  - {key: tbx.KeyCtrlTilde, want: []keyboard.Key{keyboard.KeyCtrl, '2'}},
    243  - {key: tbx.KeyCtrlSpace, want: []keyboard.Key{keyboard.KeyCtrl, '2'}},
    244  - {key: tbx.KeyCtrl4, want: []keyboard.Key{keyboard.KeyCtrl, '4'}},
    245  - {key: tbx.KeyCtrlBackslash, want: []keyboard.Key{keyboard.KeyCtrl, '4'}},
    246  - {key: tbx.KeyCtrl5, want: []keyboard.Key{keyboard.KeyCtrl, '5'}},
    247  - {key: tbx.KeyCtrlRsqBracket, want: []keyboard.Key{keyboard.KeyCtrl, '5'}},
    248  - {key: tbx.KeyCtrl6, want: []keyboard.Key{keyboard.KeyCtrl, '6'}},
    249  - {key: tbx.KeyCtrl7, want: []keyboard.Key{keyboard.KeyCtrl, '7'}},
    250  - {key: tbx.KeyCtrlSlash, want: []keyboard.Key{keyboard.KeyCtrl, '7'}},
    251  - {key: tbx.KeyCtrlUnderscore, want: []keyboard.Key{keyboard.KeyCtrl, '7'}},
    252  - {key: tbx.KeyCtrl8, want: []keyboard.Key{keyboard.KeyCtrl, '8'}},
    253  - {key: tbx.KeyCtrlA, want: []keyboard.Key{keyboard.KeyCtrl, 'a'}},
    254  - {key: tbx.KeyCtrlB, want: []keyboard.Key{keyboard.KeyCtrl, 'b'}},
    255  - {key: tbx.KeyCtrlC, want: []keyboard.Key{keyboard.KeyCtrl, 'c'}},
    256  - {key: tbx.KeyCtrlD, want: []keyboard.Key{keyboard.KeyCtrl, 'd'}},
    257  - {key: tbx.KeyCtrlE, want: []keyboard.Key{keyboard.KeyCtrl, 'e'}},
    258  - {key: tbx.KeyCtrlF, want: []keyboard.Key{keyboard.KeyCtrl, 'f'}},
    259  - {key: tbx.KeyCtrlG, want: []keyboard.Key{keyboard.KeyCtrl, 'g'}},
    260  - {key: tbx.KeyCtrlJ, want: []keyboard.Key{keyboard.KeyCtrl, 'j'}},
    261  - {key: tbx.KeyCtrlK, want: []keyboard.Key{keyboard.KeyCtrl, 'k'}},
    262  - {key: tbx.KeyCtrlL, want: []keyboard.Key{keyboard.KeyCtrl, 'l'}},
    263  - {key: tbx.KeyCtrlN, want: []keyboard.Key{keyboard.KeyCtrl, 'n'}},
    264  - {key: tbx.KeyCtrlO, want: []keyboard.Key{keyboard.KeyCtrl, 'o'}},
    265  - {key: tbx.KeyCtrlP, want: []keyboard.Key{keyboard.KeyCtrl, 'p'}},
    266  - {key: tbx.KeyCtrlQ, want: []keyboard.Key{keyboard.KeyCtrl, 'q'}},
    267  - {key: tbx.KeyCtrlR, want: []keyboard.Key{keyboard.KeyCtrl, 'r'}},
    268  - {key: tbx.KeyCtrlS, want: []keyboard.Key{keyboard.KeyCtrl, 's'}},
    269  - {key: tbx.KeyCtrlT, want: []keyboard.Key{keyboard.KeyCtrl, 't'}},
    270  - {key: tbx.KeyCtrlU, want: []keyboard.Key{keyboard.KeyCtrl, 'u'}},
    271  - {key: tbx.KeyCtrlV, want: []keyboard.Key{keyboard.KeyCtrl, 'v'}},
    272  - {key: tbx.KeyCtrlW, want: []keyboard.Key{keyboard.KeyCtrl, 'w'}},
    273  - {key: tbx.KeyCtrlX, want: []keyboard.Key{keyboard.KeyCtrl, 'x'}},
    274  - {key: tbx.KeyCtrlY, want: []keyboard.Key{keyboard.KeyCtrl, 'y'}},
    275  - {key: tbx.KeyCtrlZ, want: []keyboard.Key{keyboard.KeyCtrl, 'z'}},
     201 + {ch: 'a', want: 'a'},
     202 + {ch: 'A', want: 'A'},
     203 + {ch: 'z', want: 'z'},
     204 + {ch: 'Z', want: 'Z'},
     205 + {ch: '0', want: '0'},
     206 + {ch: '9', want: '9'},
     207 + {ch: '!', want: '!'},
     208 + {ch: ')', want: ')'},
     209 + {key: tbx.KeySpace, want: keyboard.KeySpace},
     210 + {key: tbx.KeyF1, want: keyboard.KeyF1},
     211 + {key: tbx.KeyF2, want: keyboard.KeyF2},
     212 + {key: tbx.KeyF3, want: keyboard.KeyF3},
     213 + {key: tbx.KeyF4, want: keyboard.KeyF4},
     214 + {key: tbx.KeyF5, want: keyboard.KeyF5},
     215 + {key: tbx.KeyF6, want: keyboard.KeyF6},
     216 + {key: tbx.KeyF7, want: keyboard.KeyF7},
     217 + {key: tbx.KeyF8, want: keyboard.KeyF8},
     218 + {key: tbx.KeyF9, want: keyboard.KeyF9},
     219 + {key: tbx.KeyF10, want: keyboard.KeyF10},
     220 + {key: tbx.KeyF11, want: keyboard.KeyF11},
     221 + {key: tbx.KeyF12, want: keyboard.KeyF12},
     222 + {key: tbx.KeyInsert, want: keyboard.KeyInsert},
     223 + {key: tbx.KeyDelete, want: keyboard.KeyDelete},
     224 + {key: tbx.KeyHome, want: keyboard.KeyHome},
     225 + {key: tbx.KeyEnd, want: keyboard.KeyEnd},
     226 + {key: tbx.KeyPgup, want: keyboard.KeyPgUp},
     227 + {key: tbx.KeyPgdn, want: keyboard.KeyPgDn},
     228 + {key: tbx.KeyArrowUp, want: keyboard.KeyArrowUp},
     229 + {key: tbx.KeyArrowDown, want: keyboard.KeyArrowDown},
     230 + {key: tbx.KeyArrowLeft, want: keyboard.KeyArrowLeft},
     231 + {key: tbx.KeyArrowRight, want: keyboard.KeyArrowRight},
     232 + {key: tbx.KeyCtrlTilde, want: keyboard.KeyCtrlTilde},
     233 + {key: tbx.KeyCtrlTilde, want: keyboard.KeyCtrl2},
     234 + {key: tbx.KeyCtrlTilde, want: keyboard.KeyCtrlSpace},
     235 + {key: tbx.KeyCtrl2, want: keyboard.KeyCtrlTilde},
     236 + {key: tbx.KeyCtrlSpace, want: keyboard.KeyCtrlTilde},
     237 + {key: tbx.KeyCtrlA, want: keyboard.KeyCtrlA},
     238 + {key: tbx.KeyCtrlB, want: keyboard.KeyCtrlB},
     239 + {key: tbx.KeyCtrlC, want: keyboard.KeyCtrlC},
     240 + {key: tbx.KeyCtrlD, want: keyboard.KeyCtrlD},
     241 + {key: tbx.KeyCtrlE, want: keyboard.KeyCtrlE},
     242 + {key: tbx.KeyCtrlF, want: keyboard.KeyCtrlF},
     243 + {key: tbx.KeyCtrlG, want: keyboard.KeyCtrlG},
     244 + {key: tbx.KeyBackspace, want: keyboard.KeyBackspace},
     245 + {key: tbx.KeyBackspace, want: keyboard.KeyCtrlH},
     246 + {key: tbx.KeyCtrlH, want: keyboard.KeyBackspace},
     247 + {key: tbx.KeyTab, want: keyboard.KeyTab},
     248 + {key: tbx.KeyTab, want: keyboard.KeyCtrlI},
     249 + {key: tbx.KeyCtrlI, want: keyboard.KeyTab},
     250 + {key: tbx.KeyCtrlJ, want: keyboard.KeyCtrlJ},
     251 + {key: tbx.KeyCtrlK, want: keyboard.KeyCtrlK},
     252 + {key: tbx.KeyCtrlL, want: keyboard.KeyCtrlL},
     253 + {key: tbx.KeyEnter, want: keyboard.KeyEnter},
     254 + {key: tbx.KeyEnter, want: keyboard.KeyCtrlM},
     255 + {key: tbx.KeyCtrlM, want: keyboard.KeyEnter},
     256 + {key: tbx.KeyCtrlN, want: keyboard.KeyCtrlN},
     257 + {key: tbx.KeyCtrlO, want: keyboard.KeyCtrlO},
     258 + {key: tbx.KeyCtrlP, want: keyboard.KeyCtrlP},
     259 + {key: tbx.KeyCtrlQ, want: keyboard.KeyCtrlQ},
     260 + {key: tbx.KeyCtrlR, want: keyboard.KeyCtrlR},
     261 + {key: tbx.KeyCtrlS, want: keyboard.KeyCtrlS},
     262 + {key: tbx.KeyCtrlT, want: keyboard.KeyCtrlT},
     263 + {key: tbx.KeyCtrlU, want: keyboard.KeyCtrlU},
     264 + {key: tbx.KeyCtrlV, want: keyboard.KeyCtrlV},
     265 + {key: tbx.KeyCtrlW, want: keyboard.KeyCtrlW},
     266 + {key: tbx.KeyCtrlX, want: keyboard.KeyCtrlX},
     267 + {key: tbx.KeyCtrlY, want: keyboard.KeyCtrlY},
     268 + {key: tbx.KeyCtrlZ, want: keyboard.KeyCtrlZ},
     269 + {key: tbx.KeyEsc, want: keyboard.KeyEsc},
     270 + {key: tbx.KeyEsc, want: keyboard.KeyCtrlLsqBracket},
     271 + {key: tbx.KeyEsc, want: keyboard.KeyCtrl3},
     272 + {key: tbx.KeyCtrlLsqBracket, want: keyboard.KeyEsc},
     273 + {key: tbx.KeyCtrl3, want: keyboard.KeyEsc},
     274 + {key: tbx.KeyCtrl4, want: keyboard.KeyCtrl4},
     275 + {key: tbx.KeyCtrl4, want: keyboard.KeyCtrlBackslash},
     276 + {key: tbx.KeyCtrlBackslash, want: keyboard.KeyCtrl4},
     277 + {key: tbx.KeyCtrl5, want: keyboard.KeyCtrl5},
     278 + {key: tbx.KeyCtrl5, want: keyboard.KeyCtrlRsqBracket},
     279 + {key: tbx.KeyCtrlRsqBracket, want: keyboard.KeyCtrl5},
     280 + {key: tbx.KeyCtrl6, want: keyboard.KeyCtrl6},
     281 + {key: tbx.KeyCtrl7, want: keyboard.KeyCtrl7},
     282 + {key: tbx.KeyCtrl7, want: keyboard.KeyCtrlSlash},
     283 + {key: tbx.KeyCtrl7, want: keyboard.KeyCtrlUnderscore},
     284 + {key: tbx.KeyCtrlSlash, want: keyboard.KeyCtrl7},
     285 + {key: tbx.KeyCtrlUnderscore, want: keyboard.KeyCtrl7},
     286 + {key: tbx.KeyBackspace2, want: keyboard.KeyBackspace2},
     287 + {key: tbx.KeyBackspace2, want: keyboard.KeyCtrl8},
     288 + {key: tbx.KeyCtrl8, want: keyboard.KeyBackspace2},
    276 289   }
    277 290   
    278 291   for _, tc := range tests {
    skipped 5 lines
    284 297   })
    285 298   
    286 299   gotCount := len(evs)
    287  - var wantCount int
    288  - if tc.wantErr {
    289  - wantCount = 1
    290  - } else {
    291  - wantCount = len(tc.want)
    292  - }
    293  - 
     300 + wantCount := 1
    294 301   if gotCount != wantCount {
    295 302   t.Fatalf("toTermdashEvents => got %d events, want %d, events were:\n%v", gotCount, wantCount, pretty.Sprint(evs))
    296 303   }
    297  - 
    298  - for i, ev := range evs {
    299  - if err, ok := ev.(*terminalapi.Error); ok != tc.wantErr {
    300  - t.Fatalf("toTermdashEvents => unexpected error:%v, wantErr: %v", err, tc.wantErr)
    301  - }
    302  - if _, ok := ev.(*terminalapi.Error); ok {
    303  - return
    304  - }
     304 + ev := evs[0]
    305 305   
    306  - switch e := ev.(type) {
    307  - case *terminalapi.Keyboard:
    308  - if got, want := e.Key, tc.want[i]; got != want {
    309  - t.Errorf("toTermdashEvents => got key[%d] %v, want %v", got, i, want)
    310  - }
     306 + if err, ok := ev.(*terminalapi.Error); ok != tc.wantErr {
     307 + t.Fatalf("toTermdashEvents => unexpected error:%v, wantErr: %v", err, tc.wantErr)
     308 + }
     309 + if _, ok := ev.(*terminalapi.Error); ok {
     310 + return
     311 + }
    311 312   
    312  - default:
    313  - t.Fatalf("toTermdashEvents => unexpected event type %T", e)
     313 + switch e := ev.(type) {
     314 + case *terminalapi.Keyboard:
     315 + if got, want := e.Key, tc.want; got != want {
     316 + t.Errorf("toTermdashEvents => got key %v, want %v", got, want)
    314 317   }
     318 + 
     319 + default:
     320 + t.Fatalf("toTermdashEvents => unexpected event type %T", e)
    315 321   }
    316 322   })
    317 323   }
    skipped 2 lines
  • ■ ■ ■ ■ ■
    widgetapi/widgetapi.go
    skipped 138 lines
    139 139   WantMouse MouseScope
    140 140  }
    141 141   
     142 +// Meta provide additional metadata to widgets.
     143 +type Meta struct {
     144 + // Focused asserts whether the widget's container is focused.
     145 + Focused bool
     146 +}
     147 + 
    142 148  // Widget is a single widget on the dashboard.
    143 149  // Implementations must be thread safe.
    144 150  type Widget interface {
    skipped 4 lines
    149 155   //
    150 156   // The widget must not assume that the size of the canvas or its content
    151 157   // remains the same between calls.
    152  - Draw(cvs *canvas.Canvas) error
     158 + //
     159 + // The argument meta is guaranteed to be valid (i.e. non-nil).
     160 + Draw(cvs *canvas.Canvas, meta *Meta) error
    153 161   
    154 162   // Keyboard is called when the widget is focused on the dashboard and a key
    155 163   // shortcut the widget registered for was pressed. Only called if the widget
    skipped 23 lines
  • ■ ■ ■ ■
    widgets/barchart/barchart.go
    skipped 72 lines
    73 73   
    74 74  // Draw draws the BarChart widget onto the canvas.
    75 75  // Implements widgetapi.Widget.Draw.
    76  -func (bc *BarChart) Draw(cvs *canvas.Canvas) error {
     76 +func (bc *BarChart) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    77 77   bc.mu.Lock()
    78 78   defer bc.mu.Unlock()
    79 79   
    skipped 288 lines
  • ■ ■ ■ ■ ■
    widgets/barchart/barchart_test.go
    skipped 33 lines
    34 34   opts []Option
    35 35   update func(*BarChart) error // update gets called before drawing of the widget.
    36 36   canvas image.Rectangle
     37 + meta *widgetapi.Meta
    37 38   want func(size image.Point) *faketerm.Terminal
    38 39   wantCapacity int
    39 40   wantErr bool
    skipped 620 lines
    660 661   return
    661 662   }
    662 663   
    663  - err = bc.Draw(c)
     664 + err = bc.Draw(c, tc.meta)
    664 665   if (err != nil) != tc.wantDrawErr {
    665 666   t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
    666 667   }
    skipped 188 lines
  • ■ ■ ■ ■
    widgets/button/button.go
    skipped 111 lines
    112 112   
    113 113  // Draw draws the Button widget onto the canvas.
    114 114  // Implements widgetapi.Widget.Draw.
    115  -func (b *Button) Draw(cvs *canvas.Canvas) error {
     115 +func (b *Button) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    116 116   b.mu.Lock()
    117 117   defer b.mu.Unlock()
    118 118   
    skipped 90 lines
  • ■ ■ ■ ■ ■
    widgets/button/button_test.go
    skipped 70 lines
    71 71   opts []Option
    72 72   events []terminalapi.Event
    73 73   canvas image.Rectangle
     74 + meta *widgetapi.Meta
    74 75   
    75 76   // timeSince is used to replace time.Since for tests, leave nil to use
    76 77   // the original.
    skipped 558 lines
    635 636   if err != nil {
    636 637   t.Fatalf("canvas.New => unexpected error: %v", err)
    637 638   }
    638  - err = b.Draw(c)
     639 + err = b.Draw(c, tc.meta)
    639 640   if (err != nil) != tc.wantDrawErr {
    640 641   t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
    641 642   }
    skipped 46 lines
    688 689   t.Fatalf("canvas.New => unexpected error: %v", err)
    689 690   }
    690 691   
    691  - err = b.Draw(c)
     692 + err = b.Draw(c, tc.meta)
    692 693   if (err != nil) != tc.wantDrawErr {
    693 694   t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
    694 695   }
    skipped 153 lines
  • ■ ■ ■ ■ ■ ■
    widgets/donut/circle.go
    skipped 17 lines
    18 18   
    19 19  import (
    20 20   "image"
     21 + "math"
    21 22   
    22 23   "github.com/mum4k/termdash/internal/canvas/braille"
    23  - "github.com/mum4k/termdash/internal/numbers"
    24 24  )
    25 25   
    26 26  // startEndAngles given progress indicators and the desired start angle and
    skipped 6 lines
    33 33   }
    34 34   
    35 35   mult := float64(current) / float64(total)
    36  - angleSize := numbers.Round(float64(360) * mult)
     36 + angleSize := math.Round(float64(360) * mult)
    37 37   
    38 38   if angleSize == fullCircle {
    39 39   return 0, fullCircle
    40 40   }
    41  - end = startAngle + int(numbers.Round(float64(direction)*angleSize))
     41 + end = startAngle + int(math.Round(float64(direction)*angleSize))
    42 42   
    43 43   if end < 0 {
    44 44   end += fullCircle
    skipped 75 lines
  • ■ ■ ■ ■ ■
    widgets/donut/donut.go
    skipped 19 lines
    20 20   "errors"
    21 21   "fmt"
    22 22   "image"
     23 + "math"
    23 24   "sync"
    24 25   
    25 26   "github.com/mum4k/termdash/align"
    26 27   "github.com/mum4k/termdash/internal/alignfor"
     28 + "github.com/mum4k/termdash/internal/area"
    27 29   "github.com/mum4k/termdash/internal/canvas"
    28 30   "github.com/mum4k/termdash/internal/canvas/braille"
    29 31   "github.com/mum4k/termdash/internal/draw"
    30  - "github.com/mum4k/termdash/internal/numbers"
    31 32   "github.com/mum4k/termdash/internal/runewidth"
    32 33   "github.com/mum4k/termdash/terminal/terminalapi"
    33 34   "github.com/mum4k/termdash/widgetapi"
    skipped 121 lines
    155 156  // holeRadius calculates the radius of the "hole" in the donut.
    156 157  // Returns zero if no hole should be drawn.
    157 158  func (d *Donut) holeRadius(donutRadius int) int {
    158  - r := int(numbers.Round(float64(donutRadius) / 100 * float64(d.opts.donutHolePercent)))
     159 + r := int(math.Round(float64(donutRadius) / 100 * float64(d.opts.donutHolePercent)))
    159 160   if r < 2 { // Smallest possible circle radius.
    160 161   return 0
    161 162   }
    skipped 3 lines
    165 166  // drawText draws the text label showing the progress.
    166 167  // The text is only drawn if the radius of the donut "hole" is large enough to
    167 168  // accommodate it.
    168  -func (d *Donut) drawText(cvs *canvas.Canvas, mid image.Point, holeR int) error {
     169 +// The mid point addresses coordinates in pixels on a braille canvas.
     170 +// The donutAr is the cell area for the donut itself.
     171 +func (d *Donut) drawText(cvs *canvas.Canvas, donutAr image.Rectangle, mid image.Point, holeR int) error {
    169 172   cells, first := availableCells(mid, holeR)
    170 173   t := d.progressText()
    171 174   needCells := runewidth.StringWidth(t)
    skipped 1 lines
    173 176   return nil
    174 177   }
    175 178   
     179 + if donutAr.Min.Y > 0 {
     180 + // donutAr is what the braille canvas is created from, mid is relative
     181 + // to it.
     182 + // donutAr might have non-zero Y coordinate if we are displaying a text
     183 + // label.
     184 + first.Y += donutAr.Min.Y
     185 + }
    176 186   ar := image.Rect(first.X, first.Y, first.X+cells+2, first.Y+1)
    177 187   start, err := alignfor.Text(ar, t, align.HorizontalCenter, align.VerticalMiddle)
    178 188   if err != nil {
    skipped 5 lines
    184 194   return nil
    185 195  }
    186 196   
     197 +// drawLabel draws the text label in the area.
     198 +func (d *Donut) drawLabel(cvs *canvas.Canvas, labelAr image.Rectangle) error {
     199 + start, err := alignfor.Text(labelAr, d.opts.label, d.opts.labelAlign, align.VerticalMiddle)
     200 + if err != nil {
     201 + return err
     202 + }
     203 + if err := draw.Text(
     204 + cvs, d.opts.label, start,
     205 + draw.TextOverrunMode(draw.OverrunModeThreeDot),
     206 + draw.TextMaxX(labelAr.Max.X),
     207 + draw.TextCellOpts(d.opts.labelCellOpts...),
     208 + ); err != nil {
     209 + return err
     210 + }
     211 + return nil
     212 +}
     213 + 
    187 214  // Draw draws the Donut widget onto the canvas.
    188 215  // Implements widgetapi.Widget.Draw.
    189  -func (d *Donut) Draw(cvs *canvas.Canvas) error {
     216 +func (d *Donut) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    190 217   d.mu.Lock()
    191 218   defer d.mu.Unlock()
    192 219   
    193  - bc, err := braille.New(cvs.Area())
    194  - if err != nil {
    195  - return fmt.Errorf("braille.New => %v", err)
    196  - }
    197  - 
    198 220   startA, endA := startEndAngles(d.current, d.total, d.opts.startAngle, d.opts.direction)
    199 221   if startA == endA {
    200 222   // No progress recorded, so nothing to do.
    201 223   return nil
    202 224   }
    203 225   
     226 + var donutAr, labelAr image.Rectangle
     227 + if len(d.opts.label) > 0 {
     228 + d, l, err := donutAndLabel(cvs.Area())
     229 + if err != nil {
     230 + return err
     231 + }
     232 + donutAr = d
     233 + labelAr = l
     234 + 
     235 + } else {
     236 + donutAr = cvs.Area()
     237 + }
     238 + 
     239 + if donutAr.Dx() < minSize.X || donutAr.Dy() < minSize.Y {
     240 + // Reserving area for the label might have resulted in donutAr being
     241 + // too small.
     242 + return draw.ResizeNeeded(cvs)
     243 + }
     244 + 
     245 + bc, err := braille.New(donutAr)
     246 + if err != nil {
     247 + return fmt.Errorf("braille.New => %v", err)
     248 + }
     249 + 
    204 250   mid, r := midAndRadius(bc.Area())
    205 251   if err := draw.BrailleCircle(bc, mid, r,
    206 252   draw.BrailleCircleFilled(),
    skipped 17 lines
    224 270   }
    225 271   
    226 272   if !d.opts.hideTextProgress {
    227  - return d.drawText(cvs, mid, holeR)
     273 + if err := d.drawText(cvs, donutAr, mid, holeR); err != nil {
     274 + return err
     275 + }
     276 + }
     277 + 
     278 + if !labelAr.Empty() {
     279 + if err := d.drawLabel(cvs, labelAr); err != nil {
     280 + return err
     281 + }
    228 282   }
    229 283   return nil
    230 284  }
    skipped 8 lines
    239 293   return errors.New("the Donut widget doesn't support mouse events")
    240 294  }
    241 295   
     296 +// minSize is the smallest area we can draw donut on.
     297 +var minSize = image.Point{3, 3}
     298 + 
    242 299  // Options implements widgetapi.Widget.Options.
    243 300  func (d *Donut) Options() widgetapi.Options {
    244 301   return widgetapi.Options{
    skipped 2 lines
    247 304   Ratio: image.Point{braille.RowMult, braille.ColMult},
    248 305   
    249 306   // The smallest circle that "looks" like a circle on the canvas.
    250  - MinimumSize: image.Point{3, 3},
     307 + MinimumSize: minSize,
    251 308   WantKeyboard: widgetapi.KeyScopeNone,
    252 309   WantMouse: widgetapi.MouseScopeNone,
    253 310   }
    254 311  }
    255 312   
     313 +// donutAndLabel splits the canvas area into square area for the donut and an
     314 +// area under the donut for the text label.
     315 +func donutAndLabel(cvsAr image.Rectangle) (donAr, labelAr image.Rectangle, err error) {
     316 + height := cvsAr.Dy()
     317 + // One line for the text label at the bottom.
     318 + top, labelAr, err := area.HSplitCells(cvsAr, height-1)
     319 + if err != nil {
     320 + return image.ZR, image.ZR, err
     321 + }
     322 + 
     323 + // Remove one line from the top too so the donut area remains square.
     324 + // When using braille, this effectively removes 4 pixels from both the top
     325 + // and the bottom. See braille.RowMult.
     326 + donAr, err = area.Shrink(top, 1, 0, 0, 0)
     327 + if err != nil {
     328 + return image.ZR, image.ZR, err
     329 + }
     330 + return donAr, labelAr, nil
     331 +}
     332 + 
  • ■ ■ ■ ■ ■ ■
    widgets/donut/donut_test.go
    skipped 18 lines
    19 19   "testing"
    20 20   
    21 21   "github.com/kylelemons/godebug/pretty"
     22 + "github.com/mum4k/termdash/align"
    22 23   "github.com/mum4k/termdash/cell"
    23 24   "github.com/mum4k/termdash/internal/canvas"
    24 25   "github.com/mum4k/termdash/internal/canvas/braille/testbraille"
    skipped 11 lines
    36 37   opts []Option
    37 38   update func(*Donut) error // update gets called before drawing of the widget.
    38 39   canvas image.Rectangle
     40 + meta *widgetapi.Meta
    39 41   want func(size image.Point) *faketerm.Terminal
    40 42   wantNewErr bool
    41 43   wantUpdateErr bool // whether to expect an error on a call to the update function
    skipped 100 lines
    142 144   update: func(d *Donut) error {
    143 145   return d.Percent(100)
    144 146   },
    145  - canvas: image.Rect(0, 0, 1, 1),
    146  - wantDrawErr: true,
     147 + canvas: image.Rect(0, 0, 1, 1),
     148 + want: func(size image.Point) *faketerm.Terminal {
     149 + ft := faketerm.MustNew(size)
     150 + cvs := testcanvas.MustNew(ft.Area())
     151 + testdraw.MustResizeNeeded(cvs)
     152 + testcanvas.MustApply(cvs, ft)
     153 + return ft
     154 + },
    147 155   },
    148 156   {
    149 157   desc: "smallest valid donut, 100% progress",
    skipped 12 lines
    162 170   },
    163 171   },
    164 172   {
     173 + desc: "adding label to the smallest canvas makes it too small",
     174 + opts: []Option{
     175 + Label("hi"),
     176 + },
     177 + canvas: image.Rect(0, 0, 3, 3),
     178 + update: func(d *Donut) error {
     179 + return d.Percent(100)
     180 + },
     181 + want: func(size image.Point) *faketerm.Terminal {
     182 + ft := faketerm.MustNew(size)
     183 + cvs := testcanvas.MustNew(ft.Area())
     184 + testdraw.MustResizeNeeded(cvs)
     185 + testcanvas.MustApply(cvs, ft)
     186 + return ft
     187 + },
     188 + },
     189 + {
    165 190   desc: "New sets donut options",
    166 191   opts: []Option{
    167 192   CellOpts(
    skipped 132 lines
    300 325   },
    301 326   },
    302 327   {
     328 + desc: "draws hole and label",
     329 + opts: []Option{
     330 + Label("hi"),
     331 + },
     332 + canvas: image.Rect(0, 0, 6, 6),
     333 + update: func(d *Donut) error {
     334 + return d.Percent(100, HolePercent(50))
     335 + },
     336 + want: func(size image.Point) *faketerm.Terminal {
     337 + ft := faketerm.MustNew(size)
     338 + c := testcanvas.MustNew(ft.Area())
     339 + bc := testbraille.MustNew(ft.Area())
     340 + 
     341 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5, draw.BrailleCircleFilled())
     342 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 3,
     343 + draw.BrailleCircleFilled(),
     344 + draw.BrailleCircleClearPixels(),
     345 + )
     346 + testbraille.MustCopyTo(bc, c)
     347 + 
     348 + testdraw.MustText(c, "hi", image.Point{2, 5})
     349 + 
     350 + testcanvas.MustApply(c, ft)
     351 + return ft
     352 + },
     353 + },
     354 + {
    303 355   desc: "hole as large as donut",
    304 356   canvas: image.Rect(0, 0, 6, 6),
    305 357   update: func(d *Donut) error {
    skipped 270 lines
    576 628   return ft
    577 629   },
    578 630   },
     631 + {
     632 + desc: "displays text label under the donut",
     633 + opts: []Option{
     634 + Label("hi"),
     635 + },
     636 + canvas: image.Rect(0, 0, 7, 7),
     637 + update: func(d *Donut) error {
     638 + return d.Percent(100, HolePercent(80))
     639 + },
     640 + want: func(size image.Point) *faketerm.Terminal {
     641 + ft := faketerm.MustNew(size)
     642 + c := testcanvas.MustNew(ft.Area())
     643 + bc := testbraille.MustNew(c.Area())
     644 + 
     645 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
     646 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     647 + draw.BrailleCircleFilled(),
     648 + draw.BrailleCircleClearPixels(),
     649 + )
     650 + testbraille.MustCopyTo(bc, c)
     651 + 
     652 + testdraw.MustText(c, "100%", image.Point{2, 3})
     653 + 
     654 + testdraw.MustText(c, "hi", image.Point{2, 6})
     655 + 
     656 + testcanvas.MustApply(c, ft)
     657 + return ft
     658 + },
     659 + },
     660 + {
     661 + desc: "aligns text label center with option",
     662 + opts: []Option{
     663 + Label("hi"),
     664 + LabelAlign(align.HorizontalCenter),
     665 + },
     666 + canvas: image.Rect(0, 0, 7, 7),
     667 + update: func(d *Donut) error {
     668 + return d.Percent(100, HolePercent(80))
     669 + },
     670 + want: func(size image.Point) *faketerm.Terminal {
     671 + ft := faketerm.MustNew(size)
     672 + c := testcanvas.MustNew(ft.Area())
     673 + bc := testbraille.MustNew(c.Area())
     674 + 
     675 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
     676 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     677 + draw.BrailleCircleFilled(),
     678 + draw.BrailleCircleClearPixels(),
     679 + )
     680 + testbraille.MustCopyTo(bc, c)
     681 + 
     682 + testdraw.MustText(c, "100%", image.Point{2, 3})
     683 + 
     684 + testdraw.MustText(c, "hi", image.Point{2, 6})
     685 + 
     686 + testcanvas.MustApply(c, ft)
     687 + return ft
     688 + },
     689 + },
     690 + {
     691 + desc: "aligns text label left",
     692 + opts: []Option{
     693 + Label("hi"),
     694 + LabelAlign(align.HorizontalLeft),
     695 + },
     696 + canvas: image.Rect(0, 0, 7, 7),
     697 + update: func(d *Donut) error {
     698 + return d.Percent(100, HolePercent(80))
     699 + },
     700 + want: func(size image.Point) *faketerm.Terminal {
     701 + ft := faketerm.MustNew(size)
     702 + c := testcanvas.MustNew(ft.Area())
     703 + bc := testbraille.MustNew(c.Area())
     704 + 
     705 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
     706 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     707 + draw.BrailleCircleFilled(),
     708 + draw.BrailleCircleClearPixels(),
     709 + )
     710 + testbraille.MustCopyTo(bc, c)
     711 + 
     712 + testdraw.MustText(c, "100%", image.Point{2, 3})
     713 + 
     714 + testdraw.MustText(c, "hi", image.Point{0, 6})
     715 + 
     716 + testcanvas.MustApply(c, ft)
     717 + return ft
     718 + },
     719 + },
     720 + {
     721 + desc: "aligns text label right",
     722 + opts: []Option{
     723 + Label("hi"),
     724 + LabelAlign(align.HorizontalRight),
     725 + },
     726 + canvas: image.Rect(0, 0, 7, 7),
     727 + update: func(d *Donut) error {
     728 + return d.Percent(100, HolePercent(80))
     729 + },
     730 + want: func(size image.Point) *faketerm.Terminal {
     731 + ft := faketerm.MustNew(size)
     732 + c := testcanvas.MustNew(ft.Area())
     733 + bc := testbraille.MustNew(c.Area())
     734 + 
     735 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
     736 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     737 + draw.BrailleCircleFilled(),
     738 + draw.BrailleCircleClearPixels(),
     739 + )
     740 + testbraille.MustCopyTo(bc, c)
     741 + 
     742 + testdraw.MustText(c, "100%", image.Point{2, 3})
     743 + 
     744 + testdraw.MustText(c, "hi", image.Point{5, 6})
     745 + 
     746 + testcanvas.MustApply(c, ft)
     747 + return ft
     748 + },
     749 + },
     750 + {
     751 + desc: "sets cell options on text label",
     752 + opts: []Option{
     753 + Label(
     754 + "hi",
     755 + cell.FgColor(cell.ColorRed),
     756 + cell.BgColor(cell.ColorBlue),
     757 + ),
     758 + },
     759 + canvas: image.Rect(0, 0, 7, 7),
     760 + update: func(d *Donut) error {
     761 + return d.Percent(100, HolePercent(80))
     762 + },
     763 + want: func(size image.Point) *faketerm.Terminal {
     764 + ft := faketerm.MustNew(size)
     765 + c := testcanvas.MustNew(ft.Area())
     766 + bc := testbraille.MustNew(c.Area())
     767 + 
     768 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
     769 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     770 + draw.BrailleCircleFilled(),
     771 + draw.BrailleCircleClearPixels(),
     772 + )
     773 + testbraille.MustCopyTo(bc, c)
     774 + 
     775 + testdraw.MustText(c, "100%", image.Point{2, 3})
     776 + 
     777 + testdraw.MustText(
     778 + c,
     779 + "hi",
     780 + image.Point{2, 6},
     781 + draw.TextCellOpts(
     782 + cell.FgColor(cell.ColorRed),
     783 + cell.BgColor(cell.ColorBlue),
     784 + ),
     785 + )
     786 + 
     787 + testcanvas.MustApply(c, ft)
     788 + return ft
     789 + },
     790 + },
     791 + {
     792 + desc: "text label too long, gets trimmed",
     793 + opts: []Option{
     794 + Label(
     795 + "hello world",
     796 + ),
     797 + },
     798 + canvas: image.Rect(0, 0, 7, 7),
     799 + update: func(d *Donut) error {
     800 + return d.Percent(100, HolePercent(80))
     801 + },
     802 + want: func(size image.Point) *faketerm.Terminal {
     803 + ft := faketerm.MustNew(size)
     804 + c := testcanvas.MustNew(ft.Area())
     805 + bc := testbraille.MustNew(c.Area())
     806 + 
     807 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 6, draw.BrailleCircleFilled())
     808 + testdraw.MustBrailleCircle(bc, image.Point{6, 13}, 5,
     809 + draw.BrailleCircleFilled(),
     810 + draw.BrailleCircleClearPixels(),
     811 + )
     812 + testbraille.MustCopyTo(bc, c)
     813 + 
     814 + testdraw.MustText(c, "100%", image.Point{2, 3})
     815 + 
     816 + testdraw.MustText(c, "hello …", image.Point{0, 6})
     817 + 
     818 + testcanvas.MustApply(c, ft)
     819 + return ft
     820 + },
     821 + },
    579 822   }
    580 823   
    581 824   for _, tc := range tests {
    skipped 22 lines
    604 847   }
    605 848   }
    606 849   
    607  - err = d.Draw(c)
     850 + err = d.Draw(c, tc.meta)
    608 851   if (err != nil) != tc.wantDrawErr {
    609 852   t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
    610 853   }
    skipped 66 lines
  • ■ ■ ■ ■
    widgets/donut/donutdemo/donutdemo.go
    skipped 85 lines
    86 86   defer t.Close()
    87 87   
    88 88   ctx, cancel := context.WithCancel(context.Background())
    89  - green, err := donut.New(donut.CellOpts(cell.FgColor(cell.ColorGreen)))
     89 + green, err := donut.New(
     90 + donut.CellOpts(cell.FgColor(cell.ColorGreen)),
     91 + donut.Label("text label", cell.FgColor(cell.ColorGreen)),
     92 + )
    90 93   if err != nil {
    91 94   panic(err)
    92 95   }
    skipped 54 lines
  • ■ ■ ■ ■ ■ ■
    widgets/donut/options.go
    skipped 18 lines
    19 19  import (
    20 20   "fmt"
    21 21   
     22 + "github.com/mum4k/termdash/align"
    22 23   "github.com/mum4k/termdash/cell"
    23 24  )
    24 25   
    skipped 18 lines
    43 44   
    44 45   textCellOpts []cell.Option
    45 46   cellOpts []cell.Option
     47 + 
     48 + labelCellOpts []cell.Option
     49 + labelAlign align.Horizontal
     50 + label string
    46 51   
    47 52   // The angle in degrees that represents 0 and 100% of the progress.
    48 53   startAngle int
    skipped 25 lines
    74 79   cell.FgColor(cell.ColorDefault),
    75 80   cell.BgColor(cell.ColorDefault),
    76 81   },
     82 + labelAlign: DefaultLabelAlign,
    77 83   }
    78 84  }
    79 85   
    skipped 78 lines
    158 164   })
    159 165  }
    160 166   
     167 +// Label sets a text label to be displayed under the donut.
     168 +func Label(text string, cOpts ...cell.Option) Option {
     169 + return option(func(opts *options) {
     170 + opts.label = text
     171 + opts.labelCellOpts = cOpts
     172 + })
     173 +}
     174 + 
     175 +// DefaultLabelAlign is the default value for the LabelAlign option.
     176 +const DefaultLabelAlign = align.HorizontalCenter
     177 + 
     178 +// LabelAlign sets the alignment of the label under the donut.
     179 +func LabelAlign(la align.Horizontal) Option {
     180 + return option(func(opts *options) {
     181 + opts.labelAlign = la
     182 + })
     183 +}
     184 + 
  • ■ ■ ■ ■ ■ ■
    widgets/gauge/gauge.go
    skipped 15 lines
    16 16  package gauge
    17 17   
    18 18  import (
    19  - "bytes"
    20 19   "errors"
    21 20   "fmt"
    22 21   "image"
     22 + "strings"
    23 23   "sync"
    24 24   
    25 25   "github.com/mum4k/termdash/cell"
    skipped 150 lines
    176 176  // gaugeText returns full text to be displayed within the gauge, i.e. the
    177 177  // progress text and the optional label.
    178 178  func (g *Gauge) gaugeText() string {
    179  - var b bytes.Buffer
     179 + var b strings.Builder
    180 180   b.WriteString(g.progressText())
    181 181   if g.opts.textLabel != "" {
    182 182   if b.Len() > 0 {
    skipped 67 lines
    250 250   
    251 251  // Draw draws the Gauge widget onto the canvas.
    252 252  // Implements widgetapi.Widget.Draw.
    253  -func (g *Gauge) Draw(cvs *canvas.Canvas) error {
     253 +func (g *Gauge) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    254 254   g.mu.Lock()
    255 255   defer g.mu.Unlock()
    256 256   
    skipped 81 lines
  • ■ ■ ■ ■ ■
    widgets/gauge/gauge_test.go
    skipped 49 lines
    50 50   percent *percentCall // if set, the test case calls Gauge.Percent().
    51 51   absolute *absoluteCall // if set the test case calls Gauge.Absolute().
    52 52   canvas image.Rectangle
     53 + meta *widgetapi.Meta
    53 54   want func(size image.Point) *faketerm.Terminal
    54 55   wantErr bool
    55 56   wantUpdateErr bool // whether to expect an error on a call to Gauge.Percent() or Gauge.Absolute().
    skipped 723 lines
    779 780   
    780 781   }
    781 782   
    782  - err = g.Draw(c)
     783 + err = g.Draw(c, tc.meta)
    783 784   if (err != nil) != tc.wantDrawErr {
    784 785   t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
    785 786   }
    skipped 78 lines
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/scale.go
    skipped 17 lines
    18 18   
    19 19  import (
    20 20   "fmt"
     21 + "math"
    21 22   
    22 23   "github.com/mum4k/termdash/internal/canvas/braille"
    23  - "github.com/mum4k/termdash/internal/numbers"
    24 24  )
    25 25   
    26 26  // YScaleMode determines whether the Y scale is anchored to the zero value.
    skipped 139 lines
    166 166   diff := -1 * ys.Min.Value
    167 167   v += diff
    168 168   }
    169  - pos := int(numbers.Round(v / ys.Step.Rounded))
     169 + pos := int(math.Round(v / ys.Step.Rounded))
    170 170   return positionToY(pos, ys.brailleHeight)
    171 171  }
    172 172   
    skipped 109 lines
    282 282   if xs.Min.Value > 0 {
    283 283   fv -= xs.Min.Value
    284 284   }
    285  - return int(numbers.Round(fv / xs.Step.Rounded)), nil
     285 + return int(math.Round(fv / xs.Step.Rounded)), nil
    286 286  }
    287 287   
    288 288  // ValueToCell given a value, determines the X coordinate of the cell that
    skipped 17 lines
    306 306   if err != nil {
    307 307   return nil, err
    308 308   }
    309  - return NewValue(numbers.Round(v), xs.Min.NonZeroDecimals), nil
     309 + return NewValue(math.Round(v), xs.Min.NonZeroDecimals), nil
    310 310  }
    311 311   
    312 312  // positionToY, given a position within the height, returns the Y coordinate of
    skipped 23 lines
  • ■ ■ ■ ■ ■
    widgets/linechart/linechart.go
    skipped 18 lines
    19 19   "errors"
    20 20   "fmt"
    21 21   "image"
     22 + "math"
    22 23   "sort"
    23 24   "sync"
    24 25   
    skipped 31 lines
    56 57   v := make([]float64, len(values))
    57 58   copy(v, values)
    58 59   
    59  - min, max := numbers.MinMax(v)
     60 + min, max := minMax(v)
    60 61   return &seriesValues{
    61 62   values: v,
    62 63   min: min,
    skipped 112 lines
    175 176   maximums = append(maximums, lc.opts.yAxisCustomScale.max)
    176 177   }
    177 178   
    178  - min, _ := numbers.MinMax(minimums)
    179  - _, max := numbers.MinMax(maximums)
     179 + min, _ := minMax(minimums)
     180 + _, max := minMax(maximums)
     181 + 
    180 182   return min, max
    181 183  }
    182 184   
    skipped 13 lines
    196 198   
    197 199  // Series sets the values that should be displayed as the line chart with the
    198 200  // provided label.
     201 +// The values that should not be displayed on the line chart should be represented
     202 +// as math.NaN values on the values slice.
    199 203  // Subsequent calls with the same label replace any previously provided values.
    200 204  func (lc *LineChart) Series(label string, values []float64, opts ...SeriesOption) error {
    201 205   if label == "" {
    skipped 90 lines
    292 296   
    293 297  // Draw draws the values as line charts.
    294 298  // Implements widgetapi.Widget.Draw.
    295  -func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
     299 +func (lc *LineChart) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    296 300   lc.mu.Lock()
    297 301   defer lc.mu.Unlock()
    298 302   
    skipped 65 lines
    364 368  // drawSeries draws the graph representing the stored series.
    365 369  // Returns XDetails that might be adjusted to not start at zero value if some
    366 370  // of the series didn't fit the graphs and XAxisUnscaled was provided.
     371 +// If the series has NaN values they will be ignored and not draw on the graph.
    367 372  func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) (*axes.XDetails, error) {
    368 373   graphAr := lc.graphAr(cvs, xd, yd)
    369 374   bc, err := braille.New(graphAr)
    skipped 36 lines
    406 411   
    407 412   var prev float64
    408 413   for i := 1; i < len(sv.values); i++ {
     414 + v := sv.values[i]
    409 415   prev = sv.values[i-1]
     416 + 
     417 + // Skip the values that are missing.
     418 + if math.IsNaN(v) || math.IsNaN(prev) {
     419 + continue
     420 + }
     421 + 
    410 422   if i < int(xdZoomed.Scale.Min.Value)+1 || i > int(xdZoomed.Scale.Max.Value) {
    411 423   // Don't draw lines for values that aren't supposed to be visible.
    412 424   // These are either values outside of the current zoom or
    skipped 16 lines
    429 441   if err != nil {
    430 442   return nil, fmt.Errorf("failure for series %v[%d] on scale %v, yd.Scale.ValueToPixel(%v) => %v", name, i-1, yd.Scale, prev, err)
    431 443   }
    432  - v := sv.values[i]
     444 + 
    433 445   endY, err := yd.Scale.ValueToPixel(v)
    434 446   if err != nil {
    435 447   return nil, fmt.Errorf("failure for series %v[%d] on scale %v, yd.Scale.ValueToPixel(%v) => %v", name, i, yd.Scale, v, err)
    skipped 84 lines
    520 532   return maxLen - 1
    521 533  }
    522 534   
     535 +// minMax is a wrapper around numbers.MinMax that controls
     536 +// the output if the values are NaN and sets defaults if it's
     537 +// the case.
     538 +func minMax(values []float64) (x, y float64) {
     539 + min, max := numbers.MinMax(values)
     540 + if math.IsNaN(min) {
     541 + min = 0
     542 + }
     543 + if math.IsNaN(max) {
     544 + max = 0
     545 + }
     546 + return min, max
     547 +}
     548 + 
  • ■ ■ ■ ■ ■ ■
    widgets/linechart/linechart_test.go
    skipped 35 lines
    36 36   tests := []struct {
    37 37   desc string
    38 38   canvas image.Rectangle
     39 + meta *widgetapi.Meta
    39 40   opts []Option
    40 41   writes func(*LineChart) error
    41 42   want func(size image.Point) *faketerm.Terminal
    skipped 1121 lines
    1163 1164   }
    1164 1165   // Draw once so zoom tracker is initialized.
    1165 1166   cvs := testcanvas.MustNew(image.Rect(0, 0, 20, 10))
    1166  - if err := lc.Draw(cvs); err != nil {
     1167 + if err := lc.Draw(cvs, &widgetapi.Meta{}); err != nil {
    1167 1168   return err
    1168 1169   }
    1169 1170   return lc.Mouse(&terminalapi.Mouse{
    skipped 44 lines
    1214 1215   }
    1215 1216   // Draw once so zoom tracker is initialized.
    1216 1217   cvs := testcanvas.MustNew(image.Rect(0, 0, 20, 10))
    1217  - if err := lc.Draw(cvs); err != nil {
     1218 + if err := lc.Draw(cvs, &widgetapi.Meta{}); err != nil {
    1218 1219   return err
    1219 1220   }
    1220 1221   return lc.Mouse(&terminalapi.Mouse{
    skipped 44 lines
    1265 1266   }
    1266 1267   // Draw once so zoom tracker is initialized.
    1267 1268   cvs := testcanvas.MustNew(image.Rect(0, 0, 20, 10))
    1268  - if err := lc.Draw(cvs); err != nil {
     1269 + if err := lc.Draw(cvs, &widgetapi.Meta{}); err != nil {
    1269 1270   return err
    1270 1271   }
    1271 1272   return lc.Mouse(&terminalapi.Mouse{
    skipped 44 lines
    1316 1317   // Draw twice with different canvas size to simulate resize.
    1317 1318   {
    1318 1319   cvs := testcanvas.MustNew(image.Rect(0, 0, 20, 7))
    1319  - if err := lc.Draw(cvs); err != nil {
     1320 + if err := lc.Draw(cvs, &widgetapi.Meta{}); err != nil {
    1320 1321   return err
    1321 1322   }
    1322 1323   }
    1323 1324   {
    1324 1325   cvs := testcanvas.MustNew(image.Rect(0, 0, 20, 10))
    1325  - if err := lc.Draw(cvs); err != nil {
     1326 + if err := lc.Draw(cvs, &widgetapi.Meta{}); err != nil {
    1326 1327   return err
    1327 1328   }
    1328 1329   }
    skipped 51 lines
    1380 1381   
    1381 1382   // Draw once so zoom tracker is initialized.
    1382 1383   cvs := testcanvas.MustNew(image.Rect(0, 0, 11, 10))
    1383  - if err := lc.Draw(cvs); err != nil {
     1384 + if err := lc.Draw(cvs, &widgetapi.Meta{}); err != nil {
    1384 1385   return err
    1385 1386   }
    1386 1387   return lc.Mouse(&terminalapi.Mouse{
    skipped 48 lines
    1435 1436   
    1436 1437   // Draw once so zoom tracker is initialized.
    1437 1438   cvs := testcanvas.MustNew(image.Rect(0, 0, 11, 10))
    1438  - if err := lc.Draw(cvs); err != nil {
     1439 + if err := lc.Draw(cvs, &widgetapi.Meta{}); err != nil {
    1439 1440   return err
    1440 1441   }
    1441 1442   if err := lc.Mouse(&terminalapi.Mouse{
    skipped 84 lines
    1526 1527   return ft
    1527 1528   },
    1528 1529   },
     1530 + {
     1531 + desc: "all NaN values",
     1532 + canvas: image.Rect(0, 0, 20, 10),
     1533 + writes: func(lc *LineChart) error {
     1534 + return lc.Series("first", []float64{math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN()})
     1535 + },
     1536 + wantCapacity: 36,
     1537 + want: func(size image.Point) *faketerm.Terminal {
     1538 + ft := faketerm.MustNew(size)
     1539 + c := testcanvas.MustNew(ft.Area())
     1540 + 
     1541 + // Y and X axis.
     1542 + lines := []draw.HVLine{
     1543 + {Start: image.Point{1, 0}, End: image.Point{1, 8}},
     1544 + {Start: image.Point{1, 8}, End: image.Point{19, 8}},
     1545 + }
     1546 + testdraw.MustHVLines(c, lines)
     1547 + 
     1548 + // Value labels.
     1549 + testdraw.MustText(c, "0", image.Point{0, 7})
     1550 + testdraw.MustText(c, "0", image.Point{2, 9})
     1551 + testdraw.MustText(c, "1", image.Point{6, 9})
     1552 + testdraw.MustText(c, "2", image.Point{10, 9})
     1553 + testdraw.MustText(c, "3", image.Point{14, 9})
     1554 + testdraw.MustText(c, "4", image.Point{18, 9})
     1555 + 
     1556 + testcanvas.MustApply(c, ft)
     1557 + return ft
     1558 + },
     1559 + },
     1560 + {
     1561 + desc: "first and last NaN values",
     1562 + canvas: image.Rect(0, 0, 28, 10),
     1563 + writes: func(lc *LineChart) error {
     1564 + return lc.Series("first", []float64{math.NaN(), math.NaN(), 100, 150, math.NaN()})
     1565 + },
     1566 + wantCapacity: 44,
     1567 + want: func(size image.Point) *faketerm.Terminal {
     1568 + ft := faketerm.MustNew(size)
     1569 + c := testcanvas.MustNew(ft.Area())
     1570 + 
     1571 + // Y and X axis.
     1572 + lines := []draw.HVLine{
     1573 + {Start: image.Point{5, 0}, End: image.Point{5, 8}},
     1574 + {Start: image.Point{5, 8}, End: image.Point{27, 8}},
     1575 + }
     1576 + testdraw.MustHVLines(c, lines)
     1577 + 
     1578 + // Value labels.
     1579 + testdraw.MustText(c, "0", image.Point{4, 7})
     1580 + testdraw.MustText(c, "77.44", image.Point{0, 3})
     1581 + testdraw.MustText(c, "0", image.Point{6, 9})
     1582 + testdraw.MustText(c, "1", image.Point{11, 9})
     1583 + testdraw.MustText(c, "2", image.Point{16, 9})
     1584 + testdraw.MustText(c, "3", image.Point{22, 9})
     1585 + testdraw.MustText(c, "4", image.Point{27, 9})
     1586 + 
     1587 + graphAr := image.Rect(6, 0, 25, 8)
     1588 + bc := testbraille.MustNew(graphAr)
     1589 + testdraw.MustBrailleLine(bc, image.Point{21, 10}, image.Point{32, 0})
     1590 + testbraille.MustCopyTo(bc, c)
     1591 + 
     1592 + testcanvas.MustApply(c, ft)
     1593 + return ft
     1594 + },
     1595 + },
     1596 + {
     1597 + desc: "more values than capacity, X rescales with NaN values ignored",
     1598 + canvas: image.Rect(0, 0, 11, 10),
     1599 + writes: func(lc *LineChart) error {
     1600 + return lc.Series("first", []float64{0, 1, 2, 3, 4, 5, 6, 7, math.NaN(), math.NaN(), math.NaN(), math.NaN(), 12, 13, 14, 15, 16, 17, 18, 19})
     1601 + },
     1602 + wantCapacity: 12,
     1603 + want: func(size image.Point) *faketerm.Terminal {
     1604 + ft := faketerm.MustNew(size)
     1605 + c := testcanvas.MustNew(ft.Area())
     1606 + 
     1607 + // Y and X axis.
     1608 + lines := []draw.HVLine{
     1609 + {Start: image.Point{4, 0}, End: image.Point{4, 8}},
     1610 + {Start: image.Point{4, 8}, End: image.Point{10, 8}},
     1611 + }
     1612 + testdraw.MustHVLines(c, lines)
     1613 + 
     1614 + // Value labels.
     1615 + testdraw.MustText(c, "0", image.Point{3, 7})
     1616 + testdraw.MustText(c, "9.92", image.Point{0, 3})
     1617 + testdraw.MustText(c, "0", image.Point{5, 9})
     1618 + testdraw.MustText(c, "14", image.Point{9, 9})
     1619 + 
     1620 + // Braille line.
     1621 + graphAr := image.Rect(5, 0, 11, 8)
     1622 + bc := testbraille.MustNew(graphAr)
     1623 + testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{1, 29})
     1624 + testdraw.MustBrailleLine(bc, image.Point{1, 29}, image.Point{1, 28})
     1625 + testdraw.MustBrailleLine(bc, image.Point{1, 28}, image.Point{2, 26})
     1626 + testdraw.MustBrailleLine(bc, image.Point{2, 26}, image.Point{2, 25})
     1627 + testdraw.MustBrailleLine(bc, image.Point{2, 25}, image.Point{3, 23})
     1628 + testdraw.MustBrailleLine(bc, image.Point{3, 23}, image.Point{3, 21})
     1629 + testdraw.MustBrailleLine(bc, image.Point{3, 21}, image.Point{4, 20})
     1630 + testdraw.MustBrailleLine(bc, image.Point{7, 12}, image.Point{8, 10})
     1631 + testdraw.MustBrailleLine(bc, image.Point{8, 10}, image.Point{8, 8})
     1632 + testdraw.MustBrailleLine(bc, image.Point{8, 8}, image.Point{9, 7})
     1633 + testdraw.MustBrailleLine(bc, image.Point{9, 7}, image.Point{9, 5})
     1634 + testdraw.MustBrailleLine(bc, image.Point{9, 5}, image.Point{10, 4})
     1635 + testdraw.MustBrailleLine(bc, image.Point{10, 4}, image.Point{10, 2})
     1636 + testdraw.MustBrailleLine(bc, image.Point{10, 2}, image.Point{11, 0})
     1637 + testbraille.MustCopyTo(bc, c)
     1638 + 
     1639 + testcanvas.MustApply(c, ft)
     1640 + return ft
     1641 + },
     1642 + },
    1529 1643   }
    1530 1644   
    1531 1645   for _, tc := range tests {
    skipped 22 lines
    1554 1668   }
    1555 1669   
    1556 1670   {
    1557  - err := widget.Draw(c)
     1671 + err := widget.Draw(c, tc.meta)
    1558 1672   if (err != nil) != tc.wantDrawErr {
    1559 1673   t.Fatalf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
    1560 1674   }
    skipped 133 lines
  • ■ ■ ■ ■ ■
    widgets/segmentdisplay/segment_area.go
    skipped 60 lines
    61 61   canFit int
    62 62   taken int
    63 63   )
    64  - for i := 0; i < textLen; i++ {
     64 + for i := 0; ; {
    65 65   taken += segAr.Dx()
    66 66   
    67 67   if taken > cvsAr.Dx() {
    skipped 17 lines
    85 85   // So insert neither.
    86 86   break
    87 87   }
     88 + i++
    88 89   }
    89 90   return &segArea{
    90 91   segment: segAr,
    skipped 15 lines
    106 107   return nil, err
    107 108   }
    108 109   
    109  - if segAr.canFit >= textLen {
     110 + if textLen > 0 && segAr.canFit >= textLen {
    110 111   return segAr, nil
    111 112   }
    112 113   bestSegAr = segAr
    skipped 4 lines
  • ■ ■ ■ ■ ■
    widgets/segmentdisplay/segmentdisplay.go
    skipped 16 lines
    17 17  package segmentdisplay
    18 18   
    19 19  import (
    20  - "bytes"
    21 20   "errors"
    22 21   "fmt"
    23 22   "image"
     23 + "strings"
    24 24   "sync"
    25 25   
    26 26   "github.com/mum4k/termdash/internal/alignfor"
    skipped 16 lines
    43 43  // Implements widgetapi.Widget. This object is thread-safe.
    44 44  type SegmentDisplay struct {
    45 45   // buff contains the text to be displayed.
    46  - buff bytes.Buffer
     46 + buff strings.Builder
    47 47   
    48 48   // givenWOpts are write options given for the text in buff.
    49 49   givenWOpts []*writeOptions
    50 50   // wOptsTracker tracks the positions in a buff to which the givenWOpts apply.
    51 51   wOptsTracker *attrrange.Tracker
     52 + 
     53 + // lastCanFit is the number of segments that could fit the area the last
     54 + // time Draw was called.
     55 + lastCanFit int
    52 56   
    53 57   // mu protects the widget.
    54 58   mu sync.Mutex
    skipped 79 lines
    134 138   return nil
    135 139  }
    136 140   
     141 +// Capacity returns the number of characters that can fit into the canvas.
     142 +// This is essentially the number of individual segments that can fit on the
     143 +// canvas at the time the last call to draw. Returns zero if draw wasn't
     144 +// called.
     145 +//
     146 +// Note that this capacity changes each time the terminal resizes, so there is
     147 +// no guarantee this remains the same next time Draw is called.
     148 +// Should be used as a hint only.
     149 +func (sd *SegmentDisplay) Capacity() int {
     150 + sd.mu.Lock()
     151 + defer sd.mu.Unlock()
     152 + return sd.lastCanFit
     153 +}
     154 + 
    137 155  // Reset resets the widget back to empty content.
    138 156  func (sd *SegmentDisplay) Reset() {
    139 157   sd.mu.Lock()
    skipped 21 lines
    161 179   }
    162 180   
    163 181   need := sd.buff.Len()
    164  - if need <= segAr.canFit || sd.opts.maximizeSegSize {
     182 + if (need > 0 && need <= segAr.canFit) || sd.opts.maximizeSegSize {
    165 183   return segAr, nil
    166 184   }
    167 185   
    skipped 6 lines
    174 192   
    175 193  // Draw draws the SegmentDisplay widget onto the canvas.
    176 194  // Implements widgetapi.Widget.Draw.
    177  -func (sd *SegmentDisplay) Draw(cvs *canvas.Canvas) error {
     195 +func (sd *SegmentDisplay) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    178 196   sd.mu.Lock()
    179 197   defer sd.mu.Unlock()
    180 198   
    181  - if sd.buff.Len() == 0 {
    182  - return nil
    183  - }
    184  - 
    185 199   segAr, err := sd.preprocess(cvs.Area())
    186 200   if err != nil {
    187 201   return err
     202 + }
     203 + 
     204 + sd.lastCanFit = segAr.canFit
     205 + if sd.buff.Len() == 0 {
     206 + return nil
    188 207   }
    189 208   
    190 209   text := sd.buff.String()
    skipped 75 lines
  • ■ ■ ■ ■ ■ ■
    widgets/segmentdisplay/segmentdisplay_test.go
    skipped 44 lines
    45 45   opts []Option
    46 46   update func(*SegmentDisplay) error // update gets called before drawing of the widget.
    47 47   canvas image.Rectangle
     48 + meta *widgetapi.Meta
    48 49   want func(size image.Point) *faketerm.Terminal
     50 + wantCapacity int
    49 51   wantNewErr bool
    50 52   wantUpdateErr bool // whether to expect an error on a call to the update function
    51 53   wantDrawErr bool
    skipped 69 lines
    121 123   wantUpdateErr: true,
    122 124   },
    123 125   {
    124  - desc: "draws empty without text",
    125  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     126 + desc: "draws empty without text",
     127 + canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows),
     128 + wantCapacity: 1,
    126 129   },
    127 130   {
    128 131   desc: "draws multiple segments, all fit exactly",
    skipped 22 lines
    151 154   testcanvas.MustApply(cvs, ft)
    152 155   return ft
    153 156   },
     157 + wantCapacity: 3,
    154 158   },
    155 159   {
    156 160   desc: "write sanitizes text by default",
    skipped 13 lines
    170 174   testcanvas.MustApply(cvs, ft)
    171 175   return ft
    172 176   },
     177 + wantCapacity: 2,
    173 178   },
    174 179   {
    175 180   desc: "write sanitizes text with option",
    skipped 13 lines
    189 194   testcanvas.MustApply(cvs, ft)
    190 195   return ft
    191 196   },
     197 + wantCapacity: 2,
    192 198   },
    193 199   {
    194 200   desc: "aligns segment vertical middle by default",
    skipped 10 lines
    205 211   testcanvas.MustApply(cvs, ft)
    206 212   return ft
    207 213   },
     214 + wantCapacity: 1,
    208 215   },
    209 216   {
    210 217   desc: "subsequent calls to write overwrite previous text",
    skipped 13 lines
    224 231   testcanvas.MustApply(cvs, ft)
    225 232   return ft
    226 233   },
     234 + wantCapacity: 1,
    227 235   },
    228 236   {
    229 237   desc: "sets cell options per text chunk",
    skipped 38 lines
    268 276   testcanvas.MustApply(cvs, ft)
    269 277   return ft
    270 278   },
     279 + wantCapacity: 2,
    271 280   },
    272 281   {
    273  - desc: "reset resets the text content",
    274  - canvas: image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows+2),
     282 + desc: "reset resets the text content and reports capacity when maximizing fit and with gaps",
     283 + opts: []Option{
     284 + MaximizeDisplayedText(),
     285 + },
     286 + canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows+2),
     287 + update: func(sd *SegmentDisplay) error {
     288 + if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
     289 + return err
     290 + }
     291 + sd.Reset()
     292 + return nil
     293 + },
     294 + wantCapacity: 2,
     295 + },
     296 + {
     297 + desc: "reset resets the text content and reports capacity when maximizing fit and without gaps",
     298 + opts: []Option{
     299 + GapPercent(0),
     300 + MaximizeDisplayedText(),
     301 + },
     302 + canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows+2),
    275 303   update: func(sd *SegmentDisplay) error {
    276 304   if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
    277 305   return err
    skipped 1 lines
    279 307   sd.Reset()
    280 308   return nil
    281 309   },
     310 + wantCapacity: 3,
     311 + },
     312 + {
     313 + desc: "reset resets the text content and reports capacity when maximizing segment height and gaps",
     314 + opts: []Option{
     315 + MaximizeSegmentHeight(),
     316 + },
     317 + canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows+2),
     318 + update: func(sd *SegmentDisplay) error {
     319 + if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
     320 + return err
     321 + }
     322 + sd.Reset()
     323 + return nil
     324 + },
     325 + wantCapacity: 2,
     326 + },
     327 + {
     328 + desc: "reset resets the text content and reports capacity when maximizing segment height and no gaps",
     329 + opts: []Option{
     330 + GapPercent(0),
     331 + MaximizeSegmentHeight(),
     332 + },
     333 + canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows+2),
     334 + update: func(sd *SegmentDisplay) error {
     335 + if err := sd.Write([]*TextChunk{NewChunk("123")}); err != nil {
     336 + return err
     337 + }
     338 + sd.Reset()
     339 + return nil
     340 + },
     341 + wantCapacity: 2,
    282 342   },
    283 343   {
    284 344   desc: "reset resets provided cell options",
    skipped 20 lines
    305 365   testcanvas.MustApply(cvs, ft)
    306 366   return ft
    307 367   },
     368 + wantCapacity: 1,
    308 369   },
    309 370   {
    310 371   desc: "aligns segment vertical middle with option",
    skipped 13 lines
    324 385   testcanvas.MustApply(cvs, ft)
    325 386   return ft
    326 387   },
     388 + wantCapacity: 1,
    327 389   },
    328 390   {
    329 391   desc: "aligns segment vertical top with option",
    skipped 13 lines
    343 405   testcanvas.MustApply(cvs, ft)
    344 406   return ft
    345 407   },
     408 + wantCapacity: 1,
    346 409   },
    347 410   {
    348 411   desc: "options given to Write override those given to New so aligns top",
    skipped 16 lines
    365 428   testcanvas.MustApply(cvs, ft)
    366 429   return ft
    367 430   },
     431 + wantCapacity: 1,
    368 432   },
    369 433   {
    370 434   desc: "aligns segment vertical bottom with option",
    skipped 13 lines
    384 448   testcanvas.MustApply(cvs, ft)
    385 449   return ft
    386 450   },
     451 + wantCapacity: 1,
    387 452   },
    388 453   {
    389 454   desc: "aligns segment horizontal center by default",
    skipped 10 lines
    400 465   testcanvas.MustApply(cvs, ft)
    401 466   return ft
    402 467   },
     468 + wantCapacity: 1,
    403 469   },
    404 470   {
    405 471   desc: "aligns segment horizontal center with option",
    skipped 13 lines
    419 485   testcanvas.MustApply(cvs, ft)
    420 486   return ft
    421 487   },
     488 + wantCapacity: 1,
    422 489   },
    423 490   {
    424 491   desc: "aligns segment horizontal left with option",
    skipped 13 lines
    438 505   testcanvas.MustApply(cvs, ft)
    439 506   return ft
    440 507   },
     508 + wantCapacity: 1,
    441 509   },
    442 510   {
    443 511   desc: "aligns segment horizontal right with option",
    skipped 13 lines
    457 525   testcanvas.MustApply(cvs, ft)
    458 526   return ft
    459 527   },
     528 + wantCapacity: 1,
    460 529   },
    461 530   {
    462 531   desc: "draws multiple segments, not enough space, maximizes segment height with option",
    skipped 22 lines
    485 554   testcanvas.MustApply(cvs, ft)
    486 555   return ft
    487 556   },
     557 + wantCapacity: 2,
    488 558   },
    489 559   {
    490 560   desc: "draws multiple segments, not enough space, maximizes displayed text by default and fits all",
    skipped 22 lines
    513 583   testcanvas.MustApply(cvs, ft)
    514 584   return ft
    515 585   },
     586 + wantCapacity: 3,
    516 587   },
    517 588   {
    518 589   desc: "draws multiple segments, not enough space, maximizes displayed text but cannot fit all",
    skipped 22 lines
    541 612   testcanvas.MustApply(cvs, ft)
    542 613   return ft
    543 614   },
     615 + wantCapacity: 3,
    544 616   },
    545 617   {
    546 618   desc: "draws multiple segments, not enough space, maximizes displayed text with option",
    skipped 23 lines
    570 642   testcanvas.MustApply(cvs, ft)
    571 643   return ft
    572 644   },
     645 + wantCapacity: 3,
    573 646   },
    574 647   {
    575 648   desc: "draws multiple segments with a gap by default",
    skipped 19 lines
    595 668   testcanvas.MustApply(cvs, ft)
    596 669   return ft
    597 670   },
     671 + wantCapacity: 3,
    598 672   },
    599 673   {
    600 674   desc: "draws multiple segments with a gap, exact fit",
    skipped 22 lines
    623 697   testcanvas.MustApply(cvs, ft)
    624 698   return ft
    625 699   },
     700 + wantCapacity: 3,
    626 701   },
    627 702   {
    628 703   desc: "draws multiple segments with a larger gap",
    skipped 21 lines
    650 725   testcanvas.MustApply(cvs, ft)
    651 726   return ft
    652 727   },
     728 + wantCapacity: 2,
    653 729   },
    654 730   {
    655 731   desc: "draws multiple segments with a gap, not all fit, maximizes displayed text",
    skipped 22 lines
    678 754   testcanvas.MustApply(cvs, ft)
    679 755   return ft
    680 756   },
     757 + wantCapacity: 3,
    681 758   },
    682 759   {
    683 760   desc: "draws multiple segments with a gap, not all fit, last segment would fit without a gap",
    skipped 22 lines
    706 783   testcanvas.MustApply(cvs, ft)
    707 784   return ft
    708 785   },
     786 + wantCapacity: 3,
    709 787   },
    710 788   {
    711 789   desc: "draws multiple segments with a gap, not enough space, maximizes segment height with option",
    skipped 22 lines
    734 812   testcanvas.MustApply(cvs, ft)
    735 813   return ft
    736 814   },
     815 + wantCapacity: 2,
    737 816   },
    738 817   {
    739 818   desc: "regression for #174, protects against external data mutation",
    skipped 28 lines
    768 847   testcanvas.MustApply(cvs, ft)
    769 848   return ft
    770 849   },
     850 + wantCapacity: 3,
    771 851   },
    772 852   }
    773 853   
    skipped 22 lines
    796 876   }
    797 877   }
    798 878   
    799  - err = sd.Draw(c)
     879 + err = sd.Draw(c, tc.meta)
    800 880   if (err != nil) != tc.wantDrawErr {
    801 881   t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
    802 882   }
    skipped 19 lines
    822 902   
    823 903   if diff := faketerm.Diff(want, got); diff != "" {
    824 904   t.Errorf("Draw => %v", diff)
     905 + }
     906 + 
     907 + if gotCapacity := sd.Capacity(); gotCapacity != tc.wantCapacity {
     908 + t.Errorf("Capacity => %d, want %d", gotCapacity, tc.wantCapacity)
    825 909   }
    826 910   })
    827 911   }
    skipped 39 lines
  • ■ ■ ■ ■
    widgets/sparkline/sparkline.go
    skipped 65 lines
    66 66   
    67 67  // Draw draws the SparkLine widget onto the canvas.
    68 68  // Implements widgetapi.Widget.Draw.
    69  -func (sl *SparkLine) Draw(cvs *canvas.Canvas) error {
     69 +func (sl *SparkLine) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    70 70   sl.mu.Lock()
    71 71   defer sl.mu.Unlock()
    72 72   
    skipped 182 lines
  • ■ ■ ■ ■ ■
    widgets/sparkline/sparkline_test.go
    skipped 33 lines
    34 34   opts []Option
    35 35   update func(*SparkLine) error // update gets called before drawing of the widget.
    36 36   canvas image.Rectangle
     37 + meta *widgetapi.Meta
    37 38   want func(size image.Point) *faketerm.Terminal
    38 39   wantCapacity int
    39 40   wantErr bool
    skipped 435 lines
    475 476   return
    476 477   }
    477 478   
    478  - err = sp.Draw(c)
     479 + err = sp.Draw(c, tc.meta)
    479 480   if (err != nil) != tc.wantDrawErr {
    480 481   t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
    481 482   }
    skipped 91 lines
  • ■ ■ ■ ■ ■ ■
    widgets/sparkline/sparks.go
    skipped 18 lines
    19 19   
    20 20  import (
    21 21   "fmt"
     22 + "math"
    22 23   
    23  - "github.com/mum4k/termdash/internal/numbers"
    24 24   "github.com/mum4k/termdash/internal/runewidth"
    25 25  )
    26 26   
    skipped 48 lines
    75 75   scale := float64(cellSparks) * float64(vertCells) / float64(max)
    76 76   
    77 77   // How many smallest spark elements are needed to represent the value.
    78  - elements := int(numbers.Round(float64(value) * scale))
     78 + elements := int(math.Round(float64(value) * scale))
    79 79   
    80 80   b := blocks{
    81 81   full: elements / cellSparks,
    skipped 20 lines
  • ■ ■ ■ ■
    widgets/text/text.go
    skipped 205 lines
    206 206   
    207 207  // Draw draws the text onto the canvas.
    208 208  // Implements widgetapi.Widget.Draw.
    209  -func (t *Text) Draw(cvs *canvas.Canvas) error {
     209 +func (t *Text) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
    210 210   t.mu.Lock()
    211 211   defer t.mu.Unlock()
    212 212   
    skipped 75 lines
  • ■ ■ ■ ■ ■ ■
    widgets/text/text_test.go
    skipped 34 lines
    35 35   tests := []struct {
    36 36   desc string
    37 37   canvas image.Rectangle
     38 + meta *widgetapi.Meta
    38 39   opts []Option
    39 40   writes func(*Text) error
    40 41   events func(*Text)
    skipped 560 lines
    601 602   },
    602 603   events: func(widget *Text) {
    603 604   // Draw once to roll the content all the way down before we scroll.
    604  - if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
     605 + if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3)), &widgetapi.Meta{}); err != nil {
    605 606   panic(err)
    606 607   }
    607 608   widget.Mouse(&terminalapi.Mouse{
    skipped 22 lines
    630 631   },
    631 632   events: func(widget *Text) {
    632 633   // Draw once to roll the content all the way down before we scroll.
    633  - if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
     634 + if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3)), &widgetapi.Meta{}); err != nil {
    634 635   panic(err)
    635 636   }
    636 637   widget.Keyboard(&terminalapi.Keyboard{
    skipped 22 lines
    659 660   },
    660 661   events: func(widget *Text) {
    661 662   // Draw once to roll the content all the way down before we scroll.
    662  - if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
     663 + if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3)), &widgetapi.Meta{}); err != nil {
    663 664   panic(err)
    664 665   }
    665 666   widget.Keyboard(&terminalapi.Keyboard{
    skipped 23 lines
    689 690   },
    690 691   events: func(widget *Text) {
    691 692   // Draw once to roll the content all the way down before we scroll.
    692  - if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
     693 + if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3)), &widgetapi.Meta{}); err != nil {
    693 694   panic(err)
    694 695   }
    695 696   widget.Mouse(&terminalapi.Mouse{
    skipped 23 lines
    719 720   },
    720 721   events: func(widget *Text) {
    721 722   // Draw once to roll the content all the way down before we scroll.
    722  - if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
     723 + if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3)), &widgetapi.Meta{}); err != nil {
    723 724   panic(err)
    724 725   }
    725 726   widget.Keyboard(&terminalapi.Keyboard{
    skipped 23 lines
    749 750   },
    750 751   events: func(widget *Text) {
    751 752   // Draw once to roll the content all the way down before we scroll.
    752  - if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3))); err != nil {
     753 + if err := widget.Draw(testcanvas.MustNew(image.Rect(0, 0, 10, 3)), &widgetapi.Meta{}); err != nil {
    753 754   panic(err)
    754 755   }
    755 756   widget.Keyboard(&terminalapi.Keyboard{
    skipped 42 lines
    798 799   tc.events(widget)
    799 800   }
    800 801   
    801  - if err := widget.Draw(c); err != nil {
     802 + if err := widget.Draw(c, tc.meta); err != nil {
    802 803   t.Fatalf("Draw => unexpected error: %v", err)
    803 804   }
    804 805   
    skipped 58 lines
  • ■ ■ ■ ■ ■ ■
    widgets/textinput/editor.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 textinput
     16 + 
     17 +// editor.go contains data types that edit the content of the text input field.
     18 + 
     19 +import (
     20 + "fmt"
     21 + "strings"
     22 + 
     23 + "github.com/mum4k/termdash/internal/numbers"
     24 + "github.com/mum4k/termdash/internal/runewidth"
     25 +)
     26 + 
     27 +// fieldData are the data currently present inside the text input field.
     28 +type fieldData []rune
     29 + 
     30 +// String implements fmt.Stringer.
     31 +func (fd fieldData) String() string {
     32 + var b strings.Builder
     33 + for _, r := range fd {
     34 + b.WriteRune(r)
     35 + }
     36 + return fmt.Sprintf("%q", b.String())
     37 +}
     38 + 
     39 +// insertAt inserts rune at the specified index.
     40 +func (fd *fieldData) insertAt(idx int, r rune) {
     41 + *fd = append(
     42 + (*fd)[:idx],
     43 + append(fieldData{r}, (*fd)[idx:]...)...,
     44 + )
     45 +}
     46 + 
     47 +// deleteAt deletes rune at the specified index.
     48 +func (fd *fieldData) deleteAt(idx int) {
     49 + *fd = append((*fd)[:idx], (*fd)[idx+1:]...)
     50 +}
     51 + 
     52 +// cellsBefore given an endIdx calculates startIdx that results in range that
     53 +// will take at most the provided number of cells to print on the screen.
     54 +func (fd *fieldData) cellsBefore(cells, endIdx int) int {
     55 + if endIdx == 0 {
     56 + return 0
     57 + }
     58 + 
     59 + usedCells := 0
     60 + for i := endIdx; i > 0; i-- {
     61 + prev := (*fd)[i-1]
     62 + width := runewidth.RuneWidth(prev)
     63 + 
     64 + if usedCells+width > cells {
     65 + return i
     66 + }
     67 + usedCells += width
     68 + }
     69 + return 0
     70 +}
     71 + 
     72 +// cellsAfter given a startIdx calculates endIdx that results in range that
     73 +// will take at most the provided number of cells to print on the screen.
     74 +func (fd *fieldData) cellsAfter(cells, startIdx int) int {
     75 + if startIdx >= len(*fd) || cells == 0 {
     76 + return startIdx
     77 + }
     78 + 
     79 + first := (*fd)[startIdx]
     80 + usedCells := runewidth.RuneWidth(first)
     81 + for i := startIdx + 1; i < len(*fd); i++ {
     82 + r := (*fd)[i]
     83 + width := runewidth.RuneWidth(r)
     84 + if usedCells+width > cells {
     85 + return i
     86 + }
     87 + usedCells += width
     88 + }
     89 + return len(*fd)
     90 +}
     91 + 
     92 +// minForArrows is the smallest number of cells in the window where we can
     93 +// indicate hidden text with left and right arrow.
     94 +const minForArrows = 3
     95 + 
     96 +// curMinIdx returns the lowest acceptable index for cursor position that is
     97 +// still within the visible range.
     98 +func curMinIdx(start, cells int) int {
     99 + if start == 0 || cells < minForArrows {
     100 + // The very first rune is visible, so the cursor can go all the way to
     101 + // the start.
     102 + return start
     103 + }
     104 + 
     105 + // When the first rune isn't visible, the cursor cannot go on the first
     106 + // cell in the visible range since it contains the left arrow.
     107 + return start + 1
     108 +}
     109 + 
     110 +// curMaxIdx returns the highest acceptable index for cursor position that is
     111 +// still within the visible range given the number of runes in data.
     112 +func curMaxIdx(start, end, cells, runeCount int) int {
     113 + if end == runeCount+1 || cells < minForArrows {
     114 + // The last rune is visible, so the cursor can go all the way to the
     115 + // end.
     116 + return end - 1
     117 + }
     118 + 
     119 + // When the last rune isn't visible, the cursor cannot go on the last cell
     120 + // in the window that is reserved for appending text, since it contains the
     121 + // right arrow.
     122 + return end - 2
     123 +}
     124 + 
     125 +// shiftLeft shifts the visible range left so that it again contains the
     126 +// cursor.
     127 +// The visible range includes all fieldData indexes
     128 +// in range start <= idx < end.
     129 +func (fd *fieldData) shiftLeft(start, cells, curDataPos int) (int, int) {
     130 + var startIdx int
     131 + switch {
     132 + case curDataPos == 0 || cells < minForArrows:
     133 + startIdx = curDataPos
     134 + 
     135 + default:
     136 + startIdx = curDataPos - 1
     137 + }
     138 + forRunes := cells - 1
     139 + endIdx := fd.cellsAfter(forRunes, startIdx)
     140 + endIdx++ // Space for the cursor.
     141 + 
     142 + return startIdx, endIdx
     143 +}
     144 + 
     145 +// shiftRight shifts the visible range right so that it again contains the
     146 +// cursor.
     147 +// The visible range includes all fieldData indexes
     148 +// in range start <= idx < end.
     149 +func (fd *fieldData) shiftRight(start, cells, curDataPos int) (int, int) {
     150 + var endIdx int
     151 + switch dataLen := len(*fd); {
     152 + case curDataPos == dataLen:
     153 + // Cursor is in the empty space after the data.
     154 + // Print all runes until the end of data.
     155 + endIdx = dataLen
     156 + 
     157 + default:
     158 + // Cursor is within the data, print all runes including the one the
     159 + // cursor is on.
     160 + endIdx = curDataPos + 1
     161 + }
     162 + 
     163 + forRunes := cells - 1
     164 + startIdx := fd.cellsBefore(forRunes, endIdx)
     165 + 
     166 + // Invariant, if counting form the back ends in the middle of a full-width
     167 + // rune, cellsAfter doesn't include the full-width rune. This means that we
     168 + // might have recovered space for one half-with rune at the end if there is
     169 + // one.
     170 + endIdx = fd.cellsAfter(forRunes, startIdx)
     171 + endIdx++ // Space for the cursor.
     172 + 
     173 + return startIdx, endIdx
     174 +}
     175 + 
     176 +// lastVisible given an end index of visible range asserts whether the last
     177 +// rune in the data is visible.
     178 +// The visible range includes all fieldData indexes
     179 +// in range start <= idx < end.
     180 +func (fd *fieldData) lastVisible(end int) bool {
     181 + return end-1 >= len(*fd)
     182 +}
     183 + 
     184 +// runesIn returns all the runes in the visible range.
     185 +// The visible range includes all fieldData indexes
     186 +// in range start <= idx < end.
     187 +func (fd *fieldData) runesIn(start, end int) []rune {
     188 + var runes []rune
     189 + for i, r := range (*fd)[start:] {
     190 + if i+start > end-2 { // One last space is for the cursor after the text.
     191 + break
     192 + }
     193 + runes = append(runes, r)
     194 + }
     195 + return runes
     196 +}
     197 + 
     198 +// fitRunes starting from the firstRune index returns runes that take at most
     199 +// the specified number of cells. The last cell is reserved for a cursor
     200 +// position used for appending new runes.
     201 +// This might return smaller number of runes than the size of the range,
     202 +// depending on the width of the individual runes.
     203 +// Returns the text and the start and end positions within the data.
     204 +func (fd *fieldData) fitRunes(firstRune, curPos, cells int) (string, int, int) {
     205 + forRunes := cells - 1 // One cell reserved for the cursor when appending.
     206 + 
     207 + // Determine how many runes fit from the start.
     208 + start := firstRune
     209 + end := fd.cellsAfter(forRunes, start)
     210 + end++
     211 + 
     212 + if start > 0 && fd.lastVisible(end) {
     213 + // Start is in the middle, end is visible.
     214 + // Fit runes from the end.
     215 + end = len(*fd)
     216 + start = fd.cellsBefore(forRunes, end)
     217 + end++ // Space for the cursor within the visible range.
     218 + }
     219 + 
     220 + // The fitting of runes might have resulted in a visible range that no
     221 + // longer contains the cursor (it became shorter) or the cursor was outside
     222 + // to begin with (due to cursorLeft() or cursorRight() calls).
     223 + // Shift the range so the cursor is again inside.
     224 + if curPos < curMinIdx(start, cells) {
     225 + start, end = fd.shiftLeft(start, cells, curPos)
     226 + } else if curPos > curMaxIdx(start, end, cells, len(*fd)) {
     227 + start, end = fd.shiftRight(start, cells, curPos)
     228 + }
     229 + 
     230 + runes := fd.runesIn(start, end)
     231 + useArrows := cells >= minForArrows
     232 + var b strings.Builder
     233 + for i, r := range runes {
     234 + switch {
     235 + case useArrows && i == 0 && start > 0:
     236 + // Indicate that start is hidden by replacing the first visible
     237 + // rune with an arrow.
     238 + b.WriteRune('⇦')
     239 + if rw := runewidth.RuneWidth(r); rw == 2 {
     240 + // If the replaced rune was a full-width rune, place two arrows
     241 + // to keep the same space allocation as pre-calculated.
     242 + b.WriteRune('⇦')
     243 + }
     244 + 
     245 + default:
     246 + b.WriteRune(r)
     247 + }
     248 + }
     249 + 
     250 + if useArrows && !fd.lastVisible(end) {
     251 + // Indicate that end is hidden by placing an arrow at the end.
     252 + // THis has no impact on space allocation, since the last cell is
     253 + // always reserved for the cursor or the arrow.
     254 + b.WriteRune('⇨')
     255 + }
     256 + return b.String(), start, end
     257 +}
     258 + 
     259 +// fieldEditor maintains the cursor position and allows editing of the data in
     260 +// the text input field.
     261 +// This object isn't thread-safe.
     262 +type fieldEditor struct {
     263 + // data are the data currently present in the text input field.
     264 + data fieldData
     265 + 
     266 + // curDataPos is the current position of the cursor within the data.
     267 + // The cursor is allowed to go one cell beyond the data so appending is
     268 + // possible.
     269 + curDataPos int
     270 + 
     271 + // firstRune is the index of the first displayed rune in the text input
     272 + // field.
     273 + firstRune int
     274 + 
     275 + // width is the width of the text input field last time viewFor was called.
     276 + width int
     277 +}
     278 + 
     279 +// newFieldEditor returns a new fieldEditor instance.
     280 +func newFieldEditor() *fieldEditor {
     281 + return &fieldEditor{}
     282 +}
     283 + 
     284 +// minFieldWidth is the minimum supported width of the text input field.
     285 +const minFieldWidth = 4
     286 + 
     287 +// curCell returns the index of the cell the cursor is in within the text input field.
     288 +func (fe *fieldEditor) curCell(width int) int {
     289 + if width == 0 {
     290 + return 0
     291 + }
     292 + // The index of rune within the visible range the cursor is at.
     293 + runeNum := fe.curDataPos - fe.firstRune
     294 + 
     295 + cellNum := 0
     296 + rn := 0
     297 + for i, r := range fe.data {
     298 + if i < fe.firstRune {
     299 + continue
     300 + }
     301 + if rn >= runeNum {
     302 + break
     303 + }
     304 + rn++
     305 + cellNum += runewidth.RuneWidth(r)
     306 + }
     307 + return cellNum
     308 +}
     309 + 
     310 +// viewFor returns the currently visible data inside a text field with the
     311 +// specified width and the cursor position within the field.
     312 +func (fe *fieldEditor) viewFor(width int) (string, int, error) {
     313 + if min := minFieldWidth; width < min { // One for left arrow, two for one full-width rune and one for the cursor.
     314 + return "", -1, fmt.Errorf("width %d is too small, the minimum is %d", width, min)
     315 + }
     316 + runes, start, _ := fe.data.fitRunes(fe.firstRune, fe.curDataPos, width)
     317 + fe.firstRune = start
     318 + fe.width = width
     319 + return runes, fe.curCell(width), nil
     320 +}
     321 + 
     322 +// content returns the string content in the field editor.
     323 +func (fe *fieldEditor) content() string {
     324 + return string(fe.data)
     325 +}
     326 + 
     327 +// reset resets the content back to zero.
     328 +func (fe *fieldEditor) reset() {
     329 + *fe = *newFieldEditor()
     330 +}
     331 + 
     332 +// insert inserts the rune at the current position of the cursor.
     333 +func (fe *fieldEditor) insert(r rune) {
     334 + rw := runewidth.RuneWidth(r)
     335 + if rw == 0 {
     336 + // Don't insert invisible runes.
     337 + return
     338 + }
     339 + fe.data.insertAt(fe.curDataPos, r)
     340 + fe.curDataPos++
     341 +}
     342 + 
     343 +// delete deletes the rune at the current position of the cursor.
     344 +func (fe *fieldEditor) delete() {
     345 + if fe.curDataPos >= len(fe.data) {
     346 + // Cursor not on a rune, nothing to do.
     347 + return
     348 + }
     349 + fe.data.deleteAt(fe.curDataPos)
     350 +}
     351 + 
     352 +// deleteBefore deletes the rune that is immediately to the left of the cursor.
     353 +func (fe *fieldEditor) deleteBefore() {
     354 + if fe.curDataPos == 0 {
     355 + // Cursor at the beginning, nothing to do.
     356 + return
     357 + }
     358 + fe.cursorLeft()
     359 + fe.delete()
     360 +}
     361 + 
     362 +// cursorRight moves the cursor one position to the right.
     363 +func (fe *fieldEditor) cursorRight() {
     364 + fe.curDataPos, _ = numbers.MinMaxInts([]int{fe.curDataPos + 1, len(fe.data)})
     365 +}
     366 + 
     367 +// cursorLeft moves the cursor one position to the left.
     368 +func (fe *fieldEditor) cursorLeft() {
     369 + _, fe.curDataPos = numbers.MinMaxInts([]int{fe.curDataPos - 1, 0})
     370 +}
     371 + 
     372 +// cursorStart moves the cursor to the beginning of the data.
     373 +func (fe *fieldEditor) cursorStart() {
     374 + fe.curDataPos = 0
     375 +}
     376 + 
     377 +// cursorEnd moves the cursor to the end of the data.
     378 +func (fe *fieldEditor) cursorEnd() {
     379 + fe.curDataPos = len(fe.data)
     380 +}
     381 + 
     382 +// cursorRelCell sets the cursor onto the cell index within the visible
     383 +// area.
     384 +// If the index falls before the window, the cursor is moved onto the first
     385 +// visible position.
     386 +// If the pos falls after the end of data, the cursor is moved onto the last
     387 +// visible position.
     388 +func (fe *fieldEditor) cursorRelCell(cellIdx int) {
     389 + runes, start, end := fe.data.fitRunes(fe.firstRune, fe.curDataPos, fe.width)
     390 + minDataIdx := curMinIdx(start, fe.width)
     391 + maxDataIdx := curMaxIdx(start, end, fe.width, len(fe.data))
     392 + 
     393 + // Index of the rune we should move the cursor to relative to the visible
     394 + // range.
     395 + var relRuneIdx int
     396 + var cell int
     397 + for _, r := range runes {
     398 + cell += runewidth.RuneWidth(r)
     399 + if cell > cellIdx {
     400 + break
     401 + }
     402 + relRuneIdx++
     403 + }
     404 + 
     405 + // Absolute index of the rune we should move the cursor to.
     406 + dataIdx := fe.firstRune + relRuneIdx
     407 + switch {
     408 + case dataIdx < minDataIdx:
     409 + fe.curDataPos = minDataIdx
     410 + 
     411 + case dataIdx > maxDataIdx:
     412 + fe.curDataPos = maxDataIdx
     413 + 
     414 + default:
     415 + fe.curDataPos = dataIdx
     416 + }
     417 +}
     418 + 
  • ■ ■ ■ ■ ■ ■
    widgets/textinput/editor_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 textinput
     16 + 
     17 +import (
     18 + "fmt"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 +)
     23 + 
     24 +func TestData(t *testing.T) {
     25 + tests := []struct {
     26 + desc string
     27 + data fieldData
     28 + ops func(*fieldData)
     29 + want fieldData
     30 + }{
     31 + {
     32 + desc: "appends to empty data",
     33 + ops: func(fd *fieldData) {
     34 + fd.insertAt(0, 'a')
     35 + },
     36 + want: fieldData{'a'},
     37 + },
     38 + {
     39 + desc: "appends at the end of non-empty data",
     40 + data: fieldData{'a'},
     41 + ops: func(fd *fieldData) {
     42 + fd.insertAt(1, 'b')
     43 + fd.insertAt(2, 'c')
     44 + },
     45 + want: fieldData{'a', 'b', 'c'},
     46 + },
     47 + {
     48 + desc: "appends at the beginning of non-empty data",
     49 + data: fieldData{'a'},
     50 + ops: func(fd *fieldData) {
     51 + fd.insertAt(0, 'b')
     52 + fd.insertAt(0, 'c')
     53 + },
     54 + want: fieldData{'c', 'b', 'a'},
     55 + },
     56 + {
     57 + desc: "deletes the last rune, result in empty",
     58 + data: fieldData{'a'},
     59 + ops: func(fd *fieldData) {
     60 + fd.deleteAt(0)
     61 + },
     62 + want: fieldData{},
     63 + },
     64 + {
     65 + desc: "deletes the last rune, result in non-empty",
     66 + data: fieldData{'a', 'b'},
     67 + ops: func(fd *fieldData) {
     68 + fd.deleteAt(1)
     69 + },
     70 + want: fieldData{'a'},
     71 + },
     72 + {
     73 + desc: "deletes runes in the middle",
     74 + data: fieldData{'a', 'b', 'c', 'd'},
     75 + ops: func(fd *fieldData) {
     76 + fd.deleteAt(1)
     77 + fd.deleteAt(1)
     78 + },
     79 + want: fieldData{'a', 'd'},
     80 + },
     81 + }
     82 + 
     83 + for _, tc := range tests {
     84 + t.Run(tc.desc, func(t *testing.T) {
     85 + got := tc.data
     86 + if tc.ops != nil {
     87 + tc.ops(&got)
     88 + }
     89 + t.Logf(fmt.Sprintf("got: %s", got))
     90 + if diff := pretty.Compare(tc.want, got); diff != "" {
     91 + t.Errorf("fieldData => unexpected diff (-want, +got):\n%s\n got: %q\nwant: %q", diff, got, tc.want)
     92 + }
     93 + })
     94 + }
     95 +}
     96 + 
     97 +func TestCellsBefore(t *testing.T) {
     98 + tests := []struct {
     99 + desc string
     100 + data fieldData
     101 + cells int
     102 + endIdx int
     103 + want int
     104 + }{
     105 + {
     106 + desc: "empty data and range",
     107 + cells: 1,
     108 + endIdx: 0,
     109 + want: 0,
     110 + },
     111 + {
     112 + desc: "requesting zero cells",
     113 + data: fieldData{'a', 'b', '世', 'd'},
     114 + cells: 0,
     115 + endIdx: 1,
     116 + want: 1,
     117 + },
     118 + {
     119 + desc: "data only has one rune",
     120 + data: fieldData{'a'},
     121 + cells: 1,
     122 + endIdx: 1,
     123 + want: 0,
     124 + },
     125 + {
     126 + desc: "non-empty data and empty range",
     127 + data: fieldData{'a', 'b', '世', 'd'},
     128 + cells: 1,
     129 + endIdx: 0,
     130 + want: 0,
     131 + },
     132 + {
     133 + desc: "more cells than runes from endIdx",
     134 + data: fieldData{'a', 'b', '世', 'd'},
     135 + cells: 10,
     136 + endIdx: 1,
     137 + want: 0,
     138 + },
     139 + {
     140 + desc: "less cells than runes from endIdx, stops on half-width rune",
     141 + data: fieldData{'a', 'b', '世', 'd'},
     142 + cells: 1,
     143 + endIdx: 2,
     144 + want: 1,
     145 + },
     146 + {
     147 + desc: "less cells than runes from endIdx, stops on full-width rune",
     148 + data: fieldData{'a', 'b', '世', 'd'},
     149 + cells: 2,
     150 + endIdx: 3,
     151 + want: 2,
     152 + },
     153 + {
     154 + desc: "less cells than runes from endIdx, full-width rune doesn't fit, no space for arrows",
     155 + data: fieldData{'a', 'b', '世', 'd'},
     156 + cells: 2,
     157 + endIdx: 4,
     158 + want: 3,
     159 + },
     160 + {
     161 + desc: "full-width runes only",
     162 + data: fieldData{'你', '好', '世', '界'},
     163 + cells: 7,
     164 + endIdx: 4,
     165 + want: 1,
     166 + },
     167 + }
     168 + 
     169 + for _, tc := range tests {
     170 + t.Run(tc.desc, func(t *testing.T) {
     171 + got := tc.data.cellsBefore(tc.cells, tc.endIdx)
     172 + if got != tc.want {
     173 + t.Errorf("cellsBefore => %d, want %d", got, tc.want)
     174 + }
     175 + })
     176 + }
     177 +}
     178 + 
     179 +func TestCellsAfter(t *testing.T) {
     180 + tests := []struct {
     181 + desc string
     182 + data fieldData
     183 + cells int
     184 + startIdx int
     185 + want int
     186 + }{
     187 + {
     188 + desc: "empty data and range",
     189 + cells: 1,
     190 + startIdx: 0,
     191 + want: 0,
     192 + },
     193 + {
     194 + desc: "empty data and range, non-zero start",
     195 + cells: 1,
     196 + startIdx: 1,
     197 + want: 1,
     198 + },
     199 + {
     200 + desc: "data only has one rune",
     201 + data: fieldData{'a'},
     202 + cells: 1,
     203 + startIdx: 0,
     204 + want: 1,
     205 + },
     206 + {
     207 + desc: "non-empty data and empty range",
     208 + data: fieldData{'a', 'b', '世', 'd'},
     209 + cells: 0,
     210 + startIdx: 1,
     211 + want: 1,
     212 + },
     213 + {
     214 + desc: "more cells than runes from startIdx",
     215 + data: fieldData{'a', 'b', '世', 'd'},
     216 + cells: 10,
     217 + startIdx: 1,
     218 + want: 4,
     219 + },
     220 + {
     221 + desc: "less cells than runes from startIdx, stops on half-width rune",
     222 + data: fieldData{'a', 'b', '世', 'd', 'e', 'f'},
     223 + cells: 2,
     224 + startIdx: 3,
     225 + want: 5,
     226 + },
     227 + {
     228 + desc: "less cells than runes from startIdx, stops on full-width rune",
     229 + data: fieldData{'a', 'b', '世', 'd'},
     230 + cells: 3,
     231 + startIdx: 1,
     232 + want: 3,
     233 + },
     234 + {
     235 + desc: "less cells than runes from startIdx, full-width rune doesn't fit",
     236 + data: fieldData{'a', 'b', '世', 'd'},
     237 + cells: 3,
     238 + startIdx: 0,
     239 + want: 2,
     240 + },
     241 + {
     242 + desc: "full-width runes only",
     243 + data: fieldData{'你', '好', '世', '界'},
     244 + cells: 7,
     245 + startIdx: 0,
     246 + want: 3,
     247 + },
     248 + }
     249 + 
     250 + for _, tc := range tests {
     251 + t.Run(tc.desc, func(t *testing.T) {
     252 + got := tc.data.cellsAfter(tc.cells, tc.startIdx)
     253 + if got != tc.want {
     254 + t.Errorf("cellsAfter => %d, want %d", got, tc.want)
     255 + }
     256 + })
     257 + }
     258 +}
     259 + 
     260 +func TestCurCell(t *testing.T) {
     261 + tests := []struct {
     262 + desc string
     263 + data fieldData
     264 + firstRune int
     265 + curDataPos int
     266 + width int
     267 + want int
     268 + wantErr bool
     269 + }{
     270 + {
     271 + desc: "empty data",
     272 + data: fieldData{},
     273 + curDataPos: 0,
     274 + want: 0,
     275 + },
     276 + {
     277 + desc: "cursor within the first page of data",
     278 + data: fieldData{'a', 'b', 'c', 'd'},
     279 + firstRune: 1,
     280 + curDataPos: 2,
     281 + width: 3,
     282 + want: 1,
     283 + },
     284 + {
     285 + desc: "cursor within the first page of data, after full-width rune",
     286 + data: fieldData{'a', '世', 'c', 'd'},
     287 + firstRune: 1,
     288 + curDataPos: 2,
     289 + width: 3,
     290 + want: 2,
     291 + },
     292 + {
     293 + desc: "cursor within the second page of data",
     294 + data: fieldData{'a', 'b', 'c', 'd', 'e', 'f'},
     295 + firstRune: 3,
     296 + curDataPos: 4,
     297 + width: 3,
     298 + want: 1,
     299 + },
     300 + {
     301 + desc: "cursor within the second page of data, after full-width rune",
     302 + data: fieldData{'a', 'b', 'c', '世', 'e', 'f'},
     303 + firstRune: 3,
     304 + curDataPos: 4,
     305 + width: 3,
     306 + want: 2,
     307 + },
     308 + }
     309 + 
     310 + for _, tc := range tests {
     311 + t.Run(tc.desc, func(t *testing.T) {
     312 + fe := newFieldEditor()
     313 + fe.data = tc.data
     314 + fe.firstRune = tc.firstRune
     315 + fe.curDataPos = tc.curDataPos
     316 + got := fe.curCell(tc.width)
     317 + if got != tc.want {
     318 + t.Errorf("curCell => %d, want %d", got, tc.want)
     319 + }
     320 + })
     321 + }
     322 +}
     323 + 
     324 +func TestFieldEditor(t *testing.T) {
     325 + tests := []struct {
     326 + desc string
     327 + width int
     328 + ops func(*fieldEditor) error
     329 + wantView string
     330 + wantContent string
     331 + wantCurIdx int
     332 + wantErr bool
     333 + }{
     334 + {
     335 + desc: "fails for width too small",
     336 + width: 3,
     337 + wantErr: true,
     338 + },
     339 + {
     340 + desc: "no data",
     341 + width: 4,
     342 + wantView: "",
     343 + wantContent: "",
     344 + wantCurIdx: 0,
     345 + },
     346 + {
     347 + desc: "data and cursor fit exactly",
     348 + width: 4,
     349 + ops: func(fe *fieldEditor) error {
     350 + fe.insert('a')
     351 + fe.insert('b')
     352 + fe.insert('c')
     353 + return nil
     354 + },
     355 + wantView: "abc",
     356 + wantContent: "abc",
     357 + wantCurIdx: 3,
     358 + },
     359 + {
     360 + desc: "longer data than the width, cursor at the end",
     361 + width: 4,
     362 + ops: func(fe *fieldEditor) error {
     363 + fe.insert('a')
     364 + fe.insert('b')
     365 + fe.insert('c')
     366 + fe.insert('d')
     367 + return nil
     368 + },
     369 + wantView: "⇦cd",
     370 + wantContent: "abcd",
     371 + wantCurIdx: 3,
     372 + },
     373 + {
     374 + desc: "longer data than the width, cursor at the end, has full-width runes",
     375 + width: 4,
     376 + ops: func(fe *fieldEditor) error {
     377 + fe.insert('a')
     378 + fe.insert('b')
     379 + fe.insert('c')
     380 + fe.insert('世')
     381 + return nil
     382 + },
     383 + wantView: "⇦世",
     384 + wantContent: "abc世",
     385 + wantCurIdx: 3,
     386 + },
     387 + {
     388 + desc: "width decreased, adjusts cursor and shifts data",
     389 + width: 4,
     390 + ops: func(fe *fieldEditor) error {
     391 + if _, _, err := fe.viewFor(5); err != nil {
     392 + return err
     393 + }
     394 + fe.insert('a')
     395 + fe.insert('b')
     396 + fe.insert('c')
     397 + fe.insert('d')
     398 + return nil
     399 + },
     400 + wantView: "⇦cd",
     401 + wantContent: "abcd",
     402 + wantCurIdx: 3,
     403 + },
     404 + {
     405 + desc: "cursor won't go right beyond the end of the data",
     406 + width: 4,
     407 + ops: func(fe *fieldEditor) error {
     408 + fe.insert('a')
     409 + fe.insert('b')
     410 + fe.insert('c')
     411 + fe.insert('d')
     412 + fe.cursorRight()
     413 + fe.cursorRight()
     414 + fe.cursorRight()
     415 + return nil
     416 + },
     417 + wantView: "⇦cd",
     418 + wantContent: "abcd",
     419 + wantCurIdx: 3,
     420 + },
     421 + {
     422 + desc: "moves cursor to the left",
     423 + width: 4,
     424 + ops: func(fe *fieldEditor) error {
     425 + fe.insert('a')
     426 + fe.insert('b')
     427 + fe.insert('c')
     428 + fe.insert('d')
     429 + if _, _, err := fe.viewFor(4); err != nil {
     430 + return err
     431 + }
     432 + fe.cursorLeft()
     433 + return nil
     434 + },
     435 + wantView: "⇦cd",
     436 + wantContent: "abcd",
     437 + wantCurIdx: 2,
     438 + },
     439 + {
     440 + desc: "scrolls content to the left, start becomes visible",
     441 + width: 4,
     442 + ops: func(fe *fieldEditor) error {
     443 + fe.insert('a')
     444 + fe.insert('b')
     445 + fe.insert('c')
     446 + fe.insert('d')
     447 + if _, _, err := fe.viewFor(4); err != nil {
     448 + return err
     449 + }
     450 + fe.cursorLeft()
     451 + fe.cursorLeft()
     452 + fe.cursorLeft()
     453 + return nil
     454 + },
     455 + wantView: "abc⇨",
     456 + wantContent: "abcd",
     457 + wantCurIdx: 1,
     458 + },
     459 + {
     460 + desc: "scrolls content to the left, both ends invisible",
     461 + width: 4,
     462 + ops: func(fe *fieldEditor) error {
     463 + fe.insert('a')
     464 + fe.insert('b')
     465 + fe.insert('c')
     466 + fe.insert('d')
     467 + fe.insert('e')
     468 + if _, _, err := fe.viewFor(4); err != nil {
     469 + return err
     470 + }
     471 + fe.cursorLeft()
     472 + fe.cursorLeft()
     473 + fe.cursorLeft()
     474 + return nil
     475 + },
     476 + wantView: "⇦cd⇨",
     477 + wantContent: "abcde",
     478 + wantCurIdx: 1,
     479 + },
     480 + {
     481 + desc: "scrolls left, then back right to make end visible again",
     482 + width: 4,
     483 + ops: func(fe *fieldEditor) error {
     484 + fe.insert('a')
     485 + fe.insert('b')
     486 + fe.insert('c')
     487 + fe.insert('d')
     488 + fe.insert('e')
     489 + if _, _, err := fe.viewFor(4); err != nil {
     490 + return err
     491 + }
     492 + fe.cursorLeft()
     493 + fe.cursorLeft()
     494 + fe.cursorLeft()
     495 + if _, _, err := fe.viewFor(4); err != nil {
     496 + return err
     497 + }
     498 + fe.cursorRight()
     499 + fe.cursorRight()
     500 + fe.cursorRight()
     501 + return nil
     502 + },
     503 + wantView: "⇦de",
     504 + wantContent: "abcde",
     505 + wantCurIdx: 3,
     506 + },
     507 + {
     508 + desc: "scrolls left, won't go beyond the start of data",
     509 + width: 4,
     510 + ops: func(fe *fieldEditor) error {
     511 + fe.insert('a')
     512 + fe.insert('b')
     513 + fe.insert('c')
     514 + fe.insert('d')
     515 + fe.insert('e')
     516 + if _, _, err := fe.viewFor(4); err != nil {
     517 + return err
     518 + }
     519 + fe.cursorLeft()
     520 + fe.cursorLeft()
     521 + fe.cursorLeft()
     522 + fe.cursorLeft()
     523 + fe.cursorLeft()
     524 + fe.cursorLeft()
     525 + return nil
     526 + },
     527 + wantView: "abc⇨",
     528 + wantContent: "abcde",
     529 + wantCurIdx: 0,
     530 + },
     531 + {
     532 + desc: "scrolls left, then back right won't go beyond the end of data",
     533 + width: 4,
     534 + ops: func(fe *fieldEditor) error {
     535 + fe.insert('a')
     536 + fe.insert('b')
     537 + fe.insert('c')
     538 + fe.insert('d')
     539 + fe.insert('e')
     540 + if _, _, err := fe.viewFor(4); err != nil {
     541 + return err
     542 + }
     543 + fe.cursorLeft()
     544 + fe.cursorLeft()
     545 + fe.cursorLeft()
     546 + if _, _, err := fe.viewFor(4); err != nil {
     547 + return err
     548 + }
     549 + fe.cursorRight()
     550 + fe.cursorRight()
     551 + fe.cursorRight()
     552 + fe.cursorRight()
     553 + return nil
     554 + },
     555 + wantView: "⇦de",
     556 + wantContent: "abcde",
     557 + wantCurIdx: 3,
     558 + },
     559 + {
     560 + desc: "have less data than width, all fits",
     561 + width: 4,
     562 + ops: func(fe *fieldEditor) error {
     563 + fe.insert('a')
     564 + fe.insert('b')
     565 + fe.insert('c')
     566 + if _, _, err := fe.viewFor(4); err != nil {
     567 + return err
     568 + }
     569 + return nil
     570 + },
     571 + wantView: "abc",
     572 + wantContent: "abc",
     573 + wantCurIdx: 3,
     574 + },
     575 + {
     576 + desc: "moves cursor to the start",
     577 + width: 4,
     578 + ops: func(fe *fieldEditor) error {
     579 + fe.insert('a')
     580 + fe.insert('b')
     581 + fe.insert('c')
     582 + fe.insert('d')
     583 + fe.insert('e')
     584 + if _, _, err := fe.viewFor(4); err != nil {
     585 + return err
     586 + }
     587 + fe.cursorStart()
     588 + return nil
     589 + },
     590 + wantView: "abc⇨",
     591 + wantContent: "abcde",
     592 + wantCurIdx: 0,
     593 + },
     594 + {
     595 + desc: "moves cursor to the end",
     596 + width: 4,
     597 + ops: func(fe *fieldEditor) error {
     598 + fe.insert('a')
     599 + fe.insert('b')
     600 + fe.insert('c')
     601 + fe.insert('d')
     602 + fe.insert('e')
     603 + if _, _, err := fe.viewFor(4); err != nil {
     604 + return err
     605 + }
     606 + fe.cursorStart()
     607 + if _, _, err := fe.viewFor(4); err != nil {
     608 + return err
     609 + }
     610 + fe.cursorEnd()
     611 + return nil
     612 + },
     613 + wantView: "⇦de",
     614 + wantContent: "abcde",
     615 + wantCurIdx: 3,
     616 + },
     617 + {
     618 + desc: "deletesBefore when cursor after the data",
     619 + width: 4,
     620 + ops: func(fe *fieldEditor) error {
     621 + fe.insert('a')
     622 + fe.insert('b')
     623 + fe.insert('c')
     624 + fe.insert('d')
     625 + fe.insert('e')
     626 + if _, _, err := fe.viewFor(4); err != nil {
     627 + return err
     628 + }
     629 + fe.deleteBefore()
     630 + return nil
     631 + },
     632 + wantView: "⇦cd",
     633 + wantContent: "abcd",
     634 + wantCurIdx: 3,
     635 + },
     636 + {
     637 + desc: "deletesBefore when cursor after the data, text has full-width rune",
     638 + width: 4,
     639 + ops: func(fe *fieldEditor) error {
     640 + fe.insert('a')
     641 + fe.insert('b')
     642 + fe.insert('c')
     643 + fe.insert('世')
     644 + fe.insert('e')
     645 + if _, _, err := fe.viewFor(4); err != nil {
     646 + return err
     647 + }
     648 + fe.deleteBefore()
     649 + return nil
     650 + },
     651 + wantView: "⇦世",
     652 + wantContent: "abc世",
     653 + wantCurIdx: 3,
     654 + },
     655 + {
     656 + desc: "deletesBefore when cursor in the middle",
     657 + width: 4,
     658 + ops: func(fe *fieldEditor) error {
     659 + fe.insert('a')
     660 + fe.insert('b')
     661 + fe.insert('c')
     662 + fe.insert('d')
     663 + fe.insert('e')
     664 + if _, _, err := fe.viewFor(4); err != nil {
     665 + return err
     666 + }
     667 + fe.cursorLeft()
     668 + fe.cursorLeft()
     669 + fe.cursorLeft()
     670 + if _, _, err := fe.viewFor(4); err != nil {
     671 + return err
     672 + }
     673 + fe.deleteBefore()
     674 + return nil
     675 + },
     676 + wantView: "acd⇨",
     677 + wantContent: "acde",
     678 + wantCurIdx: 1,
     679 + },
     680 + {
     681 + desc: "deletesBefore when cursor in the middle, full-width runes",
     682 + width: 4,
     683 + ops: func(fe *fieldEditor) error {
     684 + fe.insert('世')
     685 + fe.insert('b')
     686 + fe.insert('c')
     687 + fe.insert('d')
     688 + fe.insert('e')
     689 + if _, _, err := fe.viewFor(4); err != nil {
     690 + return err
     691 + }
     692 + fe.cursorLeft()
     693 + fe.cursorLeft()
     694 + fe.cursorLeft()
     695 + if _, _, err := fe.viewFor(4); err != nil {
     696 + return err
     697 + }
     698 + fe.deleteBefore()
     699 + return nil
     700 + },
     701 + wantView: "世c⇨",
     702 + wantContent: "世cde",
     703 + wantCurIdx: 2,
     704 + },
     705 + {
     706 + desc: "deletesBefore does nothing when cursor at the start",
     707 + width: 4,
     708 + ops: func(fe *fieldEditor) error {
     709 + fe.insert('a')
     710 + fe.insert('b')
     711 + fe.insert('c')
     712 + fe.insert('d')
     713 + fe.insert('e')
     714 + if _, _, err := fe.viewFor(4); err != nil {
     715 + return err
     716 + }
     717 + fe.cursorStart()
     718 + if _, _, err := fe.viewFor(4); err != nil {
     719 + return err
     720 + }
     721 + fe.deleteBefore()
     722 + return nil
     723 + },
     724 + wantView: "abc⇨",
     725 + wantContent: "abcde",
     726 + wantCurIdx: 0,
     727 + },
     728 + {
     729 + desc: "delete does nothing when cursor at the end",
     730 + width: 4,
     731 + ops: func(fe *fieldEditor) error {
     732 + fe.insert('a')
     733 + fe.insert('b')
     734 + fe.insert('c')
     735 + fe.insert('d')
     736 + fe.insert('e')
     737 + if _, _, err := fe.viewFor(4); err != nil {
     738 + return err
     739 + }
     740 + fe.delete()
     741 + return nil
     742 + },
     743 + wantView: "⇦de",
     744 + wantContent: "abcde",
     745 + wantCurIdx: 3,
     746 + },
     747 + {
     748 + desc: "delete in the middle, last rune remains hidden",
     749 + width: 4,
     750 + ops: func(fe *fieldEditor) error {
     751 + fe.insert('a')
     752 + fe.insert('b')
     753 + fe.insert('c')
     754 + fe.insert('d')
     755 + fe.insert('e')
     756 + if _, _, err := fe.viewFor(4); err != nil {
     757 + return err
     758 + }
     759 + fe.cursorStart()
     760 + if _, _, err := fe.viewFor(4); err != nil {
     761 + return err
     762 + }
     763 + fe.cursorRight()
     764 + fe.delete()
     765 + return nil
     766 + },
     767 + wantView: "acd⇨",
     768 + wantContent: "acde",
     769 + wantCurIdx: 1,
     770 + },
     771 + {
     772 + desc: "delete in the middle, last rune becomes visible",
     773 + width: 4,
     774 + ops: func(fe *fieldEditor) error {
     775 + fe.insert('a')
     776 + fe.insert('b')
     777 + fe.insert('c')
     778 + fe.insert('d')
     779 + fe.insert('e')
     780 + if _, _, err := fe.viewFor(4); err != nil {
     781 + return err
     782 + }
     783 + fe.cursorStart()
     784 + if _, _, err := fe.viewFor(4); err != nil {
     785 + return err
     786 + }
     787 + fe.cursorRight()
     788 + fe.delete()
     789 + fe.delete()
     790 + return nil
     791 + },
     792 + wantView: "ade",
     793 + wantContent: "ade",
     794 + wantCurIdx: 1,
     795 + },
     796 + {
     797 + desc: "delete in the middle, last full-width rune would be invisible, shifts to keep cursor in window",
     798 + width: 4,
     799 + ops: func(fe *fieldEditor) error {
     800 + fe.insert('a')
     801 + fe.insert('b')
     802 + fe.insert('c')
     803 + fe.insert('d')
     804 + fe.insert('世')
     805 + if _, _, err := fe.viewFor(4); err != nil {
     806 + return err
     807 + }
     808 + fe.cursorStart()
     809 + if _, _, err := fe.viewFor(4); err != nil {
     810 + return err
     811 + }
     812 + fe.cursorRight()
     813 + fe.cursorRight()
     814 + fe.delete()
     815 + fe.delete()
     816 + return nil
     817 + },
     818 + wantView: "⇦世",
     819 + wantContent: "ab世",
     820 + wantCurIdx: 1,
     821 + },
     822 + {
     823 + desc: "delete in the middle, last rune was and is visible",
     824 + width: 4,
     825 + ops: func(fe *fieldEditor) error {
     826 + fe.insert('a')
     827 + fe.insert('b')
     828 + fe.insert('c')
     829 + if _, _, err := fe.viewFor(4); err != nil {
     830 + return err
     831 + }
     832 + fe.cursorStart()
     833 + if _, _, err := fe.viewFor(4); err != nil {
     834 + return err
     835 + }
     836 + fe.cursorRight()
     837 + fe.delete()
     838 + return nil
     839 + },
     840 + wantView: "ac",
     841 + wantContent: "ac",
     842 + wantCurIdx: 1,
     843 + },
     844 + {
     845 + desc: "delete in the middle, last full-width rune was and is visible",
     846 + width: 5,
     847 + ops: func(fe *fieldEditor) error {
     848 + fe.insert('a')
     849 + fe.insert('b')
     850 + fe.insert('世')
     851 + if _, _, err := fe.viewFor(5); err != nil {
     852 + return err
     853 + }
     854 + fe.cursorStart()
     855 + if _, _, err := fe.viewFor(5); err != nil {
     856 + return err
     857 + }
     858 + fe.cursorRight()
     859 + fe.delete()
     860 + return nil
     861 + },
     862 + wantView: "a世",
     863 + wantContent: "a世",
     864 + wantCurIdx: 1,
     865 + },
     866 + {
     867 + desc: "delete last rune, contains full-width runes",
     868 + width: 5,
     869 + ops: func(fe *fieldEditor) error {
     870 + fe.insert('a')
     871 + fe.insert('b')
     872 + fe.insert('世')
     873 + if _, _, err := fe.viewFor(5); err != nil {
     874 + return err
     875 + }
     876 + fe.cursorStart()
     877 + if _, _, err := fe.viewFor(5); err != nil {
     878 + return err
     879 + }
     880 + fe.delete()
     881 + fe.delete()
     882 + fe.delete()
     883 + return nil
     884 + },
     885 + wantView: "",
     886 + wantContent: "",
     887 + wantCurIdx: 0,
     888 + },
     889 + {
     890 + desc: "half-width runes only, exact fit",
     891 + width: 4,
     892 + ops: func(fe *fieldEditor) error {
     893 + fe.insert('a')
     894 + fe.insert('b')
     895 + fe.insert('c')
     896 + if _, _, err := fe.viewFor(4); err != nil {
     897 + return err
     898 + }
     899 + return nil
     900 + },
     901 + wantView: "abc",
     902 + wantContent: "abc",
     903 + wantCurIdx: 3,
     904 + },
     905 + {
     906 + desc: "full-width runes only, exact fit",
     907 + width: 7,
     908 + ops: func(fe *fieldEditor) error {
     909 + fe.insert('你')
     910 + fe.insert('好')
     911 + fe.insert('世')
     912 + if _, _, err := fe.viewFor(7); err != nil {
     913 + return err
     914 + }
     915 + return nil
     916 + },
     917 + wantView: "你好世",
     918 + wantContent: "你好世",
     919 + wantCurIdx: 6,
     920 + },
     921 + {
     922 + desc: "half-width runes only, both ends hidden",
     923 + width: 4,
     924 + ops: func(fe *fieldEditor) error {
     925 + fe.insert('a')
     926 + fe.insert('b')
     927 + fe.insert('c')
     928 + fe.insert('d')
     929 + fe.insert('e')
     930 + if _, _, err := fe.viewFor(4); err != nil {
     931 + return err
     932 + }
     933 + fe.cursorLeft()
     934 + fe.cursorLeft()
     935 + fe.cursorLeft()
     936 + return nil
     937 + },
     938 + wantView: "⇦cd⇨",
     939 + wantContent: "abcde",
     940 + wantCurIdx: 1,
     941 + },
     942 + {
     943 + desc: "half-width runes only, both ends invisible, scrolls to make start visible",
     944 + width: 4,
     945 + ops: func(fe *fieldEditor) error {
     946 + fe.insert('a')
     947 + fe.insert('b')
     948 + fe.insert('c')
     949 + fe.insert('d')
     950 + fe.insert('e')
     951 + if _, _, err := fe.viewFor(4); err != nil {
     952 + return err
     953 + }
     954 + fe.cursorLeft()
     955 + fe.cursorLeft()
     956 + fe.cursorLeft()
     957 + fe.cursorLeft()
     958 + return nil
     959 + },
     960 + wantView: "abc⇨",
     961 + wantContent: "abcde",
     962 + wantCurIdx: 1,
     963 + },
     964 + {
     965 + desc: "half-width runes only, both ends invisible, deletes to make start visible",
     966 + width: 4,
     967 + ops: func(fe *fieldEditor) error {
     968 + fe.insert('a')
     969 + fe.insert('b')
     970 + fe.insert('c')
     971 + fe.insert('d')
     972 + fe.insert('e')
     973 + if _, _, err := fe.viewFor(4); err != nil {
     974 + return err
     975 + }
     976 + fe.cursorLeft()
     977 + fe.cursorLeft()
     978 + fe.cursorLeft()
     979 + fe.deleteBefore()
     980 + return nil
     981 + },
     982 + wantView: "acd⇨",
     983 + wantContent: "acde",
     984 + wantCurIdx: 1,
     985 + },
     986 + {
     987 + desc: "half-width runes only, deletion on second page refills the field",
     988 + width: 4,
     989 + ops: func(fe *fieldEditor) error {
     990 + fe.insert('a')
     991 + fe.insert('b')
     992 + fe.insert('c')
     993 + fe.insert('d')
     994 + fe.insert('e')
     995 + fe.insert('f')
     996 + if _, _, err := fe.viewFor(4); err != nil {
     997 + return err
     998 + }
     999 + fe.cursorLeft()
     1000 + fe.cursorLeft()
     1001 + fe.delete()
     1002 + return nil
     1003 + },
     1004 + wantView: "⇦df",
     1005 + wantContent: "abcdf",
     1006 + wantCurIdx: 2,
     1007 + },
     1008 + {
     1009 + desc: "half-width runes only, both ends invisible, scrolls to make end visible",
     1010 + width: 4,
     1011 + ops: func(fe *fieldEditor) error {
     1012 + fe.insert('a')
     1013 + fe.insert('b')
     1014 + fe.insert('c')
     1015 + fe.insert('d')
     1016 + fe.insert('e')
     1017 + if _, _, err := fe.viewFor(4); err != nil {
     1018 + return err
     1019 + }
     1020 + fe.cursorLeft()
     1021 + fe.cursorLeft()
     1022 + fe.cursorLeft()
     1023 + if _, _, err := fe.viewFor(4); err != nil {
     1024 + return err
     1025 + }
     1026 + fe.cursorRight()
     1027 + fe.cursorRight()
     1028 + return nil
     1029 + },
     1030 + wantView: "⇦de",
     1031 + wantContent: "abcde",
     1032 + wantCurIdx: 2,
     1033 + },
     1034 + {
     1035 + desc: "half-width runes only, both ends invisible, deletes to make end visible",
     1036 + width: 4,
     1037 + ops: func(fe *fieldEditor) error {
     1038 + fe.insert('a')
     1039 + fe.insert('b')
     1040 + fe.insert('c')
     1041 + fe.insert('d')
     1042 + fe.insert('e')
     1043 + if _, _, err := fe.viewFor(4); err != nil {
     1044 + return err
     1045 + }
     1046 + fe.cursorLeft()
     1047 + fe.cursorLeft()
     1048 + fe.cursorLeft()
     1049 + if _, _, err := fe.viewFor(4); err != nil {
     1050 + return err
     1051 + }
     1052 + fe.delete()
     1053 + return nil
     1054 + },
     1055 + wantView: "⇦de",
     1056 + wantContent: "abde",
     1057 + wantCurIdx: 1,
     1058 + },
     1059 + {
     1060 + desc: "full-width runes only, both ends invisible",
     1061 + width: 6,
     1062 + ops: func(fe *fieldEditor) error {
     1063 + fe.insert('你')
     1064 + fe.insert('好')
     1065 + fe.insert('世')
     1066 + fe.insert('界')
     1067 + if _, _, err := fe.viewFor(6); err != nil {
     1068 + return err
     1069 + }
     1070 + fe.cursorLeft()
     1071 + fe.cursorLeft()
     1072 + return nil
     1073 + },
     1074 + wantView: "⇦⇦世⇨",
     1075 + wantContent: "你好世界",
     1076 + wantCurIdx: 2,
     1077 + },
     1078 + {
     1079 + desc: "full-width runes only, both ends invisible, scrolls to make start visible",
     1080 + width: 6,
     1081 + ops: func(fe *fieldEditor) error {
     1082 + fe.insert('你')
     1083 + fe.insert('好')
     1084 + fe.insert('世')
     1085 + fe.insert('界')
     1086 + if _, _, err := fe.viewFor(6); err != nil {
     1087 + return err
     1088 + }
     1089 + fe.cursorLeft()
     1090 + fe.cursorLeft()
     1091 + if _, _, err := fe.viewFor(6); err != nil {
     1092 + return err
     1093 + }
     1094 + fe.cursorLeft()
     1095 + return nil
     1096 + },
     1097 + wantView: "你好⇨",
     1098 + wantContent: "你好世界",
     1099 + wantCurIdx: 2,
     1100 + },
     1101 + {
     1102 + desc: "full-width runes only, both ends invisible, deletes to make start visible",
     1103 + width: 6,
     1104 + ops: func(fe *fieldEditor) error {
     1105 + fe.insert('你')
     1106 + fe.insert('好')
     1107 + fe.insert('世')
     1108 + fe.insert('界')
     1109 + if _, _, err := fe.viewFor(6); err != nil {
     1110 + return err
     1111 + }
     1112 + fe.cursorLeft()
     1113 + fe.cursorLeft()
     1114 + if _, _, err := fe.viewFor(6); err != nil {
     1115 + return err
     1116 + }
     1117 + fe.deleteBefore()
     1118 + return nil
     1119 + },
     1120 + wantView: "你世⇨",
     1121 + wantContent: "你世界",
     1122 + wantCurIdx: 2,
     1123 + },
     1124 + {
     1125 + desc: "full-width runes only, both ends invisible, scrolls to make end visible",
     1126 + width: 6,
     1127 + ops: func(fe *fieldEditor) error {
     1128 + fe.insert('你')
     1129 + fe.insert('好')
     1130 + fe.insert('世')
     1131 + fe.insert('界')
     1132 + if _, _, err := fe.viewFor(6); err != nil {
     1133 + return err
     1134 + }
     1135 + fe.cursorLeft()
     1136 + fe.cursorLeft()
     1137 + if _, _, err := fe.viewFor(6); err != nil {
     1138 + return err
     1139 + }
     1140 + fe.cursorRight()
     1141 + return nil
     1142 + },
     1143 + wantView: "⇦⇦界",
     1144 + wantContent: "你好世界",
     1145 + wantCurIdx: 2,
     1146 + },
     1147 + {
     1148 + desc: "full-width runes only, both ends invisible, deletes to make end visible",
     1149 + width: 6,
     1150 + ops: func(fe *fieldEditor) error {
     1151 + fe.insert('你')
     1152 + fe.insert('好')
     1153 + fe.insert('世')
     1154 + fe.insert('界')
     1155 + if _, _, err := fe.viewFor(6); err != nil {
     1156 + return err
     1157 + }
     1158 + fe.cursorLeft()
     1159 + fe.cursorLeft()
     1160 + if _, _, err := fe.viewFor(6); err != nil {
     1161 + return err
     1162 + }
     1163 + fe.delete()
     1164 + return nil
     1165 + },
     1166 + wantView: "⇦⇦界",
     1167 + wantContent: "你好界",
     1168 + wantCurIdx: 2,
     1169 + },
     1170 + {
     1171 + desc: "scrolls to make full-width rune appear at the beginning",
     1172 + width: 4,
     1173 + ops: func(fe *fieldEditor) error {
     1174 + fe.insert('你')
     1175 + fe.insert('b')
     1176 + fe.insert('c')
     1177 + fe.insert('d')
     1178 + if _, _, err := fe.viewFor(4); err != nil {
     1179 + return err
     1180 + }
     1181 + fe.cursorLeft()
     1182 + fe.cursorLeft()
     1183 + fe.cursorLeft()
     1184 + return nil
     1185 + },
     1186 + wantView: "你b⇨",
     1187 + wantContent: "你bcd",
     1188 + wantCurIdx: 2,
     1189 + },
     1190 + {
     1191 + desc: "scrolls to make full-width rune appear at the end",
     1192 + width: 4,
     1193 + ops: func(fe *fieldEditor) error {
     1194 + fe.insert('a')
     1195 + fe.insert('b')
     1196 + fe.insert('c')
     1197 + fe.insert('你')
     1198 + fe.cursorStart()
     1199 + if _, _, err := fe.viewFor(4); err != nil {
     1200 + return err
     1201 + }
     1202 + fe.cursorRight()
     1203 + fe.cursorRight()
     1204 + fe.cursorRight()
     1205 + return nil
     1206 + },
     1207 + wantView: "⇦你",
     1208 + wantContent: "abc你",
     1209 + wantCurIdx: 1,
     1210 + },
     1211 + {
     1212 + desc: "inserts after last full width rune, first is half-width",
     1213 + width: 6,
     1214 + ops: func(fe *fieldEditor) error {
     1215 + fe.insert('a')
     1216 + fe.insert('b')
     1217 + fe.insert('c')
     1218 + fe.insert('你')
     1219 + if _, _, err := fe.viewFor(6); err != nil {
     1220 + return err
     1221 + }
     1222 + fe.insert('e')
     1223 + return nil
     1224 + },
     1225 + wantView: "⇦c你e",
     1226 + wantContent: "abc你e",
     1227 + wantCurIdx: 5,
     1228 + },
     1229 + {
     1230 + desc: "inserts after last full width rune, first is half-width",
     1231 + width: 6,
     1232 + ops: func(fe *fieldEditor) error {
     1233 + fe.insert('世')
     1234 + fe.insert('b')
     1235 + fe.insert('你')
     1236 + if _, _, err := fe.viewFor(6); err != nil {
     1237 + return err
     1238 + }
     1239 + fe.insert('d')
     1240 + return nil
     1241 + },
     1242 + wantView: "⇦你d",
     1243 + wantContent: "世b你d",
     1244 + wantCurIdx: 4,
     1245 + },
     1246 + {
     1247 + desc: "inserts after last full width rune, hidden rune is full-width",
     1248 + width: 6,
     1249 + ops: func(fe *fieldEditor) error {
     1250 + fe.insert('世')
     1251 + fe.insert('你')
     1252 + if _, _, err := fe.viewFor(6); err != nil {
     1253 + return err
     1254 + }
     1255 + fe.insert('c')
     1256 + fe.insert('d')
     1257 + return nil
     1258 + },
     1259 + wantView: "⇦⇦cd",
     1260 + wantContent: "世你cd",
     1261 + wantCurIdx: 4,
     1262 + },
     1263 + {
     1264 + desc: "scrolls right, first is full-width, last are half-width",
     1265 + width: 6,
     1266 + ops: func(fe *fieldEditor) error {
     1267 + fe.insert('a')
     1268 + fe.insert('你')
     1269 + fe.insert('世')
     1270 + fe.insert('d')
     1271 + fe.insert('e')
     1272 + fe.insert('f')
     1273 + fe.insert('g')
     1274 + fe.insert('h')
     1275 + fe.cursorStart()
     1276 + if _, _, err := fe.viewFor(6); err != nil {
     1277 + return err
     1278 + }
     1279 + fe.cursorRight()
     1280 + fe.cursorRight()
     1281 + fe.cursorRight()
     1282 + fe.cursorRight()
     1283 + return nil
     1284 + },
     1285 + wantView: "⇦⇦def⇨",
     1286 + wantContent: "a你世defgh",
     1287 + wantCurIdx: 3,
     1288 + },
     1289 + {
     1290 + desc: "scrolls right, first is half-width, last is full-width",
     1291 + width: 6,
     1292 + ops: func(fe *fieldEditor) error {
     1293 + fe.insert('a')
     1294 + fe.insert('b')
     1295 + fe.insert('c')
     1296 + fe.insert('你')
     1297 + fe.insert('世')
     1298 + fe.insert('f')
     1299 + fe.insert('g')
     1300 + fe.insert('h')
     1301 + fe.cursorStart()
     1302 + if _, _, err := fe.viewFor(6); err != nil {
     1303 + return err
     1304 + }
     1305 + fe.cursorRight()
     1306 + fe.cursorRight()
     1307 + fe.cursorRight()
     1308 + fe.cursorRight()
     1309 + return nil
     1310 + },
     1311 + wantView: "⇦你世⇨",
     1312 + wantContent: "abc你世fgh",
     1313 + wantCurIdx: 3,
     1314 + },
     1315 + {
     1316 + desc: "scrolls right, first and last are full-width",
     1317 + width: 6,
     1318 + ops: func(fe *fieldEditor) error {
     1319 + fe.insert('你')
     1320 + fe.insert('好')
     1321 + fe.insert('世')
     1322 + fe.insert('界')
     1323 + fe.cursorStart()
     1324 + if _, _, err := fe.viewFor(6); err != nil {
     1325 + return err
     1326 + }
     1327 + fe.cursorRight()
     1328 + fe.cursorRight()
     1329 + return nil
     1330 + },
     1331 + wantView: "⇦⇦世⇨",
     1332 + wantContent: "你好世界",
     1333 + wantCurIdx: 2,
     1334 + },
     1335 + {
     1336 + desc: "scrolls right, first and last are half-width",
     1337 + width: 6,
     1338 + ops: func(fe *fieldEditor) error {
     1339 + fe.insert('a')
     1340 + fe.insert('b')
     1341 + fe.insert('c')
     1342 + fe.insert('d')
     1343 + fe.insert('e')
     1344 + fe.insert('f')
     1345 + fe.insert('g')
     1346 + fe.cursorStart()
     1347 + if _, _, err := fe.viewFor(6); err != nil {
     1348 + return err
     1349 + }
     1350 + fe.cursorRight()
     1351 + fe.cursorRight()
     1352 + fe.cursorRight()
     1353 + fe.cursorRight()
     1354 + fe.cursorRight()
     1355 + return nil
     1356 + },
     1357 + wantView: "⇦cdef⇨",
     1358 + wantContent: "abcdefg",
     1359 + wantCurIdx: 4,
     1360 + },
     1361 + {
     1362 + desc: "scrolls left, first is full-width, last are half-width",
     1363 + width: 6,
     1364 + ops: func(fe *fieldEditor) error {
     1365 + fe.insert('a')
     1366 + fe.insert('你')
     1367 + fe.insert('世')
     1368 + fe.insert('d')
     1369 + fe.insert('e')
     1370 + fe.insert('f')
     1371 + fe.insert('g')
     1372 + fe.insert('h')
     1373 + if _, _, err := fe.viewFor(6); err != nil {
     1374 + return err
     1375 + }
     1376 + fe.cursorLeft()
     1377 + fe.cursorLeft()
     1378 + fe.cursorLeft()
     1379 + fe.cursorLeft()
     1380 + fe.cursorLeft()
     1381 + return nil
     1382 + },
     1383 + wantView: "⇦⇦def⇨",
     1384 + wantContent: "a你世defgh",
     1385 + wantCurIdx: 2,
     1386 + },
     1387 + {
     1388 + desc: "scrolls left, first is half-width, last is full-width",
     1389 + width: 6,
     1390 + ops: func(fe *fieldEditor) error {
     1391 + fe.insert('a')
     1392 + fe.insert('b')
     1393 + fe.insert('c')
     1394 + fe.insert('你')
     1395 + fe.insert('世')
     1396 + fe.insert('f')
     1397 + fe.insert('g')
     1398 + fe.insert('h')
     1399 + if _, _, err := fe.viewFor(6); err != nil {
     1400 + return err
     1401 + }
     1402 + fe.cursorLeft()
     1403 + fe.cursorLeft()
     1404 + fe.cursorLeft()
     1405 + fe.cursorLeft()
     1406 + fe.cursorLeft()
     1407 + return nil
     1408 + },
     1409 + wantView: "⇦你世⇨",
     1410 + wantContent: "abc你世fgh",
     1411 + wantCurIdx: 1,
     1412 + },
     1413 + {
     1414 + desc: "scrolls left, first and last are full-width",
     1415 + width: 6,
     1416 + ops: func(fe *fieldEditor) error {
     1417 + fe.insert('你')
     1418 + fe.insert('好')
     1419 + fe.insert('世')
     1420 + fe.insert('界')
     1421 + if _, _, err := fe.viewFor(6); err != nil {
     1422 + return err
     1423 + }
     1424 + fe.cursorLeft()
     1425 + fe.cursorLeft()
     1426 + return nil
     1427 + },
     1428 + wantView: "⇦⇦世⇨",
     1429 + wantContent: "你好世界",
     1430 + wantCurIdx: 2,
     1431 + },
     1432 + {
     1433 + desc: "scrolls left, first and last are half-width",
     1434 + width: 6,
     1435 + ops: func(fe *fieldEditor) error {
     1436 + fe.insert('a')
     1437 + fe.insert('b')
     1438 + fe.insert('c')
     1439 + fe.insert('d')
     1440 + fe.insert('e')
     1441 + fe.insert('f')
     1442 + fe.insert('g')
     1443 + if _, _, err := fe.viewFor(6); err != nil {
     1444 + return err
     1445 + }
     1446 + fe.cursorLeft()
     1447 + fe.cursorLeft()
     1448 + fe.cursorLeft()
     1449 + fe.cursorLeft()
     1450 + fe.cursorLeft()
     1451 + return nil
     1452 + },
     1453 + wantView: "⇦cdef⇨",
     1454 + wantContent: "abcdefg",
     1455 + wantCurIdx: 1,
     1456 + },
     1457 + {
     1458 + desc: "resets the field editor",
     1459 + width: 4,
     1460 + ops: func(fe *fieldEditor) error {
     1461 + fe.insert('a')
     1462 + fe.insert('b')
     1463 + fe.insert('c')
     1464 + if _, _, err := fe.viewFor(4); err != nil {
     1465 + return err
     1466 + }
     1467 + fe.reset()
     1468 + return nil
     1469 + },
     1470 + wantView: "",
     1471 + wantContent: "",
     1472 + wantCurIdx: 0,
     1473 + },
     1474 + {
     1475 + desc: "doesn't insert runes with rune width of zero",
     1476 + width: 4,
     1477 + ops: func(fe *fieldEditor) error {
     1478 + fe.insert('a')
     1479 + fe.insert('\x08')
     1480 + fe.insert('c')
     1481 + if _, _, err := fe.viewFor(4); err != nil {
     1482 + return err
     1483 + }
     1484 + return nil
     1485 + },
     1486 + wantView: "ac",
     1487 + wantContent: "ac",
     1488 + wantCurIdx: 2,
     1489 + },
     1490 + {
     1491 + desc: "all text visible, moves cursor to position zero",
     1492 + width: 6,
     1493 + ops: func(fe *fieldEditor) error {
     1494 + fe.insert('a')
     1495 + fe.insert('b')
     1496 + fe.insert('c')
     1497 + if _, _, err := fe.viewFor(6); err != nil {
     1498 + return err
     1499 + }
     1500 + fe.cursorRelCell(0)
     1501 + return nil
     1502 + },
     1503 + wantView: "abc",
     1504 + wantContent: "abc",
     1505 + wantCurIdx: 0,
     1506 + },
     1507 + {
     1508 + desc: "all text visible, moves cursor to position in the middle",
     1509 + width: 6,
     1510 + ops: func(fe *fieldEditor) error {
     1511 + fe.insert('a')
     1512 + fe.insert('b')
     1513 + fe.insert('c')
     1514 + if _, _, err := fe.viewFor(6); err != nil {
     1515 + return err
     1516 + }
     1517 + fe.cursorRelCell(1)
     1518 + return nil
     1519 + },
     1520 + wantView: "abc",
     1521 + wantContent: "abc",
     1522 + wantCurIdx: 1,
     1523 + },
     1524 + {
     1525 + desc: "all text visible, moves cursor back to the last character",
     1526 + width: 6,
     1527 + ops: func(fe *fieldEditor) error {
     1528 + fe.insert('a')
     1529 + fe.insert('b')
     1530 + fe.insert('c')
     1531 + if _, _, err := fe.viewFor(6); err != nil {
     1532 + return err
     1533 + }
     1534 + fe.cursorStart()
     1535 + fe.cursorRelCell(2)
     1536 + return nil
     1537 + },
     1538 + wantView: "abc",
     1539 + wantContent: "abc",
     1540 + wantCurIdx: 2,
     1541 + },
     1542 + {
     1543 + desc: "all text visible, moves cursor to the appending space",
     1544 + width: 6,
     1545 + ops: func(fe *fieldEditor) error {
     1546 + fe.insert('a')
     1547 + fe.insert('b')
     1548 + fe.insert('c')
     1549 + if _, _, err := fe.viewFor(6); err != nil {
     1550 + return err
     1551 + }
     1552 + fe.cursorStart()
     1553 + fe.cursorRelCell(3)
     1554 + return nil
     1555 + },
     1556 + wantView: "abc",
     1557 + wantContent: "abc",
     1558 + wantCurIdx: 3,
     1559 + },
     1560 + {
     1561 + desc: "all text visible, moves cursor before the beginning of data",
     1562 + width: 6,
     1563 + ops: func(fe *fieldEditor) error {
     1564 + fe.insert('a')
     1565 + fe.insert('b')
     1566 + fe.insert('c')
     1567 + if _, _, err := fe.viewFor(6); err != nil {
     1568 + return err
     1569 + }
     1570 + fe.cursorStart()
     1571 + fe.cursorRelCell(-1)
     1572 + return nil
     1573 + },
     1574 + wantView: "abc",
     1575 + wantContent: "abc",
     1576 + wantCurIdx: 0,
     1577 + },
     1578 + {
     1579 + desc: "all text visible, moves cursor after the appending space",
     1580 + width: 6,
     1581 + ops: func(fe *fieldEditor) error {
     1582 + fe.insert('a')
     1583 + fe.insert('b')
     1584 + fe.insert('c')
     1585 + if _, _, err := fe.viewFor(6); err != nil {
     1586 + return err
     1587 + }
     1588 + fe.cursorStart()
     1589 + fe.cursorRelCell(10)
     1590 + return nil
     1591 + },
     1592 + wantView: "abc",
     1593 + wantContent: "abc",
     1594 + wantCurIdx: 3,
     1595 + },
     1596 + {
     1597 + desc: "moves cursor when there is no text",
     1598 + width: 6,
     1599 + ops: func(fe *fieldEditor) error {
     1600 + fe.cursorRelCell(10)
     1601 + return nil
     1602 + },
     1603 + wantView: "",
     1604 + wantContent: "",
     1605 + wantCurIdx: 0,
     1606 + },
     1607 + {
     1608 + desc: "both ends hidden, moves cursor onto the left arrow",
     1609 + width: 4,
     1610 + ops: func(fe *fieldEditor) error {
     1611 + fe.insert('a')
     1612 + fe.insert('b')
     1613 + fe.insert('c')
     1614 + fe.insert('d')
     1615 + fe.insert('e')
     1616 + if _, _, err := fe.viewFor(4); err != nil {
     1617 + return err
     1618 + }
     1619 + fe.cursorLeft()
     1620 + fe.cursorLeft()
     1621 + fe.cursorLeft()
     1622 + if _, _, err := fe.viewFor(4); err != nil {
     1623 + return err
     1624 + }
     1625 + fe.cursorRight()
     1626 + fe.cursorRelCell(0)
     1627 + return nil
     1628 + },
     1629 + wantView: "⇦cd⇨",
     1630 + wantContent: "abcde",
     1631 + wantCurIdx: 1,
     1632 + },
     1633 + {
     1634 + desc: "both ends hidden, moves cursor onto the first character",
     1635 + width: 4,
     1636 + ops: func(fe *fieldEditor) error {
     1637 + fe.insert('a')
     1638 + fe.insert('b')
     1639 + fe.insert('c')
     1640 + fe.insert('d')
     1641 + fe.insert('e')
     1642 + if _, _, err := fe.viewFor(4); err != nil {
     1643 + return err
     1644 + }
     1645 + fe.cursorLeft()
     1646 + fe.cursorLeft()
     1647 + fe.cursorLeft()
     1648 + if _, _, err := fe.viewFor(4); err != nil {
     1649 + return err
     1650 + }
     1651 + fe.cursorRight()
     1652 + fe.cursorRelCell(1)
     1653 + return nil
     1654 + },
     1655 + wantView: "⇦cd⇨",
     1656 + wantContent: "abcde",
     1657 + wantCurIdx: 1,
     1658 + },
     1659 + {
     1660 + desc: "both ends hidden, moves cursor onto the right arrow",
     1661 + width: 4,
     1662 + ops: func(fe *fieldEditor) error {
     1663 + fe.insert('a')
     1664 + fe.insert('b')
     1665 + fe.insert('c')
     1666 + fe.insert('d')
     1667 + fe.insert('e')
     1668 + if _, _, err := fe.viewFor(4); err != nil {
     1669 + return err
     1670 + }
     1671 + fe.cursorLeft()
     1672 + fe.cursorLeft()
     1673 + fe.cursorLeft()
     1674 + if _, _, err := fe.viewFor(4); err != nil {
     1675 + return err
     1676 + }
     1677 + fe.cursorRelCell(3)
     1678 + return nil
     1679 + },
     1680 + wantView: "⇦cd⇨",
     1681 + wantContent: "abcde",
     1682 + wantCurIdx: 2,
     1683 + },
     1684 + {
     1685 + desc: "both ends hidden, moves cursor onto the last character",
     1686 + width: 4,
     1687 + ops: func(fe *fieldEditor) error {
     1688 + fe.insert('a')
     1689 + fe.insert('b')
     1690 + fe.insert('c')
     1691 + fe.insert('d')
     1692 + fe.insert('e')
     1693 + if _, _, err := fe.viewFor(4); err != nil {
     1694 + return err
     1695 + }
     1696 + fe.cursorLeft()
     1697 + fe.cursorLeft()
     1698 + fe.cursorLeft()
     1699 + if _, _, err := fe.viewFor(4); err != nil {
     1700 + return err
     1701 + }
     1702 + fe.cursorRelCell(2)
     1703 + return nil
     1704 + },
     1705 + wantView: "⇦cd⇨",
     1706 + wantContent: "abcde",
     1707 + wantCurIdx: 2,
     1708 + },
     1709 + {
     1710 + desc: "moves cursor onto the first cell containing a full-width rune",
     1711 + width: 8,
     1712 + ops: func(fe *fieldEditor) error {
     1713 + fe.insert('你')
     1714 + fe.insert('好')
     1715 + fe.insert('世')
     1716 + fe.insert('界')
     1717 + fe.insert('你')
     1718 + if _, _, err := fe.viewFor(8); err != nil {
     1719 + return err
     1720 + }
     1721 + fe.cursorLeft()
     1722 + fe.cursorLeft()
     1723 + fe.cursorLeft()
     1724 + if _, _, err := fe.viewFor(8); err != nil {
     1725 + return err
     1726 + }
     1727 + fe.cursorRelCell(4)
     1728 + return nil
     1729 + },
     1730 + wantView: "⇦⇦世界⇨",
     1731 + wantContent: "你好世界你",
     1732 + wantCurIdx: 4,
     1733 + },
     1734 + {
     1735 + desc: "moves cursor onto the second cell containing a full-width rune",
     1736 + width: 8,
     1737 + ops: func(fe *fieldEditor) error {
     1738 + fe.insert('你')
     1739 + fe.insert('好')
     1740 + fe.insert('世')
     1741 + fe.insert('界')
     1742 + fe.insert('你')
     1743 + if _, _, err := fe.viewFor(8); err != nil {
     1744 + return err
     1745 + }
     1746 + fe.cursorLeft()
     1747 + fe.cursorLeft()
     1748 + fe.cursorLeft()
     1749 + if _, _, err := fe.viewFor(8); err != nil {
     1750 + return err
     1751 + }
     1752 + fe.cursorRelCell(5)
     1753 + return nil
     1754 + },
     1755 + wantView: "⇦⇦世界⇨",
     1756 + wantContent: "你好世界你",
     1757 + wantCurIdx: 4,
     1758 + },
     1759 + {
     1760 + desc: "moves cursor onto the second right arrow",
     1761 + width: 8,
     1762 + ops: func(fe *fieldEditor) error {
     1763 + fe.insert('你')
     1764 + fe.insert('好')
     1765 + fe.insert('世')
     1766 + fe.insert('界')
     1767 + fe.insert('你')
     1768 + if _, _, err := fe.viewFor(8); err != nil {
     1769 + return err
     1770 + }
     1771 + fe.cursorLeft()
     1772 + fe.cursorLeft()
     1773 + fe.cursorLeft()
     1774 + if _, _, err := fe.viewFor(8); err != nil {
     1775 + return err
     1776 + }
     1777 + fe.cursorRelCell(1)
     1778 + return nil
     1779 + },
     1780 + wantView: "⇦⇦世界⇨",
     1781 + wantContent: "你好世界你",
     1782 + wantCurIdx: 2,
     1783 + },
     1784 + }
     1785 + 
     1786 + for _, tc := range tests {
     1787 + t.Run(tc.desc, func(t *testing.T) {
     1788 + fe := newFieldEditor()
     1789 + if tc.ops != nil {
     1790 + if err := tc.ops(fe); err != nil {
     1791 + t.Fatalf("ops => unexpected error: %v", err)
     1792 + }
     1793 + }
     1794 + 
     1795 + gotView, gotCurIdx, err := fe.viewFor(tc.width)
     1796 + if (err != nil) != tc.wantErr {
     1797 + t.Errorf("viewFor(%d) => unexpected error: %v, wantErr: %v", tc.width, err, tc.wantErr)
     1798 + }
     1799 + if err != nil {
     1800 + return
     1801 + }
     1802 + 
     1803 + if gotView != tc.wantView || gotCurIdx != tc.wantCurIdx {
     1804 + t.Errorf("viewFor(%d) => (%q, %d), want (%q, %d)", tc.width, gotView, gotCurIdx, tc.wantView, tc.wantCurIdx)
     1805 + }
     1806 + 
     1807 + gotContent := fe.content()
     1808 + if gotContent != tc.wantContent {
     1809 + t.Errorf("content -> %q, want %q", gotContent, tc.wantContent)
     1810 + }
     1811 + })
     1812 + }
     1813 +}
     1814 + 
  • ■ ■ ■ ■ ■ ■
    widgets/textinput/options.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 textinput
     16 + 
     17 +// options.go contains configurable options for TextInput.
     18 + 
     19 +import (
     20 + "fmt"
     21 + 
     22 + "github.com/mum4k/termdash/align"
     23 + "github.com/mum4k/termdash/cell"
     24 + "github.com/mum4k/termdash/internal/runewidth"
     25 + "github.com/mum4k/termdash/internal/wrap"
     26 + "github.com/mum4k/termdash/linestyle"
     27 +)
     28 + 
     29 +// Option is used to provide options.
     30 +type Option interface {
     31 + // set sets the provided option.
     32 + set(*options)
     33 +}
     34 + 
     35 +// option implements Option.
     36 +type option func(*options)
     37 + 
     38 +// set implements Option.set.
     39 +func (o option) set(opts *options) {
     40 + o(opts)
     41 +}
     42 + 
     43 +// options holds the provided options.
     44 +type options struct {
     45 + fillColor cell.Color
     46 + textColor cell.Color
     47 + placeHolderColor cell.Color
     48 + highlightedColor cell.Color
     49 + cursorColor cell.Color
     50 + border linestyle.LineStyle
     51 + borderColor cell.Color
     52 + 
     53 + widthPerc *int
     54 + maxWidthCells *int
     55 + label string
     56 + labelCellOpts []cell.Option
     57 + labelAlign align.Horizontal
     58 + 
     59 + placeHolder string
     60 + hideTextWith rune
     61 + 
     62 + filter FilterFn
     63 + onSubmit SubmitFn
     64 + clearOnSubmit bool
     65 +}
     66 + 
     67 +// validate validates the provided options.
     68 +func (o *options) validate() error {
     69 + if min, max, perc := 0, 100, o.widthPerc; perc != nil && (*perc <= min || *perc > max) {
     70 + return fmt.Errorf("invalid WidthPerc(%d), must be value in range %d < value <= %d", *perc, min, max)
     71 + }
     72 + if min, cells := 4, o.maxWidthCells; cells != nil && *cells < min {
     73 + return fmt.Errorf("invalid MaxWidthCells(%d), must be value in range %d <= value", *cells, min)
     74 + }
     75 + if r := o.hideTextWith; r != 0 {
     76 + if err := wrap.ValidText(string(r)); err != nil {
     77 + return fmt.Errorf("invalid HideTextWidth rune %c(%d): %v", r, r, err)
     78 + }
     79 + if got, want := runewidth.RuneWidth(r), 1; got != want {
     80 + return fmt.Errorf("invalid HideTextWidth rune %c(%d), has rune width of %d cells, only runes with width of %d are accepted", r, r, got, want)
     81 + }
     82 + }
     83 + return nil
     84 +}
     85 + 
     86 +// newOptions returns options with the default values set.
     87 +func newOptions() *options {
     88 + return &options{
     89 + fillColor: cell.ColorNumber(DefaultFillColorNumber),
     90 + placeHolderColor: cell.ColorNumber(DefaultPlaceHolderColorNumber),
     91 + highlightedColor: cell.ColorNumber(DefaultHighlightedColorNumber),
     92 + cursorColor: cell.ColorNumber(DefaultCursorColorNumber),
     93 + labelAlign: DefaultLabelAlign,
     94 + }
     95 +}
     96 + 
     97 +// DefaultFillColorNumber is the default color number for the FillColor option.
     98 +const DefaultFillColorNumber = 33
     99 + 
     100 +// FillColor sets the fill color for the text input field.
     101 +// Defaults to DefaultFillColorNumber.
     102 +func FillColor(c cell.Color) Option {
     103 + return option(func(opts *options) {
     104 + opts.fillColor = c
     105 + })
     106 +}
     107 + 
     108 +// TextColor sets the color of the text in the input field.
     109 +// Defaults to the default terminal color.
     110 +func TextColor(c cell.Color) Option {
     111 + return option(func(opts *options) {
     112 + opts.textColor = c
     113 + })
     114 +}
     115 + 
     116 +// DefaultHighlightedColorNumber is the default color number for the
     117 +// HighlightedColor option.
     118 +const DefaultHighlightedColorNumber = 0
     119 + 
     120 +// HighlightedColor sets the color of the text rune directly under the cursor.
     121 +// Defaults to the default terminal color.
     122 +func HighlightedColor(c cell.Color) Option {
     123 + return option(func(opts *options) {
     124 + opts.highlightedColor = c
     125 + })
     126 +}
     127 + 
     128 +// DefaultCursorColorNumber is the default color number for the CursorColor
     129 +// option.
     130 +const DefaultCursorColorNumber = 250
     131 + 
     132 +// CursorColor sets the color of the cursor.
     133 +// Defaults to DefaultCursorColorNumber.
     134 +func CursorColor(c cell.Color) Option {
     135 + return option(func(opts *options) {
     136 + opts.cursorColor = c
     137 + })
     138 +}
     139 + 
     140 +// Border adds a border around the text input field.
     141 +func Border(ls linestyle.LineStyle) Option {
     142 + return option(func(opts *options) {
     143 + opts.border = ls
     144 + })
     145 +}
     146 + 
     147 +// BorderColor sets the color of the border.
     148 +// Defaults to the default terminal color.
     149 +func BorderColor(c cell.Color) Option {
     150 + return option(func(opts *options) {
     151 + opts.borderColor = c
     152 + })
     153 +}
     154 + 
     155 +// WidthPerc sets the width for the text input field as a percentage of the
     156 +// container width. Must be a value in the range 0 < perc <= 100.
     157 +// Defaults to the width adjusted automatically base on the label length.
     158 +func WidthPerc(perc int) Option {
     159 + return option(func(opts *options) {
     160 + opts.widthPerc = &perc
     161 + })
     162 +}
     163 + 
     164 +// MaxWidthCells sets the maximum width of the text input field as an absolute value
     165 +// in cells. Must be a value in the range 4 <= cells.
     166 +// This doesn't limit the text that the user can input, if the text overflows
     167 +// the width of the input field, it scrolls to the left.
     168 +// Defaults to using all available width in the container.
     169 +func MaxWidthCells(cells int) Option {
     170 + return option(func(opts *options) {
     171 + opts.maxWidthCells = &cells
     172 + })
     173 +}
     174 + 
     175 +// Label adds a text label to the left of the input field.
     176 +func Label(label string, cOpts ...cell.Option) Option {
     177 + return option(func(opts *options) {
     178 + opts.label = label
     179 + opts.labelCellOpts = cOpts
     180 + })
     181 +}
     182 + 
     183 +// DefaultLabelAlign is the default value for the LabelAlign option.
     184 +const DefaultLabelAlign = align.HorizontalLeft
     185 + 
     186 +// LabelAlign sets the alignment of the label within its area.
     187 +// The label is placed to the left of the input field. The width of this area
     188 +// can be specified using the LabelWidthPerc option.
     189 +// Defaults to DefaultLabelAlign.
     190 +func LabelAlign(la align.Horizontal) Option {
     191 + return option(func(opts *options) {
     192 + opts.labelAlign = la
     193 + })
     194 +}
     195 + 
     196 +// PlaceHolder sets text to be displayed in the input field when it is empty.
     197 +// This text disappears when the text input field becomes focused.
     198 +func PlaceHolder(text string) Option {
     199 + return option(func(opts *options) {
     200 + opts.placeHolder = text
     201 + })
     202 +}
     203 + 
     204 +// DefaultPlaceHolderColorNumber is the default color number for the
     205 +// PlaceHolderColor option.
     206 +const DefaultPlaceHolderColorNumber = 194
     207 + 
     208 +// PlaceHolderColor sets the color of the placeholder text.
     209 +// Defaults to DefaultPlaceHolderColorNumber.
     210 +func PlaceHolderColor(c cell.Color) Option {
     211 + return option(func(opts *options) {
     212 + opts.placeHolderColor = c
     213 + })
     214 +}
     215 + 
     216 +// HideTextWith sets the rune that should be displayed instead of displaying
     217 +// the text. Useful for fields that accept sensitive information like
     218 +// passwords.
     219 +// The rune must be a printable rune with cell width of one.
     220 +func HideTextWith(r rune) Option {
     221 + return option(func(opts *options) {
     222 + opts.hideTextWith = r
     223 + })
     224 +}
     225 + 
     226 +// FilterFn if provided can be used to filter runes that are allowed in the
     227 +// text input field. Any rune for which this function returns false will be
     228 +// rejected.
     229 +type FilterFn func(rune) bool
     230 + 
     231 +// Filter sets a function that will be used to filter characters the user can
     232 +// input.
     233 +func Filter(fn FilterFn) Option {
     234 + return option(func(opts *options) {
     235 + opts.filter = fn
     236 + })
     237 +}
     238 + 
     239 +// SubmitFn if provided is called when the user submits the content of the text
     240 +// input field, the argument text contains all the text in the field.
     241 +// Submitting the input field clears its content.
     242 +//
     243 +// The callback function must be thread-safe as the keyboard event that
     244 +// triggers the submission comes from a separate goroutine.
     245 +type SubmitFn func(text string) error
     246 + 
     247 +// OnSubmit sets a function that will be called with the text typed by the user
     248 +// when they submit the content by pressing the Enter key.
     249 +// The SubmitFn must not attempt to read from or modify the TextInput instance
     250 +// in any way as while the SubmitFn is executing, the TextInput is mutex
     251 +// locked. If the intention is to clear the content on submission, use the
     252 +// ClearOnSubmit() option.
     253 +func OnSubmit(fn SubmitFn) Option {
     254 + return option(func(opts *options) {
     255 + opts.onSubmit = fn
     256 + })
     257 +}
     258 + 
     259 +// ClearOnSubmit sets the text input to be cleared when a submit of the content
     260 +// is triggered by the user pressing the Enter key.
     261 +func ClearOnSubmit() Option {
     262 + return option(func(opts *options) {
     263 + opts.clearOnSubmit = true
     264 + })
     265 +}
     266 + 
Please wait...
Page is in error, reload to recover