mirror of https://github.com/mum4k/termdash.git
Container now supports dynamic layout changes.
This commit is contained in:
parent
85bcf9d8d9
commit
782d7c3117
|
@ -59,6 +59,12 @@ type Container struct {
|
|||
// opts are the options provided to the container.
|
||||
opts *options
|
||||
|
||||
// clearNeeded indicates if the terminal needs to be cleared next time we
|
||||
// are clearNeeded the container.
|
||||
// This is required if the container was updated and thus the layout might
|
||||
// have changed.
|
||||
clearNeeded bool
|
||||
|
||||
// mu protects the container tree.
|
||||
// All containers in the tree share the same lock.
|
||||
mu *sync.Mutex
|
||||
|
@ -195,6 +201,13 @@ func (c *Container) Draw() error {
|
|||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.clearNeeded {
|
||||
if err := c.term.Clear(); err != nil {
|
||||
return fmt.Errorf("term.Clear => error: %v", err)
|
||||
}
|
||||
c.clearNeeded = false
|
||||
}
|
||||
|
||||
// Update the area we are tracking for focus in case the terminal size
|
||||
// changed.
|
||||
ar, err := area.FromSize(c.term.Size())
|
||||
|
@ -205,6 +218,37 @@ func (c *Container) Draw() error {
|
|||
return drawTree(c)
|
||||
}
|
||||
|
||||
// Update updates container with the specified id by setting the provided
|
||||
// options. This can be used to perform dynamic layout changes, i.e. anything
|
||||
// between replacing the widget in the container and completely changing the
|
||||
// layout and splits.
|
||||
// The argument id must match exactly one container with that was created with
|
||||
// matching ID() option. The argument id must not be an empty string.
|
||||
func (c *Container) Update(id string, opts ...Option) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
target, err := findID(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.clearNeeded = true
|
||||
|
||||
if err := applyOptions(target, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateOptions(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The currently focused container might not be reachable anymore, because
|
||||
// it was under the target. If that is so, move the focus up to the target.
|
||||
if !c.focusTracker.reachableFrom(c) {
|
||||
c.focusTracker.setActive(target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateFocus processes the mouse event and determines if it changes the
|
||||
// focused container.
|
||||
func (c *Container) updateFocus(m *terminalapi.Mouse) {
|
||||
|
|
|
@ -91,6 +91,70 @@ func TestNew(t *testing.T) {
|
|||
},
|
||||
wantContainerErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on invalid option on the first vertical child container",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(
|
||||
MarginTop(-1),
|
||||
),
|
||||
Right(),
|
||||
),
|
||||
)
|
||||
},
|
||||
wantContainerErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on invalid option on the second vertical child container",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(
|
||||
MarginTop(-1),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
wantContainerErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on invalid option on the first horizontal child container",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitHorizontal(
|
||||
Top(
|
||||
MarginTop(-1),
|
||||
),
|
||||
Bottom(),
|
||||
),
|
||||
)
|
||||
},
|
||||
wantContainerErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on invalid option on the second horizontal child container",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitHorizontal(
|
||||
Top(),
|
||||
Bottom(
|
||||
MarginTop(-1),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
wantContainerErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on MarginTopPercent too low",
|
||||
termSize: image.Point{10, 10},
|
||||
|
@ -865,6 +929,8 @@ func TestNew(t *testing.T) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
contStr := cont.String()
|
||||
t.Logf("For container: %v", contStr)
|
||||
if err := cont.Draw(); err != nil {
|
||||
t.Fatalf("Draw => unexpected error: %v", err)
|
||||
}
|
||||
|
@ -1848,3 +1914,395 @@ func TestMouse(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
termSize image.Point
|
||||
container func(ft *faketerm.Terminal) (*Container, error)
|
||||
updateID string
|
||||
updateOpts []Option
|
||||
// eventGroups are events delivered before the update.
|
||||
eventGroups []*eventGroup
|
||||
wantUpdateErr bool
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
}{
|
||||
{
|
||||
desc: "fails on empty updateID",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(ft)
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails when no container with the ID is found",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(ft)
|
||||
},
|
||||
updateID: "myID",
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "no changes when no options are provided",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myID"),
|
||||
Border(linestyle.Light),
|
||||
)
|
||||
},
|
||||
updateID: "myID",
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustBorder(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 10),
|
||||
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "fails on invalid options",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myID"),
|
||||
Border(linestyle.Light),
|
||||
)
|
||||
},
|
||||
updateID: "myID",
|
||||
updateOpts: []Option{
|
||||
MarginTop(-1),
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails when update introduces a duplicate ID",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myID"),
|
||||
Border(linestyle.Light),
|
||||
)
|
||||
},
|
||||
updateID: "myID",
|
||||
updateOpts: []Option{
|
||||
SplitVertical(
|
||||
Left(
|
||||
ID("left"),
|
||||
),
|
||||
Right(
|
||||
ID("myID"),
|
||||
),
|
||||
),
|
||||
},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
{
|
||||
desc: "removes border from the container",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myID"),
|
||||
Border(linestyle.Light),
|
||||
)
|
||||
},
|
||||
updateID: "myID",
|
||||
updateOpts: []Option{
|
||||
Border(linestyle.None),
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "places widget into a sub-container container",
|
||||
termSize: image.Point{20, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myRoot"),
|
||||
SplitVertical(
|
||||
Left(
|
||||
ID("left"),
|
||||
),
|
||||
Right(
|
||||
ID("right"),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
updateID: "right",
|
||||
updateOpts: []Option{
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{})),
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
wAr := image.Rect(10, 0, 20, 10)
|
||||
wCvs := testcanvas.MustNew(wAr)
|
||||
fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
|
||||
testcanvas.MustCopyTo(wCvs, cvs)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "places widget into root which removes children",
|
||||
termSize: image.Point{20, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myRoot"),
|
||||
SplitVertical(
|
||||
Left(
|
||||
ID("left"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Right(
|
||||
ID("right"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
updateID: "myRoot",
|
||||
updateOpts: []Option{
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{})),
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "changes container splits",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myRoot"),
|
||||
SplitVertical(
|
||||
Left(
|
||||
ID("left"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Right(
|
||||
ID("right"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
updateID: "myRoot",
|
||||
updateOpts: []Option{
|
||||
SplitHorizontal(
|
||||
Top(
|
||||
ID("left"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Bottom(
|
||||
ID("right"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
),
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 5))
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 5, 10, 10))
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "update retains original focused container if it still exists",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myRoot"),
|
||||
SplitVertical(
|
||||
Left(
|
||||
ID("left"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Right(
|
||||
ID("right"),
|
||||
Border(linestyle.Light),
|
||||
SplitHorizontal(
|
||||
Top(
|
||||
ID("rightTop"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Bottom(
|
||||
ID("rightBottom"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
eventGroups: []*eventGroup{
|
||||
// Move focus to container with ID "right".
|
||||
// It will continue to exist after the update.
|
||||
{
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Mouse{Position: image.Point{5, 0}, Button: mouse.ButtonLeft},
|
||||
&terminalapi.Mouse{Position: image.Point{5, 0}, Button: mouse.ButtonRelease},
|
||||
},
|
||||
wantProcessed: 2,
|
||||
},
|
||||
},
|
||||
updateID: "right",
|
||||
updateOpts: []Option{
|
||||
Clear(),
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 0, 5, 10))
|
||||
testdraw.MustBorder(cvs, image.Rect(5, 0, 10, 10), draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)))
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "update moves focus to the nearest parent when focused container is destroyed",
|
||||
termSize: image.Point{10, 10},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
ID("myRoot"),
|
||||
SplitVertical(
|
||||
Left(
|
||||
ID("left"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Right(
|
||||
ID("right"),
|
||||
Border(linestyle.Light),
|
||||
SplitHorizontal(
|
||||
Top(
|
||||
ID("rightTop"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
Bottom(
|
||||
ID("rightBottom"),
|
||||
Border(linestyle.Light),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
eventGroups: []*eventGroup{
|
||||
// Move focus to container with ID "rightTop".
|
||||
// It will be destroyed by calling update.
|
||||
{
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Mouse{Position: image.Point{6, 1}, Button: mouse.ButtonLeft},
|
||||
&terminalapi.Mouse{Position: image.Point{6, 1}, Button: mouse.ButtonRelease},
|
||||
},
|
||||
wantProcessed: 2,
|
||||
},
|
||||
},
|
||||
updateID: "right",
|
||||
updateOpts: []Option{
|
||||
Clear(),
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustBorder(cvs, image.Rect(0, 0, 5, 10))
|
||||
testdraw.MustBorder(cvs, image.Rect(5, 0, 10, 10), draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)))
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := faketerm.New(tc.termSize)
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
cont, err := tc.container(got)
|
||||
if err != nil {
|
||||
t.Fatalf("tc.container => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
eds := event.NewDistributionSystem()
|
||||
eh := &errorHandler{}
|
||||
// Subscribe to receive errors.
|
||||
eds.Subscribe([]terminalapi.Event{terminalapi.NewError("")}, func(ev terminalapi.Event) {
|
||||
eh.handle(ev.(*terminalapi.Error).Error())
|
||||
})
|
||||
cont.Subscribe(eds)
|
||||
// Initial draw to determine sizes of containers.
|
||||
if err := cont.Draw(); err != nil {
|
||||
t.Fatalf("Draw => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Deliver the events.
|
||||
for _, eg := range tc.eventGroups {
|
||||
for _, ev := range eg.events {
|
||||
eds.Event(ev)
|
||||
}
|
||||
if err := testevent.WaitFor(5*time.Second, func() error {
|
||||
if got, want := eds.Processed(), eg.wantProcessed; got != want {
|
||||
return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("testevent.WaitFor => %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err := cont.Update(tc.updateID, tc.updateOpts...)
|
||||
if (err != nil) != tc.wantUpdateErr {
|
||||
t.Errorf("Update => unexpected error:%v, wantErr:%v", err, tc.wantUpdateErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := cont.Draw(); err != nil {
|
||||
t.Fatalf("Draw => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var want *faketerm.Terminal
|
||||
if tc.want != nil {
|
||||
want = tc.want(tc.termSize)
|
||||
} else {
|
||||
w, err := faketerm.New(tc.termSize)
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
want = w
|
||||
}
|
||||
if diff := faketerm.Diff(want, got); diff != "" {
|
||||
t.Errorf("Draw => %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -73,9 +73,9 @@ func (ft *focusTracker) isActive(c *Container) bool {
|
|||
return ft.container == c
|
||||
}
|
||||
|
||||
// active returns the currently focused container.
|
||||
func (ft *focusTracker) active() *Container {
|
||||
return ft.container
|
||||
// setActive sets the currently active container to the one provided.
|
||||
func (ft *focusTracker) setActive(c *Container) {
|
||||
ft.container = c
|
||||
}
|
||||
|
||||
// mouse identifies mouse events that change the focused container and track
|
||||
|
@ -98,3 +98,19 @@ func (ft *focusTracker) mouse(target *Container, m *terminalapi.Mouse) {
|
|||
func (ft *focusTracker) updateArea(ar image.Rectangle) {
|
||||
ft.buttonFSM.UpdateArea(ar)
|
||||
}
|
||||
|
||||
// reachableFrom asserts whether the currently focused container is reachable
|
||||
// from the provided node in the tree.
|
||||
func (ft *focusTracker) reachableFrom(node *Container) bool {
|
||||
var (
|
||||
errStr string
|
||||
reachable bool
|
||||
)
|
||||
preOrder(node, &errStr, visitFunc(func(c *Container) error {
|
||||
if c == ft.container {
|
||||
reachable = true
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
return reachable
|
||||
}
|
||||
|
|
|
@ -271,6 +271,18 @@ func ID(id string) Option {
|
|||
})
|
||||
}
|
||||
|
||||
// Clear clears this container.
|
||||
// If the container contains a widget, the widget is removed.
|
||||
// If the container had any sub containers or splits, they are removed.
|
||||
func Clear() Option {
|
||||
return option(func(c *Container) error {
|
||||
c.opts.widget = nil
|
||||
c.first = nil
|
||||
c.second = nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// PlaceWidget places the provided widget into the container.
|
||||
// The use of this option removes any sub containers. Containers with sub
|
||||
// containers cannot have widgets.
|
||||
|
|
Loading…
Reference in New Issue