mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
Compare commits
98 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
102df20a88 | ||
![]() |
db8e463332 | ||
![]() |
9dfa205c64 | ||
![]() |
ce0dd0b666 | ||
![]() |
e980c4bb2b | ||
![]() |
4b7343b987 | ||
![]() |
ec14443ef9 | ||
![]() |
edab304013 | ||
![]() |
029ce611e8 | ||
![]() |
5b664845a5 | ||
![]() |
992d7f3bfc | ||
![]() |
ca93ed8334 | ||
![]() |
610a070aab | ||
![]() |
275c62fc47 | ||
![]() |
4465700c5d | ||
![]() |
a3806f2bb5 | ||
![]() |
bc63a16bf8 | ||
![]() |
a435b03fce | ||
![]() |
b45b65fc1d | ||
![]() |
3687adb48d | ||
![]() |
34ed1a7d7d | ||
![]() |
90a119eab5 | ||
![]() |
428633d1a0 | ||
![]() |
2e0828fdef | ||
![]() |
3569f48f38 | ||
![]() |
39084dab2f | ||
![]() |
16428274d7 | ||
![]() |
2e819cc617 | ||
![]() |
2f4398bf9b | ||
![]() |
c3a6ea841f | ||
![]() |
af91f8329d | ||
![]() |
7b0edc051a | ||
![]() |
4a7e55359d | ||
![]() |
d2e8044618 | ||
![]() |
425b8a1a84 | ||
![]() |
0361d4e3ed | ||
![]() |
821b47af21 | ||
![]() |
45a2042afb | ||
![]() |
83c2b44eaa | ||
![]() |
185582a89e | ||
![]() |
343f50342e | ||
![]() |
d0cde3f172 | ||
![]() |
120b02117f | ||
![]() |
a942dcf06f | ||
![]() |
55099dd4bd | ||
![]() |
e4ccfa3102 | ||
![]() |
e7f235a952 | ||
![]() |
244687324e | ||
![]() |
fa5cbadc35 | ||
![]() |
5a36b82b2e | ||
![]() |
5261a1fd82 | ||
![]() |
a3cf098aab | ||
![]() |
bc92fea523 | ||
![]() |
5e6de2ede6 | ||
![]() |
4627a7620f | ||
![]() |
66e7564bf6 | ||
![]() |
0cf808465f | ||
![]() |
6ac65a228e | ||
![]() |
6cf1c26327 | ||
![]() |
90333d018f | ||
![]() |
b00342e91c | ||
![]() |
906334caf2 | ||
![]() |
b1943d95cf | ||
![]() |
efe737b6ac | ||
![]() |
44628fd211 | ||
![]() |
188017287a | ||
![]() |
d57ec120c0 | ||
![]() |
155b42c608 | ||
![]() |
b01ab7c72a | ||
![]() |
493ffcf1af | ||
![]() |
9b59a47cd4 | ||
![]() |
94d07aea18 | ||
![]() |
a9e894f4c1 | ||
![]() |
52ebd80b87 | ||
![]() |
0f346f34d1 | ||
![]() |
3affaaa53b | ||
![]() |
45f26a878d | ||
![]() |
e87b1cb791 | ||
![]() |
f28390ba72 | ||
![]() |
2cbce1c330 | ||
![]() |
0aac8f46e0 | ||
![]() |
bc37cb70a7 | ||
![]() |
9beb36080d | ||
![]() |
6e46b73927 | ||
![]() |
1aba280365 | ||
![]() |
9954a0543e | ||
![]() |
07a736f643 | ||
![]() |
e02ad31755 | ||
![]() |
f0f01d71fe | ||
![]() |
4a87d5279a | ||
![]() |
f3c638c8e9 | ||
![]() |
cf2f350dff | ||
![]() |
1bfe588811 | ||
![]() |
231073378f | ||
![]() |
6aaa8f312d | ||
![]() |
7e8c26a94e | ||
![]() |
bb460d221d | ||
![]() |
047a3f8edd |
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -7,4 +7,8 @@ updates:
|
||||
directory: "/"
|
||||
target-branch: "devel"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
45
.github/workflows/go.yml
vendored
Normal file
45
.github/workflows/go.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# This workflow will builds and tests Termdash.
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "devel" ]
|
||||
pull_request:
|
||||
branches: [ "master", "devel" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [ '1.20', '1.21', 'stable' ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
go install golang.org/x/tools/cmd/cover@latest
|
||||
go install github.com/mattn/goveralls@latest
|
||||
go install golang.org/x/lint/golint@latest
|
||||
go get -t ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v -covermode=count -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Test Race
|
||||
run: CGO_ENABLED=1 go test -race ./...
|
||||
|
||||
- name: Format
|
||||
run: diff -u <(echo -n) <(gofmt -d -s .)
|
||||
|
||||
- name: Lint
|
||||
run: diff -u <(echo -n) <(golint ./...)
|
25
.travis.yml
25
.travis.yml
@ -1,25 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.16.x
|
||||
- 1.17.x
|
||||
- stable
|
||||
before_install:
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
- go get github.com/mattn/goveralls
|
||||
script:
|
||||
- go get -t ./...
|
||||
- go get -u golang.org/x/lint/golint
|
||||
# Temporarily set -mod=mod to allow modification of go.mod and go.sum.
|
||||
# This seems to be caused by a sum missing in the tcell dependency and
|
||||
# should be removed when no longer needed.
|
||||
- go test -v -covermode=count -coverprofile=coverage.out -mod=mod ./...
|
||||
- CGO_ENABLED=1 go test -mod=mod -race ./...
|
||||
- go vet ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- diff -u <(echo -n) <(./internal/scripts/autogen_licences.sh .)
|
||||
- diff -u <(echo -n) <(golint ./...)
|
||||
- $GOPATH/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
|
||||
env:
|
||||
global:
|
||||
- CGO_ENABLED=0
|
||||
- secure: VOOh/w2YNAn+psiWYjIOQ5ZhhMb6Wz7zzmhIlj0dc5mGQztFAX5TuNWOU5JokvOigFy18JhPeDJRmp661xqM4gy1Znx1odSXES3YdCwt42pmpjYIkI9lI09xTRH6WYIRmYfCHe4J3A9/CWLeDRaAU1e+YqmNyraaGzE82ouUPH/I4A9gur4j4j6t1X/t0iovyd/4qNDsetUPevQsJS224Pv6Xhg3LGnSAXMPM+tu0t3UeEfRu/l9OgP6/bnet9BUx0BryFCVJp6fAtq7x61+WRIJesugrhHVgl/dz8CgFsVjRkqWQSNnZvt07dHNOX0mZj2U22OAkH+9ZN93wScs3bDZFXozrta7eOWhrJLcJTMrAxdHYMNKmoXqQQ0TGFV/L9blOtT8uj9US3wxeD11s4TyZePWIC5hnpUsNFoGPsBB45uwW2TSwvTTEL9bxWWzjYzSkLG5P6Kk4/JkeMh3OMFCM/LutX8QDch1n/s0CfXdy7qgh5G4I9ZhGTU+huJlumeuM4U+my0EPnA3uclJ97cw0n6K7MKwKCTTA8La2ifATunKC/U4Hjo1rf9DxofIrRIvwV5zEUIn1r6ut5fO+o+MWDupkvsMqIA7QJyCLhRp+pAlPWGDZLdrFEicN/kpULH4IGUIPn532gXzEOAG+Aq0WYDVPXGLSifSyxafiDk=
|
77
CHANGELOG.md
77
CHANGELOG.md
@ -7,6 +7,77 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.20.0] - 10-Mar-2024
|
||||
|
||||
### Added
|
||||
|
||||
- Support for an alternative way of splitting containers where the size or
|
||||
percentage is specified for the right (or bottom) container and the left (or
|
||||
top) is calculated.
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump github.com/gdamore/tcell/v2 from 2.7.0 to 2.7.4.
|
||||
|
||||
## [0.19.0] - 29-Jan-2024
|
||||
|
||||
### Added
|
||||
|
||||
- Support for "Backtab" (a.k.a. Shift+Tab) as a supported keystroke for tcell
|
||||
widgets.
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated CI from Travis to Github Actions.
|
||||
- Bump github.com/gdamore/tcell/v2 from 2.5.4 to 2.7.0.
|
||||
- Change the Go version in `go.mod` to 1.21.
|
||||
- Executed `go mod tidy`.
|
||||
- CI now executes tests with Golang v1.20 and v1.21.
|
||||
|
||||
## [0.18.0] - 08-Feb-2023
|
||||
|
||||
### Added
|
||||
|
||||
- The `gauge` widget now supports drawing of a vertical threshold bar.
|
||||
- The `TextInput` widget now supports an OnChange handler that allows user code
|
||||
to be notified when the content of the text input changes.
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump github.com/gdamore/tcell/v2 from 2.5.1 to 2.5.4.
|
||||
- Bump github.com/mattn/go-runewidth from 0.0.13 to 0.0.14.
|
||||
- Bump github.com/gdamore/tcell/v2 from 2.5.1 to 2.5.3.
|
||||
- Bump github.com/gdamore/tcell/v2 from 2.5.1 to 2.5.2
|
||||
- Change the Go version in `go.mod` to 1.20.
|
||||
- Executed `go mod tidy`.
|
||||
- CI now executes tests with Golang v1.20 only.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the `Sourcegraph` badge from the main page.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Formatted all Go files with `gofmt` from Golang v1.20.
|
||||
- Fixed line coverage reporting.
|
||||
|
||||
## [0.17.0] - 07-Jul-2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for `tcell`'s `Dim` text style.
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump github.com/gdamore/tcell from 2.4.0 to 2.5.1.
|
||||
- Bump github.com/nsf/termbox-go to v1.1.1.
|
||||
- Change the Go version in `go.mod` to 1.17.
|
||||
- Executed `go mod tidy`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed invalid path to the `autogen_licences.sh` script in `.travis.ci`.
|
||||
|
||||
## [0.16.1] - 13-Jan-2022
|
||||
|
||||
### Changed
|
||||
@ -472,7 +543,11 @@ identifiers shouldn't be used externally.
|
||||
- The Gauge widget.
|
||||
- The Text widget.
|
||||
|
||||
[unreleased]: https://github.com/mum4k/termdash/compare/v0.16.1...devel
|
||||
[unreleased]: https://github.com/mum4k/termdash/compare/v0.20.0...devel
|
||||
[0.20.0]: https://github.com/mum4k/termdash/compare/v0.19.0...v0.20.0
|
||||
[0.19.0]: https://github.com/mum4k/termdash/compare/v0.18.0...v0.19.0
|
||||
[0.18.0]: https://github.com/mum4k/termdash/compare/v0.17.0...v0.18.0
|
||||
[0.17.0]: https://github.com/mum4k/termdash/compare/v0.16.1...v0.17.0
|
||||
[0.16.1]: https://github.com/mum4k/termdash/compare/v0.16.0...v0.16.1
|
||||
[0.16.0]: https://github.com/mum4k/termdash/compare/v0.15.0...v0.16.0
|
||||
[0.15.0]: https://github.com/mum4k/termdash/compare/v0.14.0...v0.15.0
|
||||
|
@ -1,6 +1,5 @@
|
||||
[](https://godoc.org/github.com/mum4k/termdash)
|
||||
[](https://travis-ci.com/mum4k/termdash)
|
||||
[](https://sourcegraph.com/github.com/mum4k/termdash?badge)
|
||||
[](https://app.travis-ci.com/github/mum4k/termdash)
|
||||
[](https://coveralls.io/github/mum4k/termdash?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/mum4k/termdash)
|
||||
[](https://github.com/mum4k/termdash/blob/master/LICENSE)
|
||||
@ -220,6 +219,7 @@ Termdash uses [this branching model](https://nvie.com/posts/a-successful-git-bra
|
||||
- [perfstat](https://github.com/flaviostutz/perfstat): Analyze and show tips about possible bottlenecks in Linux systems.
|
||||
- [gex](https://github.com/Tosch110/gex): Cosmos SDK explorer in-terminal.
|
||||
- [ali](https://github.com/nakabonne/ali): ALI HTTP load testing tool with realtime analysis.
|
||||
- [suimon](https://github.com/bartosian/suimon): SUI blockchain explorer and monitor.
|
||||
|
||||
# Disclaimer
|
||||
|
||||
|
@ -31,6 +31,7 @@ type Options struct {
|
||||
Strikethrough bool
|
||||
Inverse bool
|
||||
Blink bool
|
||||
Dim bool
|
||||
}
|
||||
|
||||
// Set allows existing options to be passed as an option.
|
||||
@ -110,3 +111,10 @@ func Blink() Option {
|
||||
co.Blink = true
|
||||
})
|
||||
}
|
||||
|
||||
// Dim makes the cell foreground color dim. Only works when using the tcell backend.
|
||||
func Dim() Option {
|
||||
return option(func(co *Options) {
|
||||
co.Dim = true
|
||||
})
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ func TestNewOptions(t *testing.T) {
|
||||
Strikethrough(),
|
||||
Inverse(),
|
||||
Blink(),
|
||||
Dim(),
|
||||
},
|
||||
want: &Options{
|
||||
Bold: true,
|
||||
@ -90,6 +91,7 @@ func TestNewOptions(t *testing.T) {
|
||||
Strikethrough: true,
|
||||
Inverse: true,
|
||||
Blink: true,
|
||||
Dim: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -180,14 +180,26 @@ func (c *Container) split() (image.Rectangle, image.Rectangle, error) {
|
||||
}
|
||||
if c.opts.splitFixed > DefaultSplitFixed {
|
||||
if c.opts.split == splitTypeVertical {
|
||||
if c.opts.splitReversed {
|
||||
return area.VSplitCellsReversed(ar, c.opts.splitFixed)
|
||||
}
|
||||
return area.VSplitCells(ar, c.opts.splitFixed)
|
||||
}
|
||||
if c.opts.splitReversed {
|
||||
return area.HSplitCellsReversed(ar, c.opts.splitFixed)
|
||||
}
|
||||
return area.HSplitCells(ar, c.opts.splitFixed)
|
||||
}
|
||||
|
||||
if c.opts.split == splitTypeVertical {
|
||||
if c.opts.splitReversed {
|
||||
return area.VSplitReversed(ar, c.opts.splitPercent)
|
||||
}
|
||||
return area.VSplit(ar, c.opts.splitPercent)
|
||||
}
|
||||
if c.opts.splitReversed {
|
||||
return area.HSplitReversed(ar, c.opts.splitPercent)
|
||||
}
|
||||
return area.HSplit(ar, c.opts.splitPercent)
|
||||
}
|
||||
|
||||
|
@ -713,7 +713,33 @@ func TestNew(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "horizontal unequal split",
|
||||
desc: "horizontal, reversed unequal split",
|
||||
termSize: image.Point{10, 20},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitHorizontal(
|
||||
Top(
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Bottom(
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
SplitPercentFromEnd(20),
|
||||
),
|
||||
)
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 16))
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 16, 10, 20))
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "horizontal fixed splits",
|
||||
termSize: image.Point{10, 20},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
@ -738,6 +764,32 @@ func TestNew(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "horizontal, reversed fixed splits",
|
||||
termSize: image.Point{10, 20},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitHorizontal(
|
||||
Top(
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Bottom(
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
SplitFixedFromEnd(4),
|
||||
),
|
||||
)
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 16))
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 16, 10, 20))
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "horizontal split, parent and children have borders",
|
||||
termSize: image.Point{10, 10},
|
||||
@ -864,6 +916,32 @@ func TestNew(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "vertical, reversed unequal split",
|
||||
termSize: image.Point{20, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Right(
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
SplitPercentFromEnd(20),
|
||||
),
|
||||
)
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 0, 16, 10))
|
||||
testdraw.MustBorder(cvs, image.Rect(16, 0, 20, 10))
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "vertical fixed splits",
|
||||
termSize: image.Point{20, 10},
|
||||
@ -890,6 +968,32 @@ func TestNew(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "vertical, reversed fixed splits",
|
||||
termSize: image.Point{20, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Right(
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
SplitFixedFromEnd(4),
|
||||
),
|
||||
)
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 0, 16, 10))
|
||||
testdraw.MustBorder(cvs, image.Rect(16, 0, 20, 10))
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "vertical split, parent and children have borders",
|
||||
termSize: image.Point{10, 10},
|
||||
@ -1939,7 +2043,7 @@ func TestMouse(t *testing.T) {
|
||||
),
|
||||
Right(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
|
||||
KeyFocusPrevious(keyboard.KeyTab),
|
||||
KeyFocusPrevious(keyboard.KeyBacktab),
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -1949,7 +2053,7 @@ func TestMouse(t *testing.T) {
|
||||
return c, nil
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyBacktab},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
@ -1966,7 +2070,7 @@ func TestMouse(t *testing.T) {
|
||||
&widgetapi.Meta{Focused: true},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
|
||||
&fakewidget.Event{
|
||||
Ev: &terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
Ev: &terminalapi.Keyboard{Key: keyboard.KeyBacktab},
|
||||
Meta: &widgetapi.EventMeta{Focused: true},
|
||||
},
|
||||
)
|
||||
|
@ -529,7 +529,7 @@ func TestFocusTrackerNextAndPrevious(t *testing.T) {
|
||||
|
||||
const (
|
||||
keyNext keyboard.Key = keyboard.KeyTab
|
||||
keyPrevious keyboard.Key = '~'
|
||||
keyPrevious keyboard.Key = keyboard.KeyBacktab
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
|
@ -53,11 +53,13 @@ func (b *Builder) Build() ([]container.Option, error) {
|
||||
|
||||
// validate recursively validates the elements that were added to the builder.
|
||||
// Validates the following per each level of Rows or Columns.:
|
||||
// The subElements are either exactly one Widget or any number of Rows and
|
||||
// Columns.
|
||||
// Each individual width or height is in the range 0 < v < 100.
|
||||
// The sum of all widths is <= 100.
|
||||
// The sum of all heights is <= 100.
|
||||
//
|
||||
// The subElements are either exactly one Widget or any number of Rows and
|
||||
// Columns.
|
||||
// Each individual width or height is in the range 0 < v < 100.
|
||||
// The sum of all widths is <= 100.
|
||||
// The sum of all heights is <= 100.
|
||||
//
|
||||
// Argument fixedSizeParent indicates if any of the parent elements uses fixed
|
||||
// size splitType.
|
||||
func validate(elems []Element, fixedSizeParent bool) error {
|
||||
@ -182,17 +184,26 @@ func build(elems []Element, parentHeightPerc, parentWidthPerc int) []container.O
|
||||
// E.g. multiple rows would specify that they want the outer split percentage
|
||||
// of 25% each, but we are representing them in a tree of containers so the
|
||||
// inner splits vary:
|
||||
// ╭─────────╮
|
||||
//
|
||||
// ╭─────────╮
|
||||
//
|
||||
// 25% │ 25% │
|
||||
// │╭───────╮│ ---
|
||||
//
|
||||
// │╭───────╮│ ---
|
||||
//
|
||||
// 25% ││ 33% ││
|
||||
// ││╭─────╮││
|
||||
//
|
||||
// ││╭─────╮││
|
||||
//
|
||||
// 25% │││ 50% │││
|
||||
// ││├─────┤││ 75%
|
||||
//
|
||||
// ││├─────┤││ 75%
|
||||
//
|
||||
// 25% │││ 50% │││
|
||||
// ││╰─────╯││
|
||||
// │╰───────╯│
|
||||
// ╰─────────╯ ---
|
||||
//
|
||||
// ││╰─────╯││
|
||||
// │╰───────╯│
|
||||
// ╰─────────╯ ---
|
||||
//
|
||||
// Argument outerPerc is the user specified percentage for the split, i.e. the
|
||||
// 25% in the example above.
|
||||
|
@ -108,9 +108,10 @@ type options struct {
|
||||
inherited inherited
|
||||
|
||||
// split identifies how is this container split.
|
||||
split splitType
|
||||
splitPercent int
|
||||
splitFixed int
|
||||
split splitType
|
||||
splitReversed bool
|
||||
splitPercent int
|
||||
splitFixed int
|
||||
|
||||
// widget is the widget in the container.
|
||||
// A container can have either two sub containers (left and right) or a
|
||||
@ -247,10 +248,11 @@ func newOptions(parent *options) *options {
|
||||
inherited: inherited{
|
||||
focusedColor: cell.ColorYellow,
|
||||
},
|
||||
hAlign: align.HorizontalCenter,
|
||||
vAlign: align.VerticalMiddle,
|
||||
splitPercent: DefaultSplitPercent,
|
||||
splitFixed: DefaultSplitFixed,
|
||||
hAlign: align.HorizontalCenter,
|
||||
vAlign: align.VerticalMiddle,
|
||||
splitReversed: DefaultSplitReversed,
|
||||
splitPercent: DefaultSplitPercent,
|
||||
splitFixed: DefaultSplitFixed,
|
||||
}
|
||||
if parent != nil {
|
||||
opts.global = parent.global
|
||||
@ -281,13 +283,17 @@ func (so splitOption) setSplit(opts *options) error {
|
||||
return so(opts)
|
||||
}
|
||||
|
||||
// DefaultSplitReversed is the default value for the SplitReversed option.
|
||||
const DefaultSplitReversed = false
|
||||
|
||||
// DefaultSplitPercent is the default value for the SplitPercent option.
|
||||
const DefaultSplitPercent = 50
|
||||
|
||||
// DefaultSplitFixed is the default value for the SplitFixed option.
|
||||
const DefaultSplitFixed = -1
|
||||
|
||||
// SplitPercent sets the relative size of the split as percentage of the available space.
|
||||
// SplitPercent sets the relative size of the split as percentage of the
|
||||
// available space.
|
||||
// When using SplitVertical, the provided size is applied to the new left
|
||||
// container, the new right container gets the reminder of the size.
|
||||
// When using SplitHorizontal, the provided size is applied to the new top
|
||||
@ -304,6 +310,25 @@ func SplitPercent(p int) SplitOption {
|
||||
})
|
||||
}
|
||||
|
||||
// SplitPercentFromEnd sets the relative size of the split as percentage of the
|
||||
// available space.
|
||||
// When using SplitVertical, the provided size is applied to the new right
|
||||
// container, the new left container gets the reminder of the size.
|
||||
// When using SplitHorizontal, the provided size is applied to the new bottom
|
||||
// container, the new top container gets the reminder of the size.
|
||||
// The provided value must be a positive number in the range 0 < p < 100.
|
||||
// If not provided, defaults to using SplitPercent with DefaultSplitPercent.
|
||||
func SplitPercentFromEnd(p int) SplitOption {
|
||||
return splitOption(func(opts *options) error {
|
||||
if min, max := 0, 100; p <= min || p >= max {
|
||||
return fmt.Errorf("invalid split percentage %d, must be in range %d < p < %d", p, min, max)
|
||||
}
|
||||
opts.splitReversed = true
|
||||
opts.splitPercent = p
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// SplitFixed sets the size of the first container to be a fixed value
|
||||
// and makes the second container take up the remaining space.
|
||||
// When using SplitVertical, the provided size is applied to the new left
|
||||
@ -311,8 +336,9 @@ func SplitPercent(p int) SplitOption {
|
||||
// When using SplitHorizontal, the provided size is applied to the new top
|
||||
// container, the new bottom container gets the reminder of the size.
|
||||
// The provided value must be a positive number in the range 0 <= cells.
|
||||
// If SplitFixed() is not specified, it defaults to SplitPercent() and its given value.
|
||||
// Only one of SplitFixed() and SplitPercent() can be specified per container.
|
||||
// If SplitFixed* or SplitPercent* is not specified, it defaults to
|
||||
// SplitPercent() and its given value.
|
||||
// Only one SplitFixed* or SplitPercent* may be specified per container.
|
||||
func SplitFixed(cells int) SplitOption {
|
||||
return splitOption(func(opts *options) error {
|
||||
if cells < 0 {
|
||||
@ -323,6 +349,27 @@ func SplitFixed(cells int) SplitOption {
|
||||
})
|
||||
}
|
||||
|
||||
// SplitFixedFromEnd sets the size of the second container to be a fixed value
|
||||
// and makes the first container take up the remaining space.
|
||||
// When using SplitVertical, the provided size is applied to the new right
|
||||
// container, the new left container gets the reminder of the size.
|
||||
// When using SplitHorizontal, the provided size is applied to the new bottom
|
||||
// container, the new top container gets the reminder of the size.
|
||||
// The provided value must be a positive number in the range 0 <= cells.
|
||||
// If SplitFixed* or SplitPercent* is not specified, it defaults to
|
||||
// SplitPercent() and its given value.
|
||||
// Only one SplitFixed* or SplitPercent* may be specified per container.
|
||||
func SplitFixedFromEnd(cells int) SplitOption {
|
||||
return splitOption(func(opts *options) error {
|
||||
if cells < 0 {
|
||||
return fmt.Errorf("invalid fixed value %d, must be in range %d <= cells", cells, 0)
|
||||
}
|
||||
opts.splitFixed = cells
|
||||
opts.splitReversed = true
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// SplitVertical splits the container along the vertical axis into two sub
|
||||
// containers. The use of this option removes any widget placed at this
|
||||
// container, containers with sub containers cannot contain widgets.
|
||||
|
18
go.mod
18
go.mod
@ -1,11 +1,19 @@
|
||||
module github.com/mum4k/termdash
|
||||
|
||||
go 1.14
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell/v2 v2.4.0
|
||||
github.com/gdamore/tcell/v2 v2.7.4
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/nsf/termbox-go v0.0.0-20201107200903-9b52a5faed9e
|
||||
golang.org/x/text v0.3.4 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15
|
||||
github.com/nsf/termbox-go v1.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/term v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
61
go.sum
61
go.sum
@ -1,25 +1,52 @@
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
|
||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/nsf/termbox-go v0.0.0-20201107200903-9b52a5faed9e h1:T8/SzSWIDoWV9trslLNfUdJ5yHrIXXuODEy5M0vou4U=
|
||||
github.com/nsf/termbox-go v0.0.0-20201107200903-9b52a5faed9e/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
|
||||
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -65,6 +65,7 @@ var buttonNames = map[Key]string{
|
||||
KeyCtrlG: "KeyCtrlG",
|
||||
KeyBackspace: "KeyBackspace",
|
||||
KeyTab: "KeyTab",
|
||||
KeyBacktab: "KeyBacktab",
|
||||
KeyCtrlJ: "KeyCtrlJ",
|
||||
KeyCtrlK: "KeyCtrlK",
|
||||
KeyCtrlL: "KeyCtrlL",
|
||||
@ -130,6 +131,7 @@ const (
|
||||
KeyCtrlG
|
||||
KeyBackspace
|
||||
KeyTab
|
||||
KeyBacktab
|
||||
KeyCtrlJ
|
||||
KeyCtrlK
|
||||
KeyCtrlL
|
||||
|
@ -38,92 +38,210 @@ func FromSize(size image.Point) (image.Rectangle, error) {
|
||||
return image.Rect(0, 0, size.X, size.Y), nil
|
||||
}
|
||||
|
||||
// HSplit returns two new areas created by splitting the provided area at the
|
||||
// specified percentage of its width. The percentage must be in the range
|
||||
// 0 <= heightPerc <= 100.
|
||||
// hSplit returns two new areas created by splitting the provided area at the
|
||||
// specified percentage of its height, applying the percentage to the top or
|
||||
// bottom area, depending on the reversed flag. The percentage must be in the
|
||||
// range 0 <= heightPerc <= 100.
|
||||
// Can return zero size areas.
|
||||
func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
func hSplit(area image.Rectangle, heightPerc int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
if min, max := 0, 100; heightPerc < min || heightPerc > max {
|
||||
return image.ZR, image.ZR, fmt.Errorf("invalid heightPerc %d, must be in range %d <= heightPerc <= %d", heightPerc, min, max)
|
||||
}
|
||||
|
||||
height := area.Dy() * heightPerc / 100
|
||||
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height)
|
||||
|
||||
if reversed {
|
||||
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Max.Y-height)
|
||||
bottom = image.Rect(area.Min.X, area.Max.Y-height, area.Max.X, area.Max.Y)
|
||||
} else {
|
||||
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height)
|
||||
bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y)
|
||||
}
|
||||
|
||||
if top.Dy() == 0 {
|
||||
top = image.ZR
|
||||
}
|
||||
bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y)
|
||||
if bottom.Dy() == 0 {
|
||||
bottom = image.ZR
|
||||
}
|
||||
|
||||
return top, bottom, nil
|
||||
}
|
||||
|
||||
// VSplit returns two new areas created by splitting the provided area at the
|
||||
// specified percentage of its width. The percentage must be in the range
|
||||
// 0 <= widthPerc <= 100.
|
||||
// HSplit returns two new areas created by splitting the provided area at the
|
||||
// specified percentage of its height, applying the percentage to the top area.
|
||||
// The percentage must be in the range 0 <= heightPerc <= 100.
|
||||
// Can return zero size areas.
|
||||
func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) {
|
||||
func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
return hSplit(area, heightPerc, false)
|
||||
}
|
||||
|
||||
// HSplitReversed returns two new areas created by splitting the provided area
|
||||
// at the specified percentage of its height, applying the percentage to the
|
||||
// bottom area. The percentage must be in the range 0 <= heightPerc <= 100.
|
||||
// Can return zero size areas.
|
||||
func HSplitReversed(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
return hSplit(area, heightPerc, true)
|
||||
}
|
||||
|
||||
// vSplit returns two new areas created by splitting the provided area at the
|
||||
// specified percentage of its width, applying the percentage to the left or
|
||||
// right area, depending on the reversed flag. The percentage must be in the
|
||||
// range 0 <= widthPerc <= 100.
|
||||
// Can return zero size areas.
|
||||
func vSplit(area image.Rectangle, widthPerc int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) {
|
||||
if min, max := 0, 100; widthPerc < min || widthPerc > max {
|
||||
return image.ZR, image.ZR, fmt.Errorf("invalid widthPerc %d, must be in range %d <= widthPerc <= %d", widthPerc, min, max)
|
||||
}
|
||||
|
||||
width := area.Dx() * widthPerc / 100
|
||||
left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y)
|
||||
|
||||
if reversed {
|
||||
left = image.Rect(area.Min.X, area.Min.Y, area.Max.X-width, area.Max.Y)
|
||||
right = image.Rect(area.Max.X-width, area.Min.Y, area.Max.X, area.Max.Y)
|
||||
} else {
|
||||
left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y)
|
||||
right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y)
|
||||
}
|
||||
|
||||
if left.Dx() == 0 {
|
||||
left = image.ZR
|
||||
}
|
||||
right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y)
|
||||
if right.Dx() == 0 {
|
||||
right = image.ZR
|
||||
}
|
||||
|
||||
return left, right, nil
|
||||
}
|
||||
|
||||
// VSplitCells returns two new areas created by splitting the provided area
|
||||
// after the specified amount of cells of its width. The number of cells must
|
||||
// be a zero or a positive integer. Providing a zero returns left=image.ZR,
|
||||
// VSplit returns two new areas created by splitting the provided area at the
|
||||
// specified percentage of its width, applying the percentage to the left area.
|
||||
// The percentage must be in the range 0 <= widthPerc <= 100.
|
||||
// Can return zero size areas.
|
||||
func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) {
|
||||
return vSplit(area, widthPerc, false)
|
||||
}
|
||||
|
||||
// VSplitReversed returns two new areas created by splitting the provided area
|
||||
// at the specified percentage of its width, applying the percentage to the
|
||||
// right area. The percentage must be in the range 0 <= widthPerc <= 100.
|
||||
// Can return zero size areas.
|
||||
func VSplitReversed(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) {
|
||||
return vSplit(area, widthPerc, true)
|
||||
}
|
||||
|
||||
// vSplitCells returns two new areas created by splitting the provided area
|
||||
// after the specified amount of cells of its width, applied to the left or
|
||||
// right area, depending on the reversed flag. The number of cells must be a
|
||||
// zero or a positive integer. Providing a zero returns left=image.ZR,
|
||||
// right=area. Providing a number equal or larger to area's width returns
|
||||
// left=area, right=image.ZR.
|
||||
func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) {
|
||||
func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) {
|
||||
if min := 0; cells < min {
|
||||
return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
|
||||
}
|
||||
if cells == 0 {
|
||||
if reversed {
|
||||
return area, image.ZR, nil
|
||||
}
|
||||
return image.ZR, area, nil
|
||||
}
|
||||
|
||||
width := area.Dx()
|
||||
if cells >= width {
|
||||
if reversed {
|
||||
return image.ZR, area, nil
|
||||
}
|
||||
return area, image.ZR, nil
|
||||
}
|
||||
|
||||
left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+cells, area.Max.Y)
|
||||
right = image.Rect(area.Min.X+cells, area.Min.Y, area.Max.X, area.Max.Y)
|
||||
splitX := area.Min.X
|
||||
if reversed {
|
||||
splitX = area.Max.X - cells
|
||||
} else {
|
||||
splitX = area.Min.X + cells
|
||||
}
|
||||
|
||||
left = image.Rect(area.Min.X, area.Min.Y, splitX, area.Max.Y)
|
||||
right = image.Rect(splitX, area.Min.Y, area.Max.X, area.Max.Y)
|
||||
|
||||
return left, right, nil
|
||||
}
|
||||
|
||||
// HSplitCells returns two new areas created by splitting the provided area
|
||||
// after the specified amount of cells of its height. The number of cells must
|
||||
// be a zero or a positive integer. Providing a zero returns top=image.ZR,
|
||||
// VSplitCells returns two new areas created by splitting the provided area
|
||||
// after the specified amount of cells of its width, as applied to the left
|
||||
// area. The number of cells must be a zero or a positive integer. Providing a
|
||||
// zero returns left=image.ZR, right=area. Providing a number equal or larger to
|
||||
// area's width returns left=area, right=image.ZR.
|
||||
func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) {
|
||||
return vSplitCells(area, cells, false)
|
||||
}
|
||||
|
||||
// VSplitCellsReversed returns two new areas created by splitting the provided
|
||||
// area after the specified amount of cells of its width, as applied to the
|
||||
// right area. The number of cells must be a zero or a positive integer.
|
||||
// Providing a zero returns left=image.ZR, right=area. Providing a number equal
|
||||
// or larger to area's width returns left=area, right=image.ZR.
|
||||
func VSplitCellsReversed(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) {
|
||||
return vSplitCells(area, cells, true)
|
||||
}
|
||||
|
||||
// hSplitCells returns two new areas created by splitting the provided area
|
||||
// after the specified amount of cells of its height, applied to the top or
|
||||
// bottom area, depending on the reversed flag. The number of cells must be a
|
||||
// zero or a positive integer. Providing a zero returns top=image.ZR,
|
||||
// bottom=area. Providing a number equal or larger to area's height returns
|
||||
// top=area, bottom=image.ZR.
|
||||
func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
func hSplitCells(area image.Rectangle, cells int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
if min := 0; cells < min {
|
||||
return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells)
|
||||
}
|
||||
if cells == 0 {
|
||||
if reversed {
|
||||
return area, image.ZR, nil
|
||||
}
|
||||
return image.ZR, area, nil
|
||||
}
|
||||
|
||||
height := area.Dy()
|
||||
if cells >= height {
|
||||
if reversed {
|
||||
return image.ZR, area, nil
|
||||
}
|
||||
return area, image.ZR, nil
|
||||
}
|
||||
|
||||
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+cells)
|
||||
bottom = image.Rect(area.Min.X, area.Min.Y+cells, area.Max.X, area.Max.Y)
|
||||
splitY := area.Min.Y
|
||||
if reversed {
|
||||
splitY = area.Max.Y - cells
|
||||
} else {
|
||||
splitY = area.Min.Y + cells
|
||||
}
|
||||
|
||||
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, splitY)
|
||||
bottom = image.Rect(area.Min.X, splitY, area.Max.X, area.Max.Y)
|
||||
|
||||
return top, bottom, nil
|
||||
}
|
||||
|
||||
// HSplitCells returns two new areas created by splitting the provided area
|
||||
// after the specified amount of cells of its height, as applied to the top
|
||||
// area. The number of cells must be a zero or a positive integer. Providing a
|
||||
// zero returns top=image.ZR, bottom=area. Providing a number equal or larger to
|
||||
// area's height returns top=area, bottom=image.ZR.
|
||||
func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
return hSplitCells(area, cells, false)
|
||||
}
|
||||
|
||||
// HSplitCellsReversed returns two new areas created by splitting the provided
|
||||
// area after the specified amount of cells of its height, as applied to the
|
||||
// bottom area. The number of cells must be a zero or a positive integer.
|
||||
// Providing a zero returns top=area, bottom=image.ZR. Providing a number equal
|
||||
// or larger to area's height returns top=image.ZR, bottom=area.
|
||||
func HSplitCellsReversed(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) {
|
||||
return hSplitCells(area, cells, true)
|
||||
}
|
||||
|
||||
// ExcludeBorder returns a new area created by subtracting a border around the
|
||||
// provided area. Return the zero area if there isn't enough space to exclude
|
||||
// the border.
|
||||
|
@ -198,6 +198,87 @@ func TestHSplit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHSplitReversed(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
area image.Rectangle
|
||||
heightPerc int
|
||||
wantTop image.Rectangle
|
||||
wantBot image.Rectangle
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails on heightPerc too small",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
heightPerc: -1,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on heightPerc too large",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
heightPerc: 101,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "zero area to begin with",
|
||||
area: image.ZR,
|
||||
heightPerc: 50,
|
||||
wantTop: image.ZR,
|
||||
wantBot: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "splitting results in zero height area on the bottom",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
heightPerc: 0,
|
||||
wantTop: image.Rect(1, 1, 2, 2),
|
||||
wantBot: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "splitting results in 100 height area on the top",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
heightPerc: 100,
|
||||
wantTop: image.ZR,
|
||||
wantBot: image.Rect(1, 1, 2, 2),
|
||||
},
|
||||
{
|
||||
desc: "splits area with even height",
|
||||
area: image.Rect(1, 1, 3, 3),
|
||||
heightPerc: 50,
|
||||
wantTop: image.Rect(1, 1, 3, 2),
|
||||
wantBot: image.Rect(1, 2, 3, 3),
|
||||
},
|
||||
{
|
||||
desc: "splits area with odd height",
|
||||
area: image.Rect(1, 1, 4, 4),
|
||||
heightPerc: 50,
|
||||
wantTop: image.Rect(1, 1, 4, 3),
|
||||
wantBot: image.Rect(1, 3, 4, 4),
|
||||
},
|
||||
{
|
||||
desc: "splits to unequal areas",
|
||||
area: image.Rect(0, 0, 4, 4),
|
||||
heightPerc: 25,
|
||||
wantTop: image.Rect(0, 0, 4, 3),
|
||||
wantBot: image.Rect(0, 3, 4, 4),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
gotTop, gotBot, err := HSplitReversed(tc.area, tc.heightPerc)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr)
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantTop, gotTop); diff != "" {
|
||||
t.Errorf("HSplit => first value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantBot, gotBot); diff != "" {
|
||||
t.Errorf("HSplit => second value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVSplit(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
@ -282,6 +363,90 @@ func TestVSplit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVSplitReversed(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
area image.Rectangle
|
||||
widthPerc int
|
||||
wantLeft image.Rectangle
|
||||
wantRight image.Rectangle
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails on widthPerc too small",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
widthPerc: -1,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on widthPerc too large",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
widthPerc: 101,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "zero area to begin with",
|
||||
area: image.ZR,
|
||||
widthPerc: 50,
|
||||
wantLeft: image.ZR,
|
||||
wantRight: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "splitting results in zero width area on the right",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
widthPerc: 0,
|
||||
wantLeft: image.Rect(1, 1, 2, 2),
|
||||
wantRight: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "splitting results in zero width area on the left",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
widthPerc: 100,
|
||||
wantLeft: image.ZR,
|
||||
wantRight: image.Rect(1, 1, 2, 2),
|
||||
},
|
||||
{
|
||||
desc: "splits area with even width",
|
||||
area: image.Rect(1, 1, 3, 3),
|
||||
widthPerc: 50,
|
||||
wantLeft: image.Rect(1, 1, 2, 3),
|
||||
wantRight: image.Rect(2, 1, 3, 3),
|
||||
},
|
||||
{
|
||||
desc: "splits area with odd width",
|
||||
area: image.Rect(1, 1, 4, 4),
|
||||
widthPerc: 50,
|
||||
wantLeft: image.Rect(1, 1, 3, 4),
|
||||
wantRight: image.Rect(3, 1, 4, 4),
|
||||
},
|
||||
{
|
||||
desc: "splits to unequal areas",
|
||||
area: image.Rect(0, 0, 4, 4),
|
||||
widthPerc: 25,
|
||||
wantLeft: image.Rect(0, 0, 3, 4),
|
||||
wantRight: image.Rect(3, 0, 4, 4),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
gotLeft, gotRight, err := VSplitReversed(tc.area, tc.widthPerc)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantLeft, gotLeft); diff != "" {
|
||||
t.Errorf("VSplit => left value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantRight, gotRight); diff != "" {
|
||||
t.Errorf("VSplit => right value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVSplitCells(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
@ -367,6 +532,91 @@ func TestVSplitCells(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVSplitCellsReversed(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
area image.Rectangle
|
||||
cells int
|
||||
wantLeft image.Rectangle
|
||||
wantRight image.Rectangle
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails on negative cells",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: -1,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "returns area as left on cells too large",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 2,
|
||||
wantLeft: image.ZR,
|
||||
wantRight: image.Rect(1, 1, 2, 2),
|
||||
},
|
||||
{
|
||||
desc: "returns area as left on cells equal area width",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 1,
|
||||
wantLeft: image.ZR,
|
||||
wantRight: image.Rect(1, 1, 2, 2),
|
||||
},
|
||||
{
|
||||
desc: "returns area as right on zero cells",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 0,
|
||||
wantRight: image.ZR,
|
||||
wantLeft: image.Rect(1, 1, 2, 2),
|
||||
},
|
||||
{
|
||||
desc: "zero area to begin with",
|
||||
area: image.ZR,
|
||||
cells: 0,
|
||||
wantLeft: image.ZR,
|
||||
wantRight: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "splits area with even width",
|
||||
area: image.Rect(1, 1, 3, 3),
|
||||
cells: 1,
|
||||
wantLeft: image.Rect(1, 1, 2, 3),
|
||||
wantRight: image.Rect(2, 1, 3, 3),
|
||||
},
|
||||
{
|
||||
desc: "splits area with odd width",
|
||||
area: image.Rect(1, 1, 4, 4),
|
||||
cells: 1,
|
||||
wantLeft: image.Rect(1, 1, 3, 4),
|
||||
wantRight: image.Rect(3, 1, 4, 4),
|
||||
},
|
||||
{
|
||||
desc: "splits to unequal areas",
|
||||
area: image.Rect(0, 0, 4, 4),
|
||||
cells: 3,
|
||||
wantLeft: image.Rect(0, 0, 1, 4),
|
||||
wantRight: image.Rect(1, 0, 4, 4),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
gotLeft, gotRight, err := VSplitCellsReversed(tc.area, tc.cells)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("VSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantLeft, gotLeft); diff != "" {
|
||||
t.Errorf("VSplitCells => left value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantRight, gotRight); diff != "" {
|
||||
t.Errorf("VSplitCells => right value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHSplitCells(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
@ -452,6 +702,91 @@ func TestHSplitCells(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHSplitCellsReversed(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
area image.Rectangle
|
||||
cells int
|
||||
wantTop image.Rectangle
|
||||
wantBottom image.Rectangle
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails on negative cells",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: -1,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "returns area as bottom on cells too large",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 2,
|
||||
wantTop: image.ZR,
|
||||
wantBottom: image.Rect(1, 1, 2, 2),
|
||||
},
|
||||
{
|
||||
desc: "returns area as bottom on cells equal area width",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 1,
|
||||
wantTop: image.ZR,
|
||||
wantBottom: image.Rect(1, 1, 2, 2),
|
||||
},
|
||||
{
|
||||
desc: "returns area as top on zero cells",
|
||||
area: image.Rect(1, 1, 2, 2),
|
||||
cells: 0,
|
||||
wantBottom: image.ZR,
|
||||
wantTop: image.Rect(1, 1, 2, 2),
|
||||
},
|
||||
{
|
||||
desc: "zero area to begin with",
|
||||
area: image.ZR,
|
||||
cells: 0,
|
||||
wantTop: image.ZR,
|
||||
wantBottom: image.ZR,
|
||||
},
|
||||
{
|
||||
desc: "splits area with even height",
|
||||
area: image.Rect(1, 1, 3, 3),
|
||||
cells: 1,
|
||||
wantTop: image.Rect(1, 1, 3, 2),
|
||||
wantBottom: image.Rect(1, 2, 3, 3),
|
||||
},
|
||||
{
|
||||
desc: "splits area with odd width",
|
||||
area: image.Rect(1, 1, 4, 4),
|
||||
cells: 1,
|
||||
wantTop: image.Rect(1, 1, 4, 3),
|
||||
wantBottom: image.Rect(1, 3, 4, 4),
|
||||
},
|
||||
{
|
||||
desc: "splits to unequal areas",
|
||||
area: image.Rect(0, 0, 4, 4),
|
||||
cells: 3,
|
||||
wantTop: image.Rect(0, 0, 4, 1),
|
||||
wantBottom: image.Rect(0, 1, 4, 4),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
gotTop, gotBottom, err := HSplitCellsReversed(tc.area, tc.cells)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("HSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantTop, gotTop); diff != "" {
|
||||
t.Errorf("HSplitCells => left value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := pretty.Compare(tc.wantBottom, gotBottom); diff != "" {
|
||||
t.Errorf("HSplitCells => right value unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExcludeBorder(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
|
@ -27,13 +27,13 @@ right and down.
|
||||
|
||||
Each cell:
|
||||
|
||||
X→ 0 1 Y
|
||||
┌───┐ ↓
|
||||
│● ●│ 0
|
||||
│● ●│ 1
|
||||
│● ●│ 2
|
||||
│● ●│ 3
|
||||
└───┘
|
||||
X→ 0 1 Y
|
||||
┌───┐ ↓
|
||||
│● ●│ 0
|
||||
│● ●│ 1
|
||||
│● ●│ 2
|
||||
│● ●│ 3
|
||||
└───┘
|
||||
|
||||
When using the braille canvas, the coordinates address the sub-cell points
|
||||
rather then cells themselves. However all points in the cell still share the
|
||||
|
@ -75,11 +75,12 @@ func VerticalTextOverrunMode(om OverrunMode) VerticalTextOption {
|
||||
|
||||
// VerticalText prints the provided text on the canvas starting at the provided point.
|
||||
// The text is printed in a vertical orientation, i.e:
|
||||
// H
|
||||
// e
|
||||
// l
|
||||
// l
|
||||
// o
|
||||
//
|
||||
// H
|
||||
// e
|
||||
// l
|
||||
// l
|
||||
// o
|
||||
func VerticalText(c *canvas.Canvas, text string, start image.Point, opts ...VerticalTextOption) error {
|
||||
ar := c.Area()
|
||||
if !start.In(ar) {
|
||||
|
@ -1,3 +1,17 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package faketerm
|
||||
|
||||
import (
|
||||
|
@ -128,9 +128,10 @@ func (ar *angleRange) contains(angle int) bool {
|
||||
// normalizeRange normalizes the start and end angles in degrees into ranges of
|
||||
// angles. Useful for cases where the 0/360 point falls within the range.
|
||||
// E.g:
|
||||
// 0,25 => angleRange{0, 26}
|
||||
// 0,360 => angleRange{0, 361}
|
||||
// 359,20 => angleRange{359, 361}, angleRange{0, 21}
|
||||
//
|
||||
// 0,25 => angleRange{0, 26}
|
||||
// 0,360 => angleRange{0, 361}
|
||||
// 359,20 => angleRange{359, 361}, angleRange{0, 21}
|
||||
func normalizeRange(start, end int) ([]*angleRange, error) {
|
||||
if start < MinAngle || start > MaxAngle {
|
||||
return nil, fmt.Errorf("invalid start angle:%d, must be in range %d <= start <= %d", start, MinAngle, MaxAngle)
|
||||
@ -159,8 +160,9 @@ func normalizeRange(start, end int) ([]*angleRange, error) {
|
||||
|
||||
// RangeSize returns the size of the degree range.
|
||||
// E.g:
|
||||
// 0,25 => 25
|
||||
// 359,1 => 2
|
||||
//
|
||||
// 0,25 => 25
|
||||
// 359,1 => 2
|
||||
func RangeSize(start, end int) (int, error) {
|
||||
ranges, err := normalizeRange(start, end)
|
||||
if err != nil {
|
||||
@ -174,8 +176,9 @@ func RangeSize(start, end int) (int, error) {
|
||||
|
||||
// RangeMid returns an angle that lies in the middle between start and end.
|
||||
// E.g:
|
||||
// 0,10 => 5
|
||||
// 350,10 => 0
|
||||
//
|
||||
// 0,10 => 5
|
||||
// 350,10 => 0
|
||||
func RangeMid(start, end int) (int, error) {
|
||||
ranges, err := normalizeRange(start, end)
|
||||
if err != nil {
|
||||
|
@ -21,19 +21,19 @@ display dot characters.
|
||||
|
||||
The following outlines segments in the display and their names.
|
||||
|
||||
---------------
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| o D1 |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| o D2 |
|
||||
| |
|
||||
| |
|
||||
| o D3 |
|
||||
---------------
|
||||
---------------
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| o D1 |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| o D2 |
|
||||
| |
|
||||
| |
|
||||
| o D3 |
|
||||
---------------
|
||||
*/
|
||||
package dotseg
|
||||
|
||||
|
@ -95,14 +95,15 @@ func SkipSlopesLTE(v int) Option {
|
||||
// This only has a visible effect when the horizontal segment has height of two
|
||||
// or the vertical segment has width of two.
|
||||
// Without this option segments with height / width of two look like this:
|
||||
// - |
|
||||
// --- ||
|
||||
// |
|
||||
//
|
||||
// With this option:
|
||||
// --- |
|
||||
// - ||
|
||||
// |
|
||||
// x - |
|
||||
// x --- ||
|
||||
// x |
|
||||
// x
|
||||
// x With this option:
|
||||
// x
|
||||
// x --- |
|
||||
// x - ||
|
||||
// x |
|
||||
func ReverseSlopes() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.reverseSlopes = true
|
||||
|
@ -21,21 +21,21 @@ display ASCII characters.
|
||||
|
||||
The following outlines segments in the display and their names.
|
||||
|
||||
A1 A2
|
||||
------- -------
|
||||
| \ | / |
|
||||
| \ | / |
|
||||
F | H J K | B
|
||||
| \ | / |
|
||||
| \ | / |
|
||||
-G1---- ----G2-
|
||||
| / | \ |
|
||||
| / | \ |
|
||||
E | N M L | C
|
||||
| / | \ |
|
||||
| / | \ |
|
||||
------- -------
|
||||
D1 D2
|
||||
A1 A2
|
||||
------- -------
|
||||
| \ | / |
|
||||
| \ | / |
|
||||
F | H J K | B
|
||||
| \ | / |
|
||||
| \ | / |
|
||||
-G1---- ----G2-
|
||||
| / | \ |
|
||||
| / | \ |
|
||||
E | N M L | C
|
||||
| / | \ |
|
||||
| / | \ |
|
||||
------- -------
|
||||
D1 D2
|
||||
*/
|
||||
package sixteen
|
||||
|
||||
|
@ -976,8 +976,10 @@ func newLayoutButtons(c *container.Container, w *widgets) (*layoutButtons, error
|
||||
|
||||
// rotateFloats returns a new slice with inputs rotated by step.
|
||||
// I.e. for a step of one:
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
//
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
//
|
||||
// And so on.
|
||||
func rotateFloats(inputs []float64, step int) []float64 {
|
||||
return append(inputs[step:], inputs[:step]...)
|
||||
@ -985,8 +987,10 @@ func rotateFloats(inputs []float64, step int) []float64 {
|
||||
|
||||
// rotateRunes returns a new slice with inputs rotated by step.
|
||||
// I.e. for a step of one:
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
//
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
//
|
||||
// And so on.
|
||||
func rotateRunes(inputs []rune, step int) []rune {
|
||||
return append(inputs[step:], inputs[:step]...)
|
||||
|
@ -70,6 +70,7 @@ func cellOptsToStyle(opts *cell.Options, colorMode terminalapi.ColorMode) tcell.
|
||||
Underline(opts.Underline).
|
||||
StrikeThrough(opts.Strikethrough).
|
||||
Reverse(opts.Inverse).
|
||||
Blink(opts.Blink)
|
||||
Blink(opts.Blink).
|
||||
Dim(opts.Dim)
|
||||
return st
|
||||
}
|
||||
|
@ -325,6 +325,11 @@ func TestCellOptsToStyle(t *testing.T) {
|
||||
opts: cell.Options{Blink: true},
|
||||
want: tcell.StyleDefault.Blink(true),
|
||||
},
|
||||
{
|
||||
colorMode: terminalapi.ColorModeNormal,
|
||||
opts: cell.Options{Dim: true},
|
||||
want: tcell.StyleDefault.Dim(true),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
@ -77,6 +77,7 @@ var tcellToTd = map[tcell.Key]keyboard.Key{
|
||||
tcell.KeyCtrlZ: keyboard.KeyCtrlZ,
|
||||
tcell.KeyBackspace: keyboard.KeyBackspace,
|
||||
tcell.KeyTab: keyboard.KeyTab,
|
||||
tcell.KeyBacktab: keyboard.KeyBacktab,
|
||||
tcell.KeyEscape: keyboard.KeyEsc,
|
||||
tcell.KeyCtrlBackslash: keyboard.KeyCtrlBackslash,
|
||||
tcell.KeyCtrlRightSq: keyboard.KeyCtrlRsqBracket,
|
||||
|
@ -218,6 +218,7 @@ func TestKeyboardKeys(t *testing.T) {
|
||||
{key: tcell.KeyBackspace, want: keyboard.KeyCtrlH},
|
||||
{key: tcell.KeyCtrlH, want: keyboard.KeyBackspace},
|
||||
{key: tcell.KeyTab, want: keyboard.KeyTab},
|
||||
{key: tcell.KeyBacktab, want: keyboard.KeyBacktab},
|
||||
{key: tcell.KeyTab, want: keyboard.KeyCtrlI},
|
||||
{key: tcell.KeyCtrlI, want: keyboard.KeyTab},
|
||||
{key: tcell.KeyCtrlJ, want: keyboard.KeyCtrlJ},
|
||||
|
@ -67,6 +67,11 @@ func cellOptsToFg(opts *cell.Options) (tbx.Attribute, error) {
|
||||
if opts.Blink {
|
||||
return 0, errors.New("Termbox: Unsupported attribute: Blink")
|
||||
}
|
||||
|
||||
if opts.Dim {
|
||||
return 0, errors.New("Termbox: Unsupported attribute: Dim")
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,7 @@ func TestCellFontModifier(t *testing.T) {
|
||||
{cell.Options{Strikethrough: true}, 0, true},
|
||||
{cell.Options{Inverse: true}, tbx.AttrReverse, false},
|
||||
{cell.Options{Blink: true}, 0, true},
|
||||
{cell.Options{Dim: true}, 0, true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
@ -136,10 +136,12 @@ func (g *Gauge) Percent(p int, opts ...Option) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// width determines the required width of the gauge drawn on the provided area
|
||||
// in order to represent the current progress.
|
||||
func (g *Gauge) width(ar image.Rectangle) int {
|
||||
mult := float32(g.current) / float32(g.total)
|
||||
// width determines the X coordinate that represents point w in rectangle ar.
|
||||
// This is used to calculate the width of the gauge drawn on the provided area
|
||||
// in order to represent the current progress or to figure out the coordinate
|
||||
// for the threshold line.
|
||||
func (g *Gauge) width(ar image.Rectangle, w int) int {
|
||||
mult := float32(w) / float32(g.total)
|
||||
width := float32(ar.Dx()) * mult
|
||||
return int(width)
|
||||
}
|
||||
@ -157,6 +159,11 @@ func (g *Gauge) usable(cvs *canvas.Canvas) image.Rectangle {
|
||||
return cvs.Area()
|
||||
}
|
||||
|
||||
// thresholdVisible determines if the threshold line should be drawn.
|
||||
func (g *Gauge) thresholdVisible() bool {
|
||||
return g.opts.threshold > 0 && g.opts.threshold < g.total
|
||||
}
|
||||
|
||||
// progressText returns the textual representation of the current progress.
|
||||
func (g *Gauge) progressText() string {
|
||||
if g.opts.hideTextProgress {
|
||||
@ -244,6 +251,26 @@ func (g *Gauge) drawText(cvs *canvas.Canvas, progress image.Rectangle) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawThreshold draws the threshold line.
|
||||
func (g *Gauge) drawThreshold(cvs *canvas.Canvas) error {
|
||||
ar := g.usable(cvs)
|
||||
|
||||
line := draw.HVLine{
|
||||
Start: image.Point{
|
||||
X: ar.Min.X + g.width(ar, g.opts.threshold),
|
||||
Y: cvs.Area().Min.Y,
|
||||
},
|
||||
End: image.Point{
|
||||
X: ar.Min.X + g.width(ar, g.opts.threshold),
|
||||
Y: cvs.Area().Max.Y - 1,
|
||||
},
|
||||
}
|
||||
return draw.HVLines(cvs, []draw.HVLine{line},
|
||||
draw.HVLineStyle(g.opts.thresholdLineStyle),
|
||||
draw.HVLineCellOpts(g.opts.thresholdCellOpts...),
|
||||
)
|
||||
}
|
||||
|
||||
// Draw draws the Gauge widget onto the canvas.
|
||||
// Implements widgetapi.Widget.Draw.
|
||||
func (g *Gauge) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
|
||||
@ -273,7 +300,7 @@ func (g *Gauge) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
|
||||
progress := image.Rect(
|
||||
usable.Min.X,
|
||||
usable.Min.Y,
|
||||
usable.Min.X+g.width(usable),
|
||||
usable.Min.X+g.width(usable, g.current),
|
||||
usable.Max.Y,
|
||||
)
|
||||
if progress.Dx() > 0 {
|
||||
@ -284,6 +311,12 @@ func (g *Gauge) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if g.thresholdVisible() {
|
||||
if err := g.drawThreshold(cvs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return g.drawText(cvs, progress)
|
||||
}
|
||||
|
||||
|
@ -70,9 +70,9 @@ func TestGauge(t *testing.T) {
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on negative height",
|
||||
desc: "fails on negative threshold",
|
||||
opts: []Option{
|
||||
Height(-1),
|
||||
Threshold(-1, linestyle.Light),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
@ -841,6 +841,157 @@ func TestGauge(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "threshold with border percentage",
|
||||
opts: []Option{
|
||||
Char('o'),
|
||||
Threshold(20, linestyle.Double),
|
||||
},
|
||||
percent: &percentCall{p: 35},
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustRectangle(c, image.Rect(0, 0, 3, 3),
|
||||
draw.RectChar('o'),
|
||||
draw.RectCellOpts(cell.BgColor(cell.ColorGreen)),
|
||||
)
|
||||
testdraw.MustText(c, "35%", image.Point{3, 1})
|
||||
testdraw.MustHVLines(c, []draw.HVLine{{
|
||||
Start: image.Point{X: 2, Y: 0},
|
||||
End: image.Point{X: 2, Y: 2},
|
||||
}}, draw.HVLineStyle(linestyle.Double))
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "threshold without border absolute",
|
||||
opts: []Option{
|
||||
Char('o'),
|
||||
Threshold(3, linestyle.Light, cell.BgColor(cell.ColorRed)),
|
||||
Border(linestyle.None),
|
||||
HideTextProgress(),
|
||||
},
|
||||
absolute: &absoluteCall{done: 4, total: 10},
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustRectangle(c, image.Rect(0, 0, 3, 3),
|
||||
draw.RectChar('o'),
|
||||
draw.RectCellOpts(cell.BgColor(cell.ColorGreen)),
|
||||
)
|
||||
testdraw.MustHVLines(c, []draw.HVLine{{
|
||||
Start: image.Point{X: 3, Y: 0},
|
||||
End: image.Point{X: 3, Y: 2},
|
||||
}}, draw.HVLineStyle(linestyle.Light),
|
||||
draw.HVLineCellOpts(cell.BgColor(cell.ColorRed)))
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "threshold outside of bounds (>=max)",
|
||||
opts: []Option{
|
||||
Char('o'),
|
||||
HideTextProgress(),
|
||||
Threshold(100, linestyle.Light), // ignored
|
||||
},
|
||||
percent: &percentCall{p: 35},
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustRectangle(c, image.Rect(0, 0, 3, 3),
|
||||
draw.RectChar('o'),
|
||||
draw.RectCellOpts(cell.BgColor(cell.ColorGreen)),
|
||||
)
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "progress below threshold",
|
||||
opts: []Option{
|
||||
Char('o'),
|
||||
Threshold(5, linestyle.Light, cell.BgColor(cell.ColorRed)),
|
||||
HideTextProgress(),
|
||||
},
|
||||
absolute: &absoluteCall{done: 4, total: 10},
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustRectangle(c, image.Rect(0, 0, 4, 3),
|
||||
draw.RectChar('o'),
|
||||
draw.RectCellOpts(cell.BgColor(cell.ColorGreen)),
|
||||
)
|
||||
testdraw.MustHVLines(c, []draw.HVLine{{
|
||||
Start: image.Point{X: 5, Y: 0},
|
||||
End: image.Point{X: 5, Y: 2},
|
||||
}}, draw.HVLineStyle(linestyle.Light),
|
||||
draw.HVLineCellOpts(cell.BgColor(cell.ColorRed)))
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "progress exactly at threshold",
|
||||
opts: []Option{
|
||||
Char('o'),
|
||||
Threshold(5, linestyle.Light, cell.BgColor(cell.ColorRed)),
|
||||
HideTextProgress(),
|
||||
},
|
||||
absolute: &absoluteCall{done: 5, total: 10},
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustRectangle(c, image.Rect(0, 0, 5, 3),
|
||||
draw.RectChar('o'),
|
||||
draw.RectCellOpts(cell.BgColor(cell.ColorGreen)),
|
||||
)
|
||||
testdraw.MustHVLines(c, []draw.HVLine{{
|
||||
Start: image.Point{X: 5, Y: 0},
|
||||
End: image.Point{X: 5, Y: 2},
|
||||
}}, draw.HVLineStyle(linestyle.Light),
|
||||
draw.HVLineCellOpts(cell.BgColor(cell.ColorRed)))
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "progress after threshold",
|
||||
opts: []Option{
|
||||
Char('o'),
|
||||
Threshold(5, linestyle.Light, cell.BgColor(cell.ColorRed)),
|
||||
HideTextProgress(),
|
||||
},
|
||||
absolute: &absoluteCall{done: 6, total: 10},
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustRectangle(c, image.Rect(0, 0, 6, 3),
|
||||
draw.RectChar('o'),
|
||||
draw.RectCellOpts(cell.BgColor(cell.ColorGreen)),
|
||||
)
|
||||
testdraw.MustHVLines(c, []draw.HVLine{{
|
||||
Start: image.Point{X: 5, Y: 0},
|
||||
End: image.Point{X: 5, Y: 2},
|
||||
}}, draw.HVLineStyle(linestyle.Light),
|
||||
draw.HVLineCellOpts(cell.BgColor(cell.ColorRed)))
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
@ -101,6 +101,7 @@ func main() {
|
||||
gauge.Color(cell.ColorNumber(33)),
|
||||
gauge.Border(linestyle.Light),
|
||||
gauge.BorderTitle("Absolute progress"),
|
||||
gauge.Threshold(43, linestyle.Light, cell.FgColor(cell.ColorRed)),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -124,6 +125,7 @@ func main() {
|
||||
gauge.Color(cell.ColorRed),
|
||||
gauge.FilledTextColor(cell.ColorBlack),
|
||||
gauge.EmptyTextColor(cell.ColorYellow),
|
||||
gauge.Threshold(20, linestyle.Double),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -47,6 +47,10 @@ type options struct {
|
||||
borderCellOpts []cell.Option
|
||||
borderTitle string
|
||||
borderTitleHAlign align.Horizontal
|
||||
// If set draws a vertical line representing the threshold.
|
||||
threshold int
|
||||
thresholdCellOpts []cell.Option
|
||||
thresholdLineStyle linestyle.LineStyle
|
||||
}
|
||||
|
||||
// newOptions returns options with the default values set.
|
||||
@ -66,6 +70,9 @@ func (o *options) validate() error {
|
||||
if got, min := o.height, 0; got < min {
|
||||
return fmt.Errorf("invalid Height %d, must be %d <= Height", got, min)
|
||||
}
|
||||
if got, min := o.threshold, 0; got < min {
|
||||
return fmt.Errorf("invalid Threshold %d, must be %d <= Threshold", got, min)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -201,3 +208,17 @@ func BorderTitleAlign(h align.Horizontal) Option {
|
||||
opts.borderTitleHAlign = h
|
||||
})
|
||||
}
|
||||
|
||||
// Threshold configures the Gauge to display a vertical threshold line at value
|
||||
// t. If the progress is set by a call to Percent(), t represents a percentage,
|
||||
// e.g. "40" means line is displayed at 40%. If the progress is set by a call to
|
||||
// Absolute(), the threshold is considered an absolute number.
|
||||
// Threshold must be positive to be displayed. If the threshold is zero or
|
||||
// greater than total, it won't be displayed. Defaults to zero.
|
||||
func Threshold(t int, ls linestyle.LineStyle, cOpts ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.threshold = t
|
||||
opts.thresholdLineStyle = ls
|
||||
opts.thresholdCellOpts = cOpts
|
||||
})
|
||||
}
|
||||
|
@ -328,9 +328,10 @@ func (xs *XScale) CellLabel(x int) (*Value, error) {
|
||||
// the position. Positions grow up, coordinates grow down.
|
||||
//
|
||||
// Positions Y Coordinates
|
||||
// 2 | 0
|
||||
// 1 | 1
|
||||
// 0 | 2
|
||||
//
|
||||
// 2 | 0
|
||||
// 1 | 1
|
||||
// 0 | 2
|
||||
func positionToY(pos int, height int) (int, error) {
|
||||
max := height - 1
|
||||
if min := 0; pos < min || pos > max {
|
||||
|
@ -62,8 +62,10 @@ func clock(ctx context.Context, sd *segmentdisplay.SegmentDisplay) {
|
||||
|
||||
// rotate returns a new slice with inputs rotated by step.
|
||||
// I.e. for a step of one:
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
//
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
//
|
||||
// And so on.
|
||||
func rotate(inputs []rune, step int) []rune {
|
||||
return append(inputs[step:], inputs[:step]...)
|
||||
|
@ -105,7 +105,9 @@ func (t *Text) contentCells() int {
|
||||
// Write writes text for the widget to display. Multiple calls append
|
||||
// additional text. The text contain cannot control characters
|
||||
// (unicode.IsControl) or space character (unicode.IsSpace) other than:
|
||||
// ' ', '\n'
|
||||
//
|
||||
// ' ', '\n'
|
||||
//
|
||||
// Any newline ('\n') characters are interpreted as newlines when displaying
|
||||
// the text.
|
||||
func (t *Text) Write(text string, wOpts ...WriteOption) error {
|
||||
|
@ -274,11 +274,14 @@ type fieldEditor struct {
|
||||
|
||||
// width is the width of the text input field last time viewFor was called.
|
||||
width int
|
||||
|
||||
// onChange if provided is the handler called when fieldData changes
|
||||
onChange ChangeFn
|
||||
}
|
||||
|
||||
// newFieldEditor returns a new fieldEditor instance.
|
||||
func newFieldEditor() *fieldEditor {
|
||||
return &fieldEditor{}
|
||||
func newFieldEditor(onChange ChangeFn) *fieldEditor {
|
||||
return &fieldEditor{onChange: onChange}
|
||||
}
|
||||
|
||||
// minFieldWidth is the minimum supported width of the text input field.
|
||||
@ -326,7 +329,7 @@ func (fe *fieldEditor) content() string {
|
||||
|
||||
// reset resets the content back to zero.
|
||||
func (fe *fieldEditor) reset() {
|
||||
*fe = *newFieldEditor()
|
||||
*fe = *newFieldEditor(fe.onChange)
|
||||
}
|
||||
|
||||
// insert inserts the rune at the current position of the cursor.
|
||||
@ -338,6 +341,9 @@ func (fe *fieldEditor) insert(r rune) {
|
||||
}
|
||||
fe.data.insertAt(fe.curDataPos, r)
|
||||
fe.curDataPos++
|
||||
if fe.onChange != nil {
|
||||
fe.onChange(string(fe.data))
|
||||
}
|
||||
}
|
||||
|
||||
// delete deletes the rune at the current position of the cursor.
|
||||
@ -347,6 +353,9 @@ func (fe *fieldEditor) delete() {
|
||||
return
|
||||
}
|
||||
fe.data.deleteAt(fe.curDataPos)
|
||||
if fe.onChange != nil {
|
||||
fe.onChange(string(fe.data))
|
||||
}
|
||||
}
|
||||
|
||||
// deleteBefore deletes the rune that is immediately to the left of the cursor.
|
||||
|
@ -309,7 +309,7 @@ func TestCurCell(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
fe := newFieldEditor()
|
||||
fe := newFieldEditor(nil)
|
||||
fe.data = tc.data
|
||||
fe.firstRune = tc.firstRune
|
||||
fe.curDataPos = tc.curDataPos
|
||||
@ -323,13 +323,14 @@ func TestCurCell(t *testing.T) {
|
||||
|
||||
func TestFieldEditor(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
width int
|
||||
ops func(*fieldEditor) error
|
||||
wantView string
|
||||
wantContent string
|
||||
wantCurIdx int
|
||||
wantErr bool
|
||||
desc string
|
||||
width int
|
||||
ops func(*fieldEditor) error
|
||||
wantView string
|
||||
wantContent string
|
||||
wantCurIdx int
|
||||
wantErr bool
|
||||
wantOnChangeCalls int
|
||||
}{
|
||||
{
|
||||
desc: "fails for width too small",
|
||||
@ -352,9 +353,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.insert('c')
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "longer data than the width, cursor at the end",
|
||||
@ -366,9 +368,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.insert('d')
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "longer data than the width, cursor at the end, has full-width runes",
|
||||
@ -380,9 +383,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.insert('世')
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦世",
|
||||
wantContent: "abc世",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦世",
|
||||
wantContent: "abc世",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "width decreased, adjusts cursor and shifts data",
|
||||
@ -397,9 +401,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.insert('d')
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "cursor won't go right beyond the end of the data",
|
||||
@ -414,9 +419,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "moves cursor to the left",
|
||||
@ -432,9 +438,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "scrolls content to the left, start becomes visible",
|
||||
@ -452,9 +459,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 1,
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "scrolls content to the left, both ends invisible",
|
||||
@ -473,9 +481,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "scrolls left, then back right to make end visible again",
|
||||
@ -500,9 +509,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "scrolls left, won't go beyond the start of data",
|
||||
@ -524,9 +534,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 0,
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 0,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "scrolls left, then back right won't go beyond the end of data",
|
||||
@ -552,9 +563,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "have less data than width, all fits",
|
||||
@ -568,9 +580,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "moves cursor to the start",
|
||||
@ -587,9 +600,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorStart()
|
||||
return nil
|
||||
},
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 0,
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 0,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "moves cursor to the end",
|
||||
@ -610,9 +624,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorEnd()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "deletesBefore when cursor after the data",
|
||||
@ -629,9 +644,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.deleteBefore()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦cd",
|
||||
wantContent: "abcd",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 6,
|
||||
},
|
||||
{
|
||||
desc: "deletesBefore when cursor after the data, text has full-width rune",
|
||||
@ -648,9 +664,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.deleteBefore()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦世",
|
||||
wantContent: "abc世",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦世",
|
||||
wantContent: "abc世",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 6,
|
||||
},
|
||||
{
|
||||
desc: "deletesBefore when cursor in the middle",
|
||||
@ -673,9 +690,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.deleteBefore()
|
||||
return nil
|
||||
},
|
||||
wantView: "acd⇨",
|
||||
wantContent: "acde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "acd⇨",
|
||||
wantContent: "acde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 6,
|
||||
},
|
||||
{
|
||||
desc: "deletesBefore when cursor in the middle, full-width runes",
|
||||
@ -698,9 +716,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.deleteBefore()
|
||||
return nil
|
||||
},
|
||||
wantView: "世c⇨",
|
||||
wantContent: "世cde",
|
||||
wantCurIdx: 2,
|
||||
wantView: "世c⇨",
|
||||
wantContent: "世cde",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 6,
|
||||
},
|
||||
{
|
||||
desc: "deletesBefore does nothing when cursor at the start",
|
||||
@ -721,9 +740,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.deleteBefore()
|
||||
return nil
|
||||
},
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 0,
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 0,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "delete does nothing when cursor at the end",
|
||||
@ -740,9 +760,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "delete in the middle, last rune remains hidden",
|
||||
@ -764,9 +785,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "acd⇨",
|
||||
wantContent: "acde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "acd⇨",
|
||||
wantContent: "acde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 6,
|
||||
},
|
||||
{
|
||||
desc: "delete in the middle, last rune becomes visible",
|
||||
@ -789,9 +811,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "ade",
|
||||
wantContent: "ade",
|
||||
wantCurIdx: 1,
|
||||
wantView: "ade",
|
||||
wantContent: "ade",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 7,
|
||||
},
|
||||
{
|
||||
desc: "delete in the middle, last full-width rune would be invisible, shifts to keep cursor in window",
|
||||
@ -815,9 +838,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦世",
|
||||
wantContent: "ab世",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦世",
|
||||
wantContent: "ab世",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 7,
|
||||
},
|
||||
{
|
||||
desc: "delete in the middle, last rune was and is visible",
|
||||
@ -837,9 +861,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "ac",
|
||||
wantContent: "ac",
|
||||
wantCurIdx: 1,
|
||||
wantView: "ac",
|
||||
wantContent: "ac",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "delete in the middle, last full-width rune was and is visible",
|
||||
@ -859,9 +884,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "a世",
|
||||
wantContent: "a世",
|
||||
wantCurIdx: 1,
|
||||
wantView: "a世",
|
||||
wantContent: "a世",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "delete last rune, contains full-width runes",
|
||||
@ -882,9 +908,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "",
|
||||
wantContent: "",
|
||||
wantCurIdx: 0,
|
||||
wantView: "",
|
||||
wantContent: "",
|
||||
wantCurIdx: 0,
|
||||
wantOnChangeCalls: 6,
|
||||
},
|
||||
{
|
||||
desc: "half-width runes only, exact fit",
|
||||
@ -898,9 +925,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "full-width runes only, exact fit",
|
||||
@ -914,9 +942,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantView: "你好世",
|
||||
wantContent: "你好世",
|
||||
wantCurIdx: 6,
|
||||
wantView: "你好世",
|
||||
wantContent: "你好世",
|
||||
wantCurIdx: 6,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "half-width runes only, both ends hidden",
|
||||
@ -935,9 +964,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "half-width runes only, both ends invisible, scrolls to make start visible",
|
||||
@ -957,9 +987,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "abc⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "half-width runes only, both ends invisible, deletes to make start visible",
|
||||
@ -979,9 +1010,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.deleteBefore()
|
||||
return nil
|
||||
},
|
||||
wantView: "acd⇨",
|
||||
wantContent: "acde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "acd⇨",
|
||||
wantContent: "acde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 6,
|
||||
},
|
||||
{
|
||||
desc: "half-width runes only, deletion on second page refills the field",
|
||||
@ -1001,9 +1033,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦df",
|
||||
wantContent: "abcdf",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦df",
|
||||
wantContent: "abcdf",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 7,
|
||||
},
|
||||
{
|
||||
desc: "half-width runes only, both ends invisible, scrolls to make end visible",
|
||||
@ -1027,9 +1060,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦de",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "half-width runes only, both ends invisible, deletes to make end visible",
|
||||
@ -1052,9 +1086,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦de",
|
||||
wantContent: "abde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦de",
|
||||
wantContent: "abde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 6,
|
||||
},
|
||||
{
|
||||
desc: "full-width runes only, both ends invisible",
|
||||
@ -1071,9 +1106,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦世⇨",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦⇦世⇨",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "full-width runes only, both ends invisible, scrolls to make start visible",
|
||||
@ -1094,9 +1130,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "你好⇨",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantView: "你好⇨",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "full-width runes only, both ends invisible, deletes to make start visible",
|
||||
@ -1117,9 +1154,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.deleteBefore()
|
||||
return nil
|
||||
},
|
||||
wantView: "你世⇨",
|
||||
wantContent: "你世界",
|
||||
wantCurIdx: 2,
|
||||
wantView: "你世⇨",
|
||||
wantContent: "你世界",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "full-width runes only, both ends invisible, scrolls to make end visible",
|
||||
@ -1140,9 +1178,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦界",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦⇦界",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "full-width runes only, both ends invisible, deletes to make end visible",
|
||||
@ -1163,9 +1202,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.delete()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦界",
|
||||
wantContent: "你好界",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦⇦界",
|
||||
wantContent: "你好界",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "scrolls to make full-width rune appear at the beginning",
|
||||
@ -1183,9 +1223,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "你b⇨",
|
||||
wantContent: "你bcd",
|
||||
wantCurIdx: 2,
|
||||
wantView: "你b⇨",
|
||||
wantContent: "你bcd",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "scrolls to make full-width rune appear at the end",
|
||||
@ -1204,9 +1245,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦你",
|
||||
wantContent: "abc你",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦你",
|
||||
wantContent: "abc你",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "inserts after last full width rune, first is half-width",
|
||||
@ -1222,9 +1264,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.insert('e')
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦c你e",
|
||||
wantContent: "abc你e",
|
||||
wantCurIdx: 5,
|
||||
wantView: "⇦c你e",
|
||||
wantContent: "abc你e",
|
||||
wantCurIdx: 5,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "inserts after last full width rune, first is half-width",
|
||||
@ -1239,9 +1282,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.insert('d')
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦你d",
|
||||
wantContent: "世b你d",
|
||||
wantCurIdx: 4,
|
||||
wantView: "⇦你d",
|
||||
wantContent: "世b你d",
|
||||
wantCurIdx: 4,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "inserts after last full width rune, hidden rune is full-width",
|
||||
@ -1256,9 +1300,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.insert('d')
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦cd",
|
||||
wantContent: "世你cd",
|
||||
wantCurIdx: 4,
|
||||
wantView: "⇦⇦cd",
|
||||
wantContent: "世你cd",
|
||||
wantCurIdx: 4,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "scrolls right, first is full-width, last are half-width",
|
||||
@ -1282,9 +1327,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦def⇨",
|
||||
wantContent: "a你世defgh",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦⇦def⇨",
|
||||
wantContent: "a你世defgh",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 8,
|
||||
},
|
||||
{
|
||||
desc: "scrolls right, first is half-width, last is full-width",
|
||||
@ -1308,9 +1354,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦你世⇨",
|
||||
wantContent: "abc你世fgh",
|
||||
wantCurIdx: 3,
|
||||
wantView: "⇦你世⇨",
|
||||
wantContent: "abc你世fgh",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 8,
|
||||
},
|
||||
{
|
||||
desc: "scrolls right, first and last are full-width",
|
||||
@ -1328,9 +1375,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦世⇨",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦⇦世⇨",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "scrolls right, first and last are half-width",
|
||||
@ -1354,9 +1402,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRight()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cdef⇨",
|
||||
wantContent: "abcdefg",
|
||||
wantCurIdx: 4,
|
||||
wantView: "⇦cdef⇨",
|
||||
wantContent: "abcdefg",
|
||||
wantCurIdx: 4,
|
||||
wantOnChangeCalls: 7,
|
||||
},
|
||||
{
|
||||
desc: "scrolls left, first is full-width, last are half-width",
|
||||
@ -1380,9 +1429,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦def⇨",
|
||||
wantContent: "a你世defgh",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦⇦def⇨",
|
||||
wantContent: "a你世defgh",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 8,
|
||||
},
|
||||
{
|
||||
desc: "scrolls left, first is half-width, last is full-width",
|
||||
@ -1406,9 +1456,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦你世⇨",
|
||||
wantContent: "abc你世fgh",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦你世⇨",
|
||||
wantContent: "abc你世fgh",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 8,
|
||||
},
|
||||
{
|
||||
desc: "scrolls left, first and last are full-width",
|
||||
@ -1425,9 +1476,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦世⇨",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦⇦世⇨",
|
||||
wantContent: "你好世界",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 4,
|
||||
},
|
||||
{
|
||||
desc: "scrolls left, first and last are half-width",
|
||||
@ -1450,9 +1502,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorLeft()
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cdef⇨",
|
||||
wantContent: "abcdefg",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦cdef⇨",
|
||||
wantContent: "abcdefg",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 7,
|
||||
},
|
||||
{
|
||||
desc: "resets the field editor",
|
||||
@ -1467,9 +1520,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.reset()
|
||||
return nil
|
||||
},
|
||||
wantView: "",
|
||||
wantContent: "",
|
||||
wantCurIdx: 0,
|
||||
wantView: "",
|
||||
wantContent: "",
|
||||
wantCurIdx: 0,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "doesn't insert runes with rune width of zero",
|
||||
@ -1483,9 +1537,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantView: "ac",
|
||||
wantContent: "ac",
|
||||
wantCurIdx: 2,
|
||||
wantView: "ac",
|
||||
wantContent: "ac",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 2,
|
||||
},
|
||||
{
|
||||
desc: "all text visible, moves cursor to position zero",
|
||||
@ -1500,9 +1555,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(0)
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 0,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 0,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "all text visible, moves cursor to position in the middle",
|
||||
@ -1517,9 +1573,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(1)
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 1,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "all text visible, moves cursor back to the last character",
|
||||
@ -1535,9 +1592,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(2)
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 2,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "all text visible, moves cursor to the appending space",
|
||||
@ -1553,9 +1611,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(3)
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "all text visible, moves cursor before the beginning of data",
|
||||
@ -1571,9 +1630,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(-1)
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 0,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 0,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "all text visible, moves cursor after the appending space",
|
||||
@ -1589,9 +1649,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(10)
|
||||
return nil
|
||||
},
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantView: "abc",
|
||||
wantContent: "abc",
|
||||
wantCurIdx: 3,
|
||||
wantOnChangeCalls: 3,
|
||||
},
|
||||
{
|
||||
desc: "moves cursor when there is no text",
|
||||
@ -1626,9 +1687,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(0)
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "both ends hidden, moves cursor onto the first character",
|
||||
@ -1652,9 +1714,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(1)
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 1,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "both ends hidden, moves cursor onto the right arrow",
|
||||
@ -1677,9 +1740,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(3)
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "both ends hidden, moves cursor onto the last character",
|
||||
@ -1702,9 +1766,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(2)
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦cd⇨",
|
||||
wantContent: "abcde",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "moves cursor onto the first cell containing a full-width rune",
|
||||
@ -1727,9 +1792,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(4)
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦世界⇨",
|
||||
wantContent: "你好世界你",
|
||||
wantCurIdx: 4,
|
||||
wantView: "⇦⇦世界⇨",
|
||||
wantContent: "你好世界你",
|
||||
wantCurIdx: 4,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "moves cursor onto the second cell containing a full-width rune",
|
||||
@ -1752,9 +1818,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(5)
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦世界⇨",
|
||||
wantContent: "你好世界你",
|
||||
wantCurIdx: 4,
|
||||
wantView: "⇦⇦世界⇨",
|
||||
wantContent: "你好世界你",
|
||||
wantCurIdx: 4,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
{
|
||||
desc: "moves cursor onto the second right arrow",
|
||||
@ -1777,15 +1844,20 @@ func TestFieldEditor(t *testing.T) {
|
||||
fe.cursorRelCell(1)
|
||||
return nil
|
||||
},
|
||||
wantView: "⇦⇦世界⇨",
|
||||
wantContent: "你好世界你",
|
||||
wantCurIdx: 2,
|
||||
wantView: "⇦⇦世界⇨",
|
||||
wantContent: "你好世界你",
|
||||
wantCurIdx: 2,
|
||||
wantOnChangeCalls: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
fe := newFieldEditor()
|
||||
var changeCount int
|
||||
fe := newFieldEditor(func(data string) {
|
||||
changeCount++
|
||||
})
|
||||
|
||||
if tc.ops != nil {
|
||||
if err := tc.ops(fe); err != nil {
|
||||
t.Fatalf("ops => unexpected error: %v", err)
|
||||
@ -1808,6 +1880,10 @@ func TestFieldEditor(t *testing.T) {
|
||||
if gotContent != tc.wantContent {
|
||||
t.Errorf("content -> %q, want %q", gotContent, tc.wantContent)
|
||||
}
|
||||
|
||||
if tc.wantOnChangeCalls != changeCount {
|
||||
t.Errorf("unexpected number of onChange calls -> %d, want %d", changeCount, tc.wantOnChangeCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -167,6 +167,7 @@ func newForm(cancel context.CancelFunc) (*form, error) {
|
||||
func formLayout(c *container.Container, f *form) error {
|
||||
return c.Update("root",
|
||||
container.KeyFocusNext(keyboard.KeyTab),
|
||||
container.KeyFocusPrevious(keyboard.KeyBacktab),
|
||||
container.KeyFocusGroupsNext(keyboard.KeyArrowDown, 1),
|
||||
container.KeyFocusGroupsPrevious(keyboard.KeyArrowUp, 1),
|
||||
container.KeyFocusGroupsNext(keyboard.KeyArrowRight, 2),
|
||||
|
@ -63,6 +63,7 @@ type options struct {
|
||||
|
||||
filter FilterFn
|
||||
onSubmit SubmitFn
|
||||
onChange ChangeFn
|
||||
clearOnSubmit bool
|
||||
exclusiveKeyboardOnFocus bool
|
||||
}
|
||||
@ -269,6 +270,21 @@ func OnSubmit(fn SubmitFn) Option {
|
||||
})
|
||||
}
|
||||
|
||||
// ChangeFn when passed to OnChage will be called with all the text in the text
|
||||
// input each time it gets modified.
|
||||
//
|
||||
// This function must be thread-safe as the keyboard event that
|
||||
// triggers the submission comes from a separate goroutine.
|
||||
type ChangeFn func(data string)
|
||||
|
||||
// OnChange sets a function that will be called when the content of the text input
|
||||
// field changes.
|
||||
func OnChange(fn ChangeFn) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.onChange = fn
|
||||
})
|
||||
}
|
||||
|
||||
// ClearOnSubmit sets the text input to be cleared when a submit of the content
|
||||
// is triggered by the user pressing the Enter key.
|
||||
func ClearOnSubmit() Option {
|
||||
|
@ -70,10 +70,9 @@ func New(opts ...Option) (*TextInput, error) {
|
||||
return nil, err
|
||||
}
|
||||
ti := &TextInput{
|
||||
editor: newFieldEditor(),
|
||||
editor: newFieldEditor(opt.onChange),
|
||||
opts: opt,
|
||||
}
|
||||
|
||||
for _, r := range ti.opts.defaultText {
|
||||
ti.editor.insert(r)
|
||||
}
|
||||
|
@ -29,13 +29,16 @@ import (
|
||||
"github.com/mum4k/termdash/terminal/tcell"
|
||||
"github.com/mum4k/termdash/widgets/button"
|
||||
"github.com/mum4k/termdash/widgets/segmentdisplay"
|
||||
"github.com/mum4k/termdash/widgets/text"
|
||||
"github.com/mum4k/termdash/widgets/textinput"
|
||||
)
|
||||
|
||||
// rotate returns a new slice with inputs rotated by step.
|
||||
// I.e. for a step of one:
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
//
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
//
|
||||
// And so on.
|
||||
func rotate(inputs []rune, step int) []rune {
|
||||
return append(inputs[step:], inputs[:step]...)
|
||||
@ -126,11 +129,20 @@ func main() {
|
||||
updateText := make(chan string)
|
||||
go rollText(ctx, rollingSD, updateText)
|
||||
|
||||
mirror, err := text.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
input, err := textinput.New(
|
||||
textinput.Label("New text:", cell.FgColor(cell.ColorNumber(33))),
|
||||
textinput.MaxWidthCells(20),
|
||||
textinput.Border(linestyle.Light),
|
||||
textinput.PlaceHolder("Enter any text"),
|
||||
textinput.OnChange(func(data string) {
|
||||
mirror.Reset()
|
||||
mirror.Write(data)
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -177,18 +189,30 @@ func main() {
|
||||
),
|
||||
)
|
||||
builder.Add(
|
||||
grid.RowHeightPerc(20,
|
||||
grid.Widget(
|
||||
input,
|
||||
container.AlignHorizontal(align.HorizontalCenter),
|
||||
container.AlignVertical(align.VerticalBottom),
|
||||
container.MarginBottom(1),
|
||||
grid.RowHeightPerc(40,
|
||||
grid.RowHeightPerc(50,
|
||||
grid.Widget(
|
||||
input,
|
||||
container.AlignHorizontal(align.HorizontalCenter),
|
||||
container.AlignVertical(align.VerticalBottom),
|
||||
container.MarginBottom(1),
|
||||
),
|
||||
),
|
||||
grid.RowHeightPerc(50,
|
||||
grid.Widget(
|
||||
mirror,
|
||||
container.Border(linestyle.Light),
|
||||
container.BorderTitle("Text"),
|
||||
container.AlignHorizontal(align.HorizontalCenter),
|
||||
container.AlignHorizontal(align.Horizontal(align.VerticalBottom)),
|
||||
container.MarginBottom(1),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
builder.Add(
|
||||
grid.RowHeightPerc(40,
|
||||
grid.RowHeightPerc(20,
|
||||
grid.ColWidthPerc(20),
|
||||
grid.ColWidthPerc(20,
|
||||
grid.Widget(
|
||||
|
Loading…
x
Reference in New Issue
Block a user