tcell/views/boxlayout.go

332 lines
6.8 KiB
Go

// Copyright 2016 The Tcell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use 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 views
import (
"github.com/gdamore/tcell/v2"
)
// BoxLayout is a container Widget that lays out its child widgets in
// either a horizontal row or a vertical column.
type BoxLayout struct {
view View
orient Orientation
style tcell.Style // backing style
cells []*boxLayoutCell
width int
height int
changed bool
WidgetWatchers
}
type boxLayoutCell struct {
widget Widget
fill float64 // fill factor - 0.0 means no expansion
pad int // count of padding spaces (stretch)
frac float64 // calculated residual spacing, used internally
view *ViewPort
}
func (b *BoxLayout) hLayout() {
w, h := b.view.Size()
totf := 0.0
for _, c := range b.cells {
x, y := c.widget.Size()
totf += c.fill
b.width += x
if y > b.height {
b.height = y
}
c.pad = 0
c.frac = 0
}
extra := w - b.width
if extra < 0 {
extra = 0
}
resid := extra
if totf == 0 {
resid = 0
}
for _, c := range b.cells {
if c.fill > 0 {
c.frac = float64(extra) * c.fill / totf
c.pad = int(c.frac)
c.frac -= float64(c.pad)
resid -= c.pad
}
}
// Distribute any left over padding. We try to give it to the
// the cells with the highest residual fraction. It should be
// the case that no single cell gets more than one more cell.
for resid > 0 {
var best *boxLayoutCell
for _, c := range b.cells {
if c.fill == 0 {
continue
}
if best == nil || c.frac > best.frac {
best = c
}
}
best.pad++
best.frac = 0
resid--
}
x, y, xinc := 0, 0, 0
for _, c := range b.cells {
cw, _ := c.widget.Size()
xinc = cw + c.pad
cw += c.pad
c.view.Resize(x, y, cw, h)
c.widget.Resize()
x += xinc
}
}
func (b *BoxLayout) vLayout() {
w, h := b.view.Size()
totf := 0.0
for _, c := range b.cells {
x, y := c.widget.Size()
b.height += y
totf += c.fill
if x > b.width {
b.width = x
}
c.pad = 0
c.frac = 0
}
extra := h - b.height
if extra < 0 {
extra = 0
}
resid := extra
if totf == 0 {
resid = 0
}
for _, c := range b.cells {
if c.fill > 0 {
c.frac = float64(extra) * c.fill / totf
c.pad = int(c.frac)
c.frac -= float64(c.pad)
resid -= c.pad
}
}
// Distribute any left over padding. We try to give it to the
// the cells with the highest residual fraction. It should be
// the case that no single cell gets more than one more cell.
for resid > 0 {
var best *boxLayoutCell
for _, c := range b.cells {
if c.fill == 0 {
continue
}
if best == nil || c.frac > best.frac {
best = c
}
}
best.pad++
best.frac = 0
resid--
}
x, y, yinc := 0, 0, 0
for _, c := range b.cells {
_, ch := c.widget.Size()
yinc = ch + c.pad
ch += c.pad
c.view.Resize(x, y, w, ch)
c.widget.Resize()
y += yinc
}
}
func (b *BoxLayout) layout() {
if b.view == nil {
return
}
b.width, b.height = 0, 0
switch b.orient {
case Horizontal:
b.hLayout()
case Vertical:
b.vLayout()
default:
panic("Bad orientation")
}
b.changed = false
}
// Resize adjusts the layout when the underlying View changes size.
func (b *BoxLayout) Resize() {
b.layout()
// Now also let the children know we resized.
for i := range b.cells {
b.cells[i].widget.Resize()
}
b.PostEventWidgetResize(b)
}
// Draw is called to update the displayed content.
func (b *BoxLayout) Draw() {
if b.view == nil {
return
}
if b.changed {
b.layout()
}
b.view.Fill(' ', b.style)
for i := range b.cells {
b.cells[i].widget.Draw()
}
}
// Size returns the preferred size in character cells (width, height).
func (b *BoxLayout) Size() (int, int) {
return b.width, b.height
}
// SetView sets the View object used for the text bar.
func (b *BoxLayout) SetView(view View) {
b.changed = true
b.view = view
for _, c := range b.cells {
c.view.SetView(view)
}
}
// HandleEvent implements a tcell.EventHandler. The only events
// we care about are Widget change events from our children. We
// watch for those so that if the child changes, we can arrange
// to update our layout.
func (b *BoxLayout) HandleEvent(ev tcell.Event) bool {
switch ev.(type) {
case *EventWidgetContent:
// This can only have come from one of our children.
b.changed = true
b.PostEventWidgetContent(b)
return true
}
for _, c := range b.cells {
if c.widget.HandleEvent(ev) {
return true
}
}
return false
}
// AddWidget adds a widget to the end of the BoxLayout.
func (b *BoxLayout) AddWidget(widget Widget, fill float64) {
c := &boxLayoutCell{
widget: widget,
fill: fill,
view: NewViewPort(b.view, 0, 0, 0, 0),
}
widget.SetView(c.view)
b.cells = append(b.cells, c)
b.changed = true
widget.Watch(b)
b.layout()
b.PostEventWidgetContent(b)
}
// InsertWidget inserts a widget at the given offset. Offset 0 is the
// front. If the index is longer than the number of widgets, then it
// just gets appended to the end.
func (b *BoxLayout) InsertWidget(index int, widget Widget, fill float64) {
c := &boxLayoutCell{
widget: widget,
fill: fill,
view: NewViewPort(b.view, 0, 0, 0, 0),
}
c.widget.SetView(c.view)
if index < 0 {
index = 0
}
if index > len(b.cells) {
index = len(b.cells)
}
b.cells = append(b.cells, c)
copy(b.cells[index+1:], b.cells[index:])
b.cells[index] = c
widget.Watch(b)
b.layout()
b.PostEventWidgetContent(b)
}
// RemoveWidget removes a Widget from the layout.
func (b *BoxLayout) RemoveWidget(widget Widget) {
changed := false
for i := 0; i < len(b.cells); i++ {
if b.cells[i].widget == widget {
b.cells = append(b.cells[:i], b.cells[i+1:]...)
changed = true
}
}
if !changed {
return
}
b.changed = true
widget.Unwatch(b)
b.layout()
b.PostEventWidgetContent(b)
}
// Widgets returns the list of Widgets for this BoxLayout.
func (b *BoxLayout) Widgets() []Widget {
w := make([]Widget, 0, len(b.cells))
for _, c := range b.cells {
w = append(w, c.widget)
}
return w
}
// SetOrientation sets the orientation as either Horizontal or Vertical.
func (b *BoxLayout) SetOrientation(orient Orientation) {
if b.orient != orient {
b.orient = orient
b.changed = true
b.PostEventWidgetContent(b)
}
}
// SetStyle sets the style used.
func (b *BoxLayout) SetStyle(style tcell.Style) {
b.style = style
b.PostEventWidgetContent(b)
}
// NewBoxLayout creates an empty BoxLayout.
func NewBoxLayout(orient Orientation) *BoxLayout {
return &BoxLayout{orient: orient}
}