Created Panel and PanelContainer untested code
This commit is contained in:
@@ -15,8 +15,8 @@ type Component interface {
|
||||
// A component knows its position and size, which is used to draw itself in
|
||||
// its bounding rectangle.
|
||||
Draw(tcell.Screen)
|
||||
// Components can be focused, which may affect how it handles events. For
|
||||
// example, when a button is focused, the Return key may be pressed to
|
||||
// Components can be focused, which may affect how it handles events or draws.
|
||||
// For example, when a button is focused, the Return key may be pressed to
|
||||
// activate the button.
|
||||
SetFocused(bool)
|
||||
// Applies the theme to the component and all of its children.
|
||||
@@ -35,5 +35,7 @@ type Component interface {
|
||||
// used, instead.
|
||||
SetSize(int, int)
|
||||
|
||||
// It is good practice for a Component to check if it is focused before handling
|
||||
// events.
|
||||
tcell.EventHandler // A Component can handle events
|
||||
}
|
||||
|
212
ui/panel.go
Executable file
212
ui/panel.go
Executable file
@@ -0,0 +1,212 @@
|
||||
package ui
|
||||
|
||||
import "github.com/gdamore/tcell/v2"
|
||||
|
||||
// A PanelKind describes how to interpret the fields of a Panel.
|
||||
type PanelKind uint8
|
||||
|
||||
const (
|
||||
PanelKindEmpty PanelKind = iota
|
||||
PanelKindSingle // Single item. Takes up all available space
|
||||
PanelKindSplitVert // Items are above or below eachother
|
||||
PanelKindSplitHor // Items are left or right of eachother
|
||||
)
|
||||
|
||||
// A Panel represents a container for a split view between two items. The Kind
|
||||
// tells how to interpret the Left and Right fields. The SplitAt is the column
|
||||
// between 0 and width or height, representing the position of the split between
|
||||
// the Left and Right, respectively.
|
||||
//
|
||||
// If the Kind is equal to PanelKindEmpty, then both Left and Right are nil.
|
||||
// If the Kind is equal to PanelKindSingle, then only Left has value,
|
||||
// and its value will NOT be of type Panel. The SplitAt will not be used,
|
||||
// as the Left will take up the whole space.
|
||||
// If the Kind is equal to PanelKindSplitVert, then both Left and Right will
|
||||
// have value, and they will both have to be of type Panel. The split will
|
||||
// be represented vertically, and the SplitAt spans 0 to height; top to bottom,
|
||||
// respectively.
|
||||
// If the Kind is equal to PanelKindSplitHor, then both Left and Right will
|
||||
// have value, and they will both have to be of type Panel. The split will
|
||||
// be represented horizontally, and the SplitAt spans 0 to width; left to right.
|
||||
type Panel struct {
|
||||
Parent *Panel
|
||||
Left Component
|
||||
Right Component
|
||||
SplitAt int
|
||||
Kind PanelKind
|
||||
Focused bool
|
||||
|
||||
x, y int
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
// UpdateSplits uses the position and size of the Panel, along with its Weight
|
||||
// and Kind, to appropriately size and place its children. It calls UpdateSplits()
|
||||
// on its child Panels.
|
||||
func (p *Panel) UpdateSplits() {
|
||||
switch p.Kind {
|
||||
case PanelKindSingle:
|
||||
p.Left.SetPos(p.x, p.y)
|
||||
p.Left.SetSize(p.width, p.height)
|
||||
case PanelKindSplitVert:
|
||||
p.Left.SetPos(p.x, p.y)
|
||||
p.Left.SetSize(p.width, p.SplitAt)
|
||||
p.Right.SetPos(p.x, p.y+p.SplitAt)
|
||||
p.Right.SetSize(p.width, p.height-p.SplitAt)
|
||||
|
||||
p.Left.(*Panel).UpdateSplits()
|
||||
p.Right.(*Panel).UpdateSplits()
|
||||
case PanelKindSplitHor:
|
||||
p.Left.SetPos(p.x, p.y)
|
||||
p.Left.SetSize(p.SplitAt, p.height)
|
||||
p.Right.SetPos(p.x+p.SplitAt, p.y)
|
||||
p.Right.SetSize(p.width-p.SplitAt, p.height)
|
||||
|
||||
p.Left.(*Panel).UpdateSplits()
|
||||
p.Right.(*Panel).UpdateSplits()
|
||||
}
|
||||
}
|
||||
|
||||
// Same as EachLeaf, but returns true if any call to `f` returned true.
|
||||
func (p *Panel) eachLeaf(f func(*Component) bool) bool {
|
||||
switch p.Kind {
|
||||
case PanelKindSingle:
|
||||
return f(&p.Left)
|
||||
case PanelKindSplitVert:
|
||||
fallthrough
|
||||
case PanelKindSplitHor:
|
||||
if p.Left.(*Panel).eachLeaf(f) {
|
||||
return true
|
||||
}
|
||||
return p.Right.(*Panel).eachLeaf(f)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// EachLeaf visits the entire tree in left-most order, and calls function `f`
|
||||
// at each individual Component (never for Panels). If the function `f` returns
|
||||
// true, then visiting stops.
|
||||
func (p *Panel) EachLeaf(f func(*Component) bool) {
|
||||
p.eachLeaf(f)
|
||||
}
|
||||
|
||||
// IsLeaf returns whether the Panel is a leaf or not. A leaf is a panel with
|
||||
// Kind `PanelKindEmpty` or `PanelKindSingle`.
|
||||
func (p *Panel) IsLeaf() bool {
|
||||
switch p.Kind {
|
||||
case PanelKindEmpty:
|
||||
fallthrough
|
||||
case PanelKindSingle:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Panel) Draw(s tcell.Screen) {
|
||||
switch p.Kind {
|
||||
case PanelKindSplitVert:
|
||||
fallthrough
|
||||
case PanelKindSplitHor:
|
||||
p.Right.Draw(s)
|
||||
fallthrough
|
||||
case PanelKindSingle:
|
||||
p.Left.Draw(s)
|
||||
}
|
||||
}
|
||||
|
||||
// SetFocused sets this Panel's Focused field to `v`. Then, if the Panel's Kind
|
||||
// is PanelKindSingle, it sets its child (not a Panel) focused to `v`, also.
|
||||
func (p *Panel) SetFocused(v bool) {
|
||||
p.Focused = v
|
||||
switch p.Kind {
|
||||
case PanelKindSplitVert:
|
||||
fallthrough
|
||||
case PanelKindSplitHor:
|
||||
p.Right.SetFocused(v)
|
||||
fallthrough
|
||||
case PanelKindSingle:
|
||||
p.Left.SetFocused(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Panel) SetTheme(theme *Theme) {
|
||||
switch p.Kind {
|
||||
case PanelKindSplitVert:
|
||||
fallthrough
|
||||
case PanelKindSplitHor:
|
||||
p.Right.SetTheme(theme)
|
||||
fallthrough
|
||||
case PanelKindSingle:
|
||||
p.Left.SetTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPos returns the position of the panel.
|
||||
func (p *Panel) GetPos() (int, int) {
|
||||
return p.width, p.height
|
||||
}
|
||||
|
||||
// SetPos sets the position of the panel.
|
||||
func (p *Panel) SetPos(x, y int) {
|
||||
p.x, p.y = x, y
|
||||
}
|
||||
|
||||
// GetMinSize returns the combined minimum sizes of the Panel's children.
|
||||
func (p *Panel) GetMinSize() (int, int) {
|
||||
switch p.Kind {
|
||||
case PanelKindSingle:
|
||||
return p.Left.GetMinSize()
|
||||
case PanelKindSplitVert:
|
||||
// use max width, add heights
|
||||
lWidth, lHeight := p.Left.GetMinSize()
|
||||
rWidth, rHeight := p.Right.GetMinSize()
|
||||
return Max(lWidth, rWidth), lHeight + rHeight
|
||||
case PanelKindSplitHor:
|
||||
// use max height, add widths
|
||||
lWidth, lHeight := p.Left.GetMinSize()
|
||||
rWidth, rHeight := p.Right.GetMinSize()
|
||||
return lWidth + rWidth, Max(lHeight, rHeight)
|
||||
default:
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Panel) GetSize() (int, int) {
|
||||
return p.width, p.height
|
||||
}
|
||||
|
||||
// SetSize sets the Panel size to the given width, and height. It will not check
|
||||
// against GetMinSize() because it may be costly to do so. SetSize clamps the
|
||||
// Panel's SplitAt to be within the new size of the Panel.
|
||||
func (p *Panel) SetSize(width, height int) {
|
||||
p.width, p.height = width, height
|
||||
switch p.Kind {
|
||||
case PanelKindSplitVert:
|
||||
p.SplitAt = Min(p.SplitAt, height)
|
||||
case PanelKindSplitHor:
|
||||
p.SplitAt = Min(p.SplitAt, width)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleEvent propogates the event to all children, calling HandleEvent()
|
||||
// on left-most items. As usual: returns true if handled, false if unhandled.
|
||||
// This function relies on the behavior of the child Components to only handle
|
||||
// events if they are focused.
|
||||
func (p *Panel) HandleEvent(event tcell.Event) bool {
|
||||
switch p.Kind {
|
||||
case PanelKindSingle:
|
||||
return p.Left.HandleEvent(event)
|
||||
case PanelKindSplitVert:
|
||||
fallthrough
|
||||
case PanelKindSplitHor:
|
||||
if p.Left.HandleEvent(event) {
|
||||
return true
|
||||
}
|
||||
return p.Right.HandleEvent(event)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
145
ui/panelcontainer.go
Executable file
145
ui/panelcontainer.go
Executable file
@@ -0,0 +1,145 @@
|
||||
package ui
|
||||
|
||||
import "github.com/gdamore/tcell/v2"
|
||||
|
||||
type SplitKind uint8
|
||||
|
||||
const (
|
||||
SplitVertical SplitKind = SplitKind(PanelKindSplitVert) + iota
|
||||
SplitHorizontal
|
||||
)
|
||||
|
||||
type PanelContainer struct {
|
||||
root *Panel
|
||||
floating []*Panel
|
||||
selected **Panel // Only Panels with PanelKindSingle
|
||||
focused bool
|
||||
theme *Theme
|
||||
}
|
||||
|
||||
func NewPanelContainer(theme *Theme) *PanelContainer {
|
||||
root := &Panel{Kind: PanelKindEmpty}
|
||||
return &PanelContainer{
|
||||
root: root,
|
||||
floating: make([]*Panel, 0, 3),
|
||||
selected: &root,
|
||||
theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
// ClearSelected makes the selected Panel empty, but does not delete it from
|
||||
// the tree.
|
||||
func (c *PanelContainer) ClearSelected() Component {
|
||||
item := (**c.selected).Left
|
||||
(**c.selected).Left = nil
|
||||
(**c.selected).Kind = PanelKindEmpty
|
||||
(*c.selected).UpdateSplits()
|
||||
return item
|
||||
}
|
||||
|
||||
// DeleteSelected deletes the selected Panel and returns its child Component.
|
||||
// If the selected Panel is the root Panel, ClearSelected() is called, instead.
|
||||
func (c *PanelContainer) DeleteSelected() Component {
|
||||
if !(*c.selected).IsLeaf() {
|
||||
panic("selected is not leaf")
|
||||
}
|
||||
|
||||
// If selected is root, just make it empty
|
||||
if *c.selected == c.root {
|
||||
return c.ClearSelected()
|
||||
} else {
|
||||
item := (**c.selected).Left
|
||||
p := (**c.selected).Parent
|
||||
if *c.selected == (*p).Left { // If we're deleting the parent's Left
|
||||
(*p).Left = (*p).Right
|
||||
(*p).Right = nil
|
||||
} else { // Deleting parent's Right
|
||||
(*p).Right = nil
|
||||
}
|
||||
(*p).Kind = PanelKindSingle
|
||||
(*c.selected) = nil // Tell garbage collector to come pick up selected (being safe)
|
||||
c.selected = &p
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
// SplitSelected splits the selected Panel with the given Component `item`.
|
||||
// The type of split (vertical or horizontal) is determined with the `kind`.
|
||||
// If `item` is nil, the new Panel will be of kind empty.
|
||||
func (c *PanelContainer) SplitSelected(kind SplitKind, item Component) {
|
||||
if !(*c.selected).IsLeaf() {
|
||||
panic("selected is not leaf")
|
||||
}
|
||||
|
||||
// It should be asserted that whatever is selected is either PanelKindEmpty or PanelKindSingle
|
||||
if item == nil {
|
||||
(**c.selected).Right = &Panel{Parent: *c.selected, Kind: PanelKindEmpty}
|
||||
} else {
|
||||
(**c.selected).Right = &Panel{Parent: *c.selected, Left: item, Kind: PanelKindSingle}
|
||||
}
|
||||
(**c.selected).Kind = PanelKind(kind)
|
||||
(*c.selected).UpdateSplits()
|
||||
panel := (**c.selected).Left.(*Panel) // TODO: watch me... might be a bug lurking in a hidden copy here
|
||||
c.selected = &panel
|
||||
}
|
||||
|
||||
func (c *PanelContainer) SetSelected(item Component) {
|
||||
if !(*c.selected).IsLeaf() {
|
||||
panic("selected is not leaf")
|
||||
}
|
||||
|
||||
(**c.selected).Left = item
|
||||
(**c.selected).Kind = PanelKindSingle
|
||||
}
|
||||
|
||||
func (c *PanelContainer) FloatSelected() {
|
||||
|
||||
}
|
||||
|
||||
func (c *PanelContainer) UnfloatSelected() {
|
||||
|
||||
}
|
||||
|
||||
func (c *PanelContainer) Draw(s tcell.Screen) {
|
||||
c.root.Draw(s)
|
||||
}
|
||||
|
||||
func (c *PanelContainer) SetFocused(v bool) {
|
||||
c.focused = v
|
||||
// TODO: update focused on selected children
|
||||
}
|
||||
|
||||
func (c *PanelContainer) SetTheme(theme *Theme) {
|
||||
c.theme = theme
|
||||
c.root.SetTheme(theme)
|
||||
for i := range c.floating {
|
||||
c.floating[i].SetTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PanelContainer) GetPos() (int, int) {
|
||||
return c.root.GetPos()
|
||||
}
|
||||
|
||||
func (c *PanelContainer) SetPos(x, y int) {
|
||||
c.root.SetPos(x, y)
|
||||
c.root.UpdateSplits()
|
||||
}
|
||||
|
||||
func (c *PanelContainer) GetMinSize() (int, int) {
|
||||
return c.root.GetMinSize()
|
||||
}
|
||||
|
||||
func (c *PanelContainer) GetSize() (int, int) {
|
||||
return c.root.GetSize()
|
||||
}
|
||||
|
||||
func (c *PanelContainer) SetSize(width, height int) {
|
||||
c.root.SetSize(width, height)
|
||||
c.root.UpdateSplits()
|
||||
}
|
||||
|
||||
func (c *PanelContainer) HandleEvent(event tcell.Event) bool {
|
||||
// Call handle event on selected Panel
|
||||
return false
|
||||
}
|
Reference in New Issue
Block a user