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')
1 files changed, 0 insertions, 471 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
deleted file mode 100644
index 54cef7856..000000000
--- a/examples/go-dashboard/src/github.com/mum4k/termdash/container/container.go
+++ /dev/null
@@ -1,471 +0,0 @@
-// 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,
- }
-}