Noah Petherbridge
0a6054baa6
SetTabsHidden(bool) can hide the tab row on a TabFrame, leaving only the content frames behind. This way the caller can provide the transition changes via SetTab and use the TabFrame only for its management features.
294 lines
7.3 KiB
Go
294 lines
7.3 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/ui/style"
|
|
"git.kirsle.net/go/ui/theme"
|
|
)
|
|
|
|
// TabFrame is a tabbed notebook of multiple frames showing
|
|
// tab names along the top and clicking them reveals each
|
|
// named tab.
|
|
type TabFrame struct {
|
|
Name string
|
|
Frame
|
|
|
|
supervisor *Supervisor
|
|
style *style.Button
|
|
|
|
// Child widgets.
|
|
header *Frame
|
|
content *Frame
|
|
tabButtons []*Button
|
|
tabFrames []*Frame
|
|
currentTabKey string
|
|
}
|
|
|
|
// NewTabFrame creates a new Frame.
|
|
func NewTabFrame(name string) *TabFrame {
|
|
w := &TabFrame{
|
|
Name: name,
|
|
style: Theme.TabFrame,
|
|
header: NewFrame(name + " Header"),
|
|
content: NewFrame(name + " Content"),
|
|
tabButtons: []*Button{},
|
|
tabFrames: []*Frame{},
|
|
}
|
|
|
|
// Initialize the root frame of this widget.
|
|
w.Frame.Setup()
|
|
|
|
// Pack the high-level layout into the root frame.
|
|
// Only root needs to Present for this widget.
|
|
w.Frame.Pack(w.header, Pack{
|
|
Side: N,
|
|
FillX: true,
|
|
})
|
|
w.Frame.Pack(w.content, Pack{
|
|
Side: N,
|
|
Fill: true,
|
|
Expand: true,
|
|
})
|
|
|
|
w.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG
|
|
w.IDFunc(func() string {
|
|
return fmt.Sprintf("TabFrame<%s>",
|
|
name,
|
|
)
|
|
})
|
|
|
|
return w
|
|
}
|
|
|
|
// AddTab creates a new content tab. The key is a unique identifier
|
|
// for the tab and is how the TabFrame knows which tab is selected.
|
|
//
|
|
// The child widget would probably be a Label or Image but could be
|
|
// any other kind of widget.
|
|
//
|
|
// The first tab added becomes the selected tab by default.
|
|
func (w *TabFrame) AddTab(key string, child Widget) *Frame {
|
|
// Create the tab button for this tab.
|
|
button := NewButton(key, child)
|
|
button.SetStyle(w.style)
|
|
button.FixedColor = true
|
|
button.SetOutlineSize(0)
|
|
button.SetBorderSize(1)
|
|
w.setButtonStyle(button, len(w.tabButtons) == 0)
|
|
w.header.Pack(button, Pack{
|
|
Side: W,
|
|
})
|
|
|
|
button.Handle(MouseDown, func(ed EventData) error {
|
|
w.SetTab(key)
|
|
return nil
|
|
})
|
|
|
|
// Create the frame of the tab's body.
|
|
frame := NewFrame(key)
|
|
frame.Configure(Config{
|
|
BorderSize: w.style.BorderSize,
|
|
Background: w.style.Background,
|
|
BorderStyle: BorderRaised,
|
|
})
|
|
if len(w.tabFrames) > 0 {
|
|
frame.Hide()
|
|
}
|
|
|
|
// Pack this frame into the content part of the widget.
|
|
w.content.Pack(frame, Pack{
|
|
Side: N,
|
|
FillX: true,
|
|
})
|
|
|
|
w.tabButtons = append(w.tabButtons, button)
|
|
w.tabFrames = append(w.tabFrames, frame)
|
|
return frame
|
|
}
|
|
|
|
// SetTabsHidden can hide the tab buttons and reveal only their frames.
|
|
// It would be up to the caller to SetTab between the frames, using the
|
|
// TabFrame only for placement and tab handling.
|
|
func (w *TabFrame) SetTabsHidden(hidden bool) {
|
|
if hidden {
|
|
w.header.Hide()
|
|
} else {
|
|
w.header.Show()
|
|
}
|
|
}
|
|
|
|
// set the tab style between active and inactive
|
|
func (w *TabFrame) setButtonStyle(button *Button, active bool) {
|
|
var style = button.GetStyle()
|
|
if active {
|
|
button.SetBackground(style.Background)
|
|
button.SetBorderStyle(BorderRaised)
|
|
if label, ok := button.child.(*Label); ok {
|
|
label.Font.Color = style.Foreground
|
|
}
|
|
} else {
|
|
button.SetBackground(style.Background.Darken(theme.BorderColorOffset))
|
|
button.SetBorderStyle(BorderSolid)
|
|
if label, ok := button.child.(*Label); ok {
|
|
label.Font.Color = style.Foreground
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetTab changes the selected tab to the new value. If the
|
|
// tab doesn't exist, the first tab is selected.
|
|
func (w *TabFrame) SetTab(key string) bool {
|
|
var found bool
|
|
for i, frame := range w.tabFrames {
|
|
button := w.tabButtons[i]
|
|
if frame.Name == key {
|
|
frame.Show()
|
|
w.setButtonStyle(button, true)
|
|
w.currentTabKey = key
|
|
found = true
|
|
} else {
|
|
frame.Hide()
|
|
w.setButtonStyle(button, false)
|
|
}
|
|
}
|
|
|
|
if !found && len(w.tabFrames) > 0 {
|
|
w.tabFrames[0].Show()
|
|
w.currentTabKey = w.tabFrames[0].Name
|
|
}
|
|
|
|
return found
|
|
}
|
|
|
|
// Supervise activates the tab frame using your supervisor. If you
|
|
// don't call this, the tab buttons won't be clickable!
|
|
//
|
|
// Call this AFTER adding all tabs. This function calls Supervisor.Add
|
|
// on all tab buttons.
|
|
func (w *TabFrame) Supervise(supervisor *Supervisor) {
|
|
for _, button := range w.tabButtons {
|
|
supervisor.Add(button)
|
|
}
|
|
}
|
|
|
|
// SetStyle controls the visual styling of the tab button bar.
|
|
func (w *TabFrame) SetStyle(style *style.Button) {
|
|
w.style = style
|
|
for _, button := range w.tabButtons {
|
|
button.SetStyle(style)
|
|
w.setButtonStyle(button, !button.Hidden())
|
|
}
|
|
}
|
|
|
|
// Compute the size of the Frame.
|
|
func (w *TabFrame) Compute(e render.Engine) {
|
|
// Compute all the child frames.
|
|
w.Frame.Compute(e)
|
|
|
|
// Call the BaseWidget Compute in case we have subscribers.
|
|
w.BaseWidget.Compute(e)
|
|
}
|
|
|
|
// Present the Frame.
|
|
func (w *TabFrame) Present(e render.Engine, P render.Point) {
|
|
if w.Hidden() {
|
|
return
|
|
}
|
|
|
|
var (
|
|
S = w.Size()
|
|
)
|
|
|
|
// Draw the widget's border and everything.
|
|
w.DrawBox(e, P)
|
|
|
|
// Draw the background color.
|
|
e.DrawBox(w.Background(), render.Rect{
|
|
X: P.X + w.BoxThickness(1),
|
|
Y: P.Y + w.BoxThickness(1),
|
|
W: S.W - w.BoxThickness(2),
|
|
H: S.H - w.BoxThickness(2),
|
|
})
|
|
|
|
// Present the root frame.
|
|
w.Frame.Present(e, P)
|
|
|
|
// Draw the borders over the tabs.
|
|
w.presentBorders(e, P)
|
|
|
|
// Call the BaseWidget Present in case we have subscribers.
|
|
w.BaseWidget.Present(e, P)
|
|
}
|
|
|
|
/*
|
|
presentBorders handles drawing the borders around tab buttons.
|
|
|
|
The tabs are simple Button widgets but drawn with no borders. Instead,
|
|
borders are painted on post-hoc in the Present function.
|
|
*/
|
|
func (w *TabFrame) presentBorders(e render.Engine, P render.Point) {
|
|
if len(w.tabButtons) == 0 || w.header.Hidden() {
|
|
return
|
|
}
|
|
|
|
// Prep some variables.
|
|
var (
|
|
// The 1st and last tab button widgets.
|
|
first = w.tabButtons[0]
|
|
last = w.tabButtons[len(w.tabButtons)-1]
|
|
topLeft = AbsolutePosition(first)
|
|
bottomRight = AbsolutePosition(last)
|
|
|
|
// The absolute bounding box of the tabs part of the UI,
|
|
// from the top-left corner of Tab #1 to the bottom-right
|
|
// corner of the final tab.
|
|
bounding = render.Rect{
|
|
X: P.X, //topLeft.X + first.BoxThickness(4),
|
|
Y: P.Y, //topLeft.Y + first.BoxThickness(4),
|
|
W: bottomRight.X + last.Size().W - topLeft.X,
|
|
H: bottomRight.Y + last.Size().H - topLeft.Y,
|
|
}
|
|
|
|
// The very bottom edge of the whole tab bar,
|
|
// to overlap the BorderSize=1 along their buttons.
|
|
bottomLine = []render.Point{
|
|
render.NewPoint(P.X+1, bounding.Y+bounding.H-1),
|
|
render.NewPoint(bounding.X+bounding.W-1, bounding.Y+bounding.H-1),
|
|
}
|
|
)
|
|
|
|
// Draw a shadow border on all the inactive tabs' right edges,
|
|
// so they don't all blend together in solid grey.
|
|
// Note: the active button has a BorderSize=1 and others are 0.
|
|
for i, button := range w.tabButtons {
|
|
if button.Name != w.currentTabKey {
|
|
// If it immediately precedes the current tab, do not draw the line,
|
|
// it would cover the highlight color of the current tab's button.
|
|
if i+1 < len(w.tabButtons) && w.tabButtons[i+1].Name == w.currentTabKey {
|
|
continue
|
|
}
|
|
|
|
var (
|
|
abs = AbsolutePosition(button)
|
|
size = button.BoxSize()
|
|
points = []render.Point{
|
|
render.NewPoint(abs.X+size.W-1, abs.Y+2),
|
|
render.NewPoint(abs.X+size.W-1, abs.Y+size.H-2),
|
|
}
|
|
)
|
|
e.DrawLine(button.Background().Darken(theme.BorderColorOffset), points[0], points[1])
|
|
}
|
|
}
|
|
|
|
// Erase the button edge from all tabs.
|
|
e.DrawLine(w.style.Background, bottomLine[0], bottomLine[1])
|
|
e.DrawBox(w.style.Background, render.Rect{
|
|
X: bottomLine[0].X + 1,
|
|
Y: bottomLine[0].Y,
|
|
W: bounding.W - 2,
|
|
H: 4,
|
|
})
|
|
}
|