1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-27 13:48:49 +08:00
termdash/container/container.go

304 lines
9.0 KiB
Go
Raw Normal View History

// 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.
2018-03-27 19:01:35 +01:00
/*
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
2018-03-27 19:01:35 +01:00
canvases assigned to the placed widgets.
*/
package container
import (
"fmt"
"image"
"sync"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/event"
"github.com/mum4k/termdash/internal/align"
"github.com/mum4k/termdash/internal/area"
2018-03-27 19:01:35 +01:00
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgetapi"
2018-03-27 19:01:35 +01:00
)
// Container wraps either sub containers or widgets and positions them on the
// terminal.
2019-02-20 23:36:58 -05:00
// This is thread-safe.
2018-03-27 19:01:35 +01:00
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.
2018-03-27 19:01:35 +01:00
term terminalapi.Terminal
2018-04-06 04:56:36 +02:00
// 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.
area image.Rectangle
2018-03-27 19:01:35 +01:00
// opts are the options provided to the container.
opts *options
// mu protects the container tree.
// All containers in the tree share the same lock.
mu *sync.Mutex
2018-03-27 19:01:35 +01:00
}
// 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)
}
2018-03-27 19:01:35 +01:00
// 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) {
size := t.Size()
root := &Container{
term: t,
// The root container has access to the entire terminal.
area: image.Rect(0, 0, size.X, size.Y),
2018-04-06 04:56:36 +02:00
opts: newOptions( /* parent = */ nil),
mu: &sync.Mutex{},
2018-03-27 19:01:35 +01:00
}
2018-04-06 04:56:36 +02:00
// Initially the root is focused.
root.focusTracker = newFocusTracker(root)
if err := applyOptions(root, opts...); err != nil {
return nil, err
}
return root, nil
2018-03-27 19:01:35 +01:00
}
// newChild creates a new child container of the given parent.
func newChild(parent *Container, area image.Rectangle) *Container {
return &Container{
2018-04-06 04:56:36 +02:00
parent: parent,
term: parent.term,
focusTracker: parent.focusTracker,
area: area,
opts: newOptions(parent.opts),
mu: parent.mu,
2018-03-27 19:01:35 +01:00
}
}
// hasBorder determines if this container has a border.
func (c *Container) hasBorder() bool {
return c.opts.border != draw.LineStyleNone
}
// 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
}
adjusted := c.usable()
wOpts := c.opts.widget.Options()
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)
}
adjusted, err := align.Rectangle(c.usable(), adjusted, c.opts.hAlign, c.opts.vAlign)
if err != nil {
return image.ZR, err
}
return adjusted, 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 := c.usable()
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() (*Container, error) {
ar, _, err := c.split()
if err != nil {
return nil, err
}
c.first = newChild(c, ar)
return c.first, nil
2018-03-27 19:01:35 +01:00
}
// createSecond creates and returns the second sub container of this container.
func (c *Container) createSecond() (*Container, error) {
_, ar, err := c.split()
if err != nil {
return nil, err
}
c.second = newChild(c, ar)
return c.second, nil
2018-03-27 19:01:35 +01:00
}
// Draw draws this container and all of its sub containers.
func (c *Container) Draw() error {
c.mu.Lock()
defer c.mu.Unlock()
2018-04-06 04:56:36 +02:00
return drawTree(c)
}
// updateFocus processes the mouse event and determines if it changes the
// focused container.
func (c *Container) updateFocus(m *terminalapi.Mouse) {
c.mu.Lock()
defer c.mu.Unlock()
target := pointCont(c, m.Position)
if target == nil { // Ignore mouse clicks where no containers are.
return
}
c.focusTracker.mouse(target, m)
}
// keyboardToWidget forwards the keyboard event to the widget unconditionally.
func (c *Container) keyboardToWidget(k *terminalapi.Keyboard, scope widgetapi.KeyScope) error {
c.mu.Lock()
defer c.mu.Unlock()
if scope == widgetapi.KeyScopeFocused && !c.focusTracker.isActive(c) {
return nil
}
return c.opts.widget.Keyboard(k)
}
// mouseToWidget forwards the mouse event to the widget.
func (c *Container) mouseToWidget(m *terminalapi.Mouse, scope widgetapi.MouseScope) error {
c.mu.Lock()
defer c.mu.Unlock()
target := pointCont(c, m.Position)
if target == nil { // Ignore mouse clicks where no containers are.
return nil
}
// Ignore clicks falling outside of the container.
if scope != widgetapi.MouseScopeGlobal && !m.Position.In(c.area) {
return nil
}
// Ignore clicks falling outside of the widget's canvas.
wa, err := c.widgetArea()
if err != nil {
return err
}
if scope == widgetapi.MouseScopeWidget && !m.Position.In(wa) {
return nil
}
// 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 := wa.Min
var wm *terminalapi.Mouse
if m.Position.In(wa) {
wm = &terminalapi.Mouse{
Position: m.Position.Sub(offset),
Button: m.Button,
}
} else {
wm = &terminalapi.Mouse{
Position: image.Point{-1, -1},
Button: m.Button,
}
}
return c.opts.widget.Mouse(wm)
}
// Subscribe tells the container to subscribe itself and widgets to the
// provided event distribution system.
func (c *Container) Subscribe(eds *event.DistributionSystem) {
2019-02-20 23:36:58 -05:00
c.mu.Lock()
defer c.mu.Unlock()
// maxReps is the maximum number of repetitive events towards widgets
// before we throttle them.
const maxReps = 10
root := rootCont(c)
// Subscriber the container itself in order to track keyboard focus.
eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
root.updateFocus(ev.(*terminalapi.Mouse))
}, event.MaxRepetitive(0)) // One event is enough to change the focus.
// Subscribe any widgets that specify Keyboard or Mouse in their options.
var errStr string
preOrder(root, &errStr, visitFunc(func(c *Container) error {
if c.hasWidget() {
wOpt := c.opts.widget.Options()
switch scope := wOpt.WantKeyboard; scope {
case widgetapi.KeyScopeNone:
// Widget doesn't want any keyboard events.
default:
eds.Subscribe([]terminalapi.Event{&terminalapi.Keyboard{}}, func(ev terminalapi.Event) {
if err := c.keyboardToWidget(ev.(*terminalapi.Keyboard), scope); err != nil {
eds.Event(terminalapi.NewErrorf("failed to send global keyboard event %v to widget %T: %v", ev, c.opts.widget, err))
}
}, event.MaxRepetitive(maxReps))
}
switch scope := wOpt.WantMouse; scope {
case widgetapi.MouseScopeNone:
// Widget doesn't want any mouse events.
default:
eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
if err := c.mouseToWidget(ev.(*terminalapi.Mouse), scope); err != nil {
eds.Event(terminalapi.NewErrorf("failed to send mouse event %v to widget %T: %v", ev, c.opts.widget, err))
}
}, event.MaxRepetitive(maxReps))
}
}
return nil
}))
}