summaryrefslogtreecommitdiff
path: root/examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go
diff options
context:
space:
mode:
Diffstat (limited to 'examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go')
-rw-r--r--examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go471
1 files changed, 471 insertions, 0 deletions
diff --git a/examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go b/examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go
new file mode 100644
index 000000000..54cef7856
--- /dev/null
+++ b/examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go
@@ -0,0 +1,471 @@
+// Copyright 2018 Google Inc.
+//
+// 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 container defines a type that wraps other containers or widgets.
+
+The container supports splitting container into sub containers, defining
+container styles and placing widgets. The container also creates and manages
+canvases assigned to the placed widgets.
+*/
+package container
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "sync"
+
+ "github.com/mum4k/termdash/linestyle"
+ "github.com/mum4k/termdash/private/alignfor"
+ "github.com/mum4k/termdash/private/area"
+ "github.com/mum4k/termdash/private/event"
+ "github.com/mum4k/termdash/terminal/terminalapi"
+ "github.com/mum4k/termdash/widgetapi"
+)
+
+// Container wraps either sub containers or widgets and positions them on the
+// terminal.
+// This is thread-safe.
+type Container struct {
+ // parent is the parent container, nil if this is the root container.
+ parent *Container
+ // The sub containers, if these aren't nil, the widget must be.
+ first *Container
+ second *Container
+
+ // term is the terminal this container is placed on.
+ // All containers in the tree share the same terminal.
+ term terminalapi.Terminal
+
+ // focusTracker tracks the active (focused) container.
+ // All containers in the tree share the same tracker.
+ focusTracker *focusTracker
+
+ // area is the area of the terminal this container has access to.
+ // Initialized the first time Draw is called.
+ area image.Rectangle
+
+ // 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
+}
+
+// String represents the container metadata in a human readable format.
+// Implements fmt.Stringer.
+func (c *Container) String() string {
+ return fmt.Sprintf("Container@%p{parent:%p, first:%p, second:%p, area:%+v}", c, c.parent, c.first, c.second, c.area)
+}
+
+// New returns a new root container that will use the provided terminal and
+// applies the provided options.
+func New(t terminalapi.Terminal, opts ...Option) (*Container, error) {
+ root := &Container{
+ term: t,
+ opts: newOptions( /* parent = */ nil),
+ mu: &sync.Mutex{},
+ }
+
+ // Initially the root is focused.
+ root.focusTracker = newFocusTracker(root)
+ if err := applyOptions(root, opts...); err != nil {
+ return nil, err
+ }
+ if err := validateOptions(root); err != nil {
+ return nil, err
+ }
+ return root, nil
+}
+
+// newChild creates a new child container of the given parent.
+func newChild(parent *Container, opts []Option) (*Container, error) {
+ child := &Container{
+ parent: parent,
+ term: parent.term,
+ focusTracker: parent.focusTracker,
+ opts: newOptions(parent.opts),
+ mu: parent.mu,
+ }
+ if err := applyOptions(child, opts...); err != nil {
+ return nil, err
+ }
+ return child, nil
+}
+
+// hasBorder determines if this container has a border.
+func (c *Container) hasBorder() bool {
+ return c.opts.border != linestyle.None
+}
+
+// hasWidget determines if this container has a widget.
+func (c *Container) hasWidget() bool {
+ return c.opts.widget != nil
+}
+
+// usable returns the usable area in this container.
+// This depends on whether the container has a border, etc.
+func (c *Container) usable() image.Rectangle {
+ if c.hasBorder() {
+ return area.ExcludeBorder(c.area)
+ }
+ return c.area
+}
+
+// widgetArea returns the area in the container that is available for the
+// widget's canvas. Takes the container border, widget's requested maximum size
+// and ratio and container's alignment into account.
+// Returns a zero area if the container has no widget.
+func (c *Container) widgetArea() (image.Rectangle, error) {
+ if !c.hasWidget() {
+ return image.ZR, nil
+ }
+
+ padded, err := c.opts.padding.apply(c.usable())
+ if err != nil {
+ return image.ZR, err
+ }
+ wOpts := c.opts.widget.Options()
+
+ adjusted := padded
+ if maxX := wOpts.MaximumSize.X; maxX > 0 && adjusted.Dx() > maxX {
+ adjusted.Max.X -= adjusted.Dx() - maxX
+ }
+ if maxY := wOpts.MaximumSize.Y; maxY > 0 && adjusted.Dy() > maxY {
+ adjusted.Max.Y -= adjusted.Dy() - maxY
+ }
+
+ if wOpts.Ratio.X > 0 && wOpts.Ratio.Y > 0 {
+ adjusted = area.WithRatio(adjusted, wOpts.Ratio)
+ }
+ aligned, err := alignfor.Rectangle(padded, adjusted, c.opts.hAlign, c.opts.vAlign)
+ if err != nil {
+ return image.ZR, err
+ }
+ return aligned, nil
+}
+
+// split splits the container's usable area into child areas.
+// Panics if the container isn't configured for a split.
+func (c *Container) split() (image.Rectangle, image.Rectangle, error) {
+ ar, err := c.opts.padding.apply(c.usable())
+ if err != nil {
+ return image.ZR, image.ZR, err
+ }
+ if c.opts.splitFixed > DefaultSplitFixed {
+ if c.opts.split == splitTypeVertical {
+ return area.VSplitCells(ar, c.opts.splitFixed)
+ }
+ return area.HSplitCells(ar, c.opts.splitFixed)
+ }
+
+ if c.opts.split == splitTypeVertical {
+ return area.VSplit(ar, c.opts.splitPercent)
+ }
+ return area.HSplit(ar, c.opts.splitPercent)
+}
+
+// createFirst creates and returns the first sub container of this container.
+func (c *Container) createFirst(opts []Option) error {
+ first, err := newChild(c, opts)
+ if err != nil {
+ return err
+ }
+ c.first = first
+ return nil
+}
+
+// createSecond creates and returns the second sub container of this container.
+func (c *Container) createSecond(opts []Option) error {
+ second, err := newChild(c, opts)
+ if err != nil {
+ return err
+ }
+ c.second = second
+ return nil
+}
+
+// Draw draws this container and all of its sub containers.
+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())
+ if err != nil {
+ return err
+ }
+ c.focusTracker.updateArea(ar)
+ 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.
+// Caller must hold c.mu.
+func (c *Container) updateFocus(m *terminalapi.Mouse) {
+ target := pointCont(c, m.Position)
+ if target == nil { // Ignore mouse clicks where no containers are.
+ return
+ }
+ c.focusTracker.mouse(target, m)
+}
+
+// processEvent processes events delivered to the container.
+func (c *Container) processEvent(ev terminalapi.Event) error {
+ // This is done in two stages.
+ // 1) under lock we traverse the container and identify all targets
+ // (widgets) that should receive the event.
+ // 2) lock is released and events are delivered to the widgets. Widgets
+ // themselves are thread-safe. Lock must be releases when delivering,
+ // because some widgets might try to mutate the container when they
+ // receive the event, like dynamically change the layout.
+ c.mu.Lock()
+ sendFn, err := c.prepareEvTargets(ev)
+ c.mu.Unlock()
+ if err != nil {
+ return err
+ }
+ return sendFn()
+}
+
+// prepareEvTargets returns a closure, that when called delivers the event to
+// widgets that registered for it.
+// Also processes the event on behalf of the container (tracks keyboard focus).
+// Caller must hold c.mu.
+func (c *Container) prepareEvTargets(ev terminalapi.Event) (func() error, error) {
+ switch e := ev.(type) {
+ case *terminalapi.Mouse:
+ c.updateFocus(ev.(*terminalapi.Mouse))
+
+ targets, err := c.mouseEvTargets(e)
+ if err != nil {
+ return nil, err
+ }
+ return func() error {
+ for _, mt := range targets {
+ if err := mt.widget.Mouse(mt.ev); err != nil {
+ return err
+ }
+ }
+ return nil
+ }, nil
+
+ case *terminalapi.Keyboard:
+ targets := c.keyEvTargets()
+ return func() error {
+ for _, w := range targets {
+ if err := w.Keyboard(e); err != nil {
+ return err
+ }
+ }
+ return nil
+ }, nil
+
+ default:
+ return nil, fmt.Errorf("container received an unsupported event type %T", ev)
+ }
+}
+
+// keyEvTargets returns those widgets found in the container that should
+// receive this keyboard event.
+// Caller must hold c.mu.
+func (c *Container) keyEvTargets() []widgetapi.Widget {
+ var (
+ errStr string
+ widgets []widgetapi.Widget
+ )
+
+ // All the widgets that should receive this event.
+ // For now stable ordering (preOrder).
+ preOrder(c, &errStr, visitFunc(func(cur *Container) error {
+ if !cur.hasWidget() {
+ return nil
+ }
+
+ wOpt := cur.opts.widget.Options()
+ switch wOpt.WantKeyboard {
+ case widgetapi.KeyScopeNone:
+ // Widget doesn't want any keyboard events.
+ return nil
+
+ case widgetapi.KeyScopeFocused:
+ if cur.focusTracker.isActive(cur) {
+ widgets = append(widgets, cur.opts.widget)
+ }
+
+ case widgetapi.KeyScopeGlobal:
+ widgets = append(widgets, cur.opts.widget)
+ }
+ return nil
+ }))
+ return widgets
+}
+
+// mouseEvTarget contains a mouse event adjusted relative to the widget's area
+// and the widget that should receive it.
+type mouseEvTarget struct {
+ // widget is the widget that should receive the mouse event.
+ widget widgetapi.Widget
+ // ev is the adjusted mouse event.
+ ev *terminalapi.Mouse
+}
+
+// newMouseEvTarget returns a new newMouseEvTarget.
+func newMouseEvTarget(w widgetapi.Widget, wArea image.Rectangle, ev *terminalapi.Mouse) *mouseEvTarget {
+ return &mouseEvTarget{
+ widget: w,
+ ev: adjustMouseEv(ev, wArea),
+ }
+}
+
+// mouseEvTargets returns those widgets found in the container that should
+// receive this mouse event.
+// Caller must hold c.mu.
+func (c *Container) mouseEvTargets(m *terminalapi.Mouse) ([]*mouseEvTarget, error) {
+ var (
+ errStr string
+ widgets []*mouseEvTarget
+ )
+
+ // All the widgets that should receive this event.
+ // For now stable ordering (preOrder).
+ preOrder(c, &errStr, visitFunc(func(cur *Container) error {
+ if !cur.hasWidget() {
+ return nil
+ }
+
+ wOpts := cur.opts.widget.Options()
+ wa, err := cur.widgetArea()
+ if err != nil {
+ return err
+ }
+
+ switch wOpts.WantMouse {
+ case widgetapi.MouseScopeNone:
+ // Widget doesn't want any mouse events.
+ return nil
+
+ case widgetapi.MouseScopeWidget:
+ // Only if the event falls inside of the widget's canvas.
+ if m.Position.In(wa) {
+ widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
+ }
+
+ case widgetapi.MouseScopeContainer:
+ // Only if the event falls inside the widget's parent container.
+ if m.Position.In(cur.area) {
+ widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
+ }
+
+ case widgetapi.MouseScopeGlobal:
+ // Widget wants all mouse events.
+ widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
+ }
+ return nil
+ }))
+
+ if errStr != "" {
+ return nil, errors.New(errStr)
+ }
+ return widgets, nil
+}
+
+// Subscribe tells the container to subscribe itself and widgets to the
+// provided event distribution system.
+// This method is private to termdash, stability isn't guaranteed and changes
+// won't be backward compatible.
+func (c *Container) Subscribe(eds *event.DistributionSystem) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ // maxReps is the maximum number of repetitive events towards widgets
+ // before we throttle them.
+ const maxReps = 10
+
+ // Subscriber the container itself in order to track keyboard focus.
+ want := []terminalapi.Event{
+ &terminalapi.Keyboard{},
+ &terminalapi.Mouse{},
+ }
+ eds.Subscribe(want, func(ev terminalapi.Event) {
+ if err := c.processEvent(ev); err != nil {
+ eds.Event(terminalapi.NewErrorf("failed to process event %v: %v", ev, err))
+ }
+ }, event.MaxRepetitive(maxReps))
+}
+
+// adjustMouseEv adjusts the mouse event relative to the widget area.
+func adjustMouseEv(m *terminalapi.Mouse, wArea image.Rectangle) *terminalapi.Mouse {
+ // The sent mouse coordinate is relative to the widget canvas, i.e. zero
+ // based, even though the widget might not be in the top left corner on the
+ // terminal.
+ offset := wArea.Min
+ if m.Position.In(wArea) {
+ return &terminalapi.Mouse{
+ Position: m.Position.Sub(offset),
+ Button: m.Button,
+ }
+ }
+ return &terminalapi.Mouse{
+ Position: image.Point{-1, -1},
+ Button: m.Button,
+ }
+}