From 03c8c3bcadcdfbfa31e905a12173087dfe523775 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Mon, 5 Apr 2021 16:22:40 -0500 Subject: [PATCH] Created Panel and PanelContainer untested code --- ui/component.go | 6 +- ui/panel.go | 212 +++++++++++++++++++++++++++++++++++++++++++ ui/panelcontainer.go | 145 +++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+), 2 deletions(-) create mode 100755 ui/panel.go create mode 100755 ui/panelcontainer.go diff --git a/ui/component.go b/ui/component.go index d291b6f..107b270 100644 --- a/ui/component.go +++ b/ui/component.go @@ -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 } diff --git a/ui/panel.go b/ui/panel.go new file mode 100755 index 0000000..00a139e --- /dev/null +++ b/ui/panel.go @@ -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 + } +} diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go new file mode 100755 index 0000000..86c5cfd --- /dev/null +++ b/ui/panelcontainer.go @@ -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 +}