Noah Petherbridge
fee6e1e105
New properties are added to EventData for Supervisor events: * Widget: a reference to the widget which is receiving the event. * Clicked (bool): for MouseMove events records if the primary button is pressed. * func RelativePoint(): returns a version of EventData.Point adjusted to be relative to the Widget (0,0 at the Widget's absolute position on screen). Other changes: * Destroy() method for the Widget interface: widgets that need to free up resources on teardown should define this, the BaseWidget provides a no-op implementation. * Window.Resize() will properly resize a Window. * Window.Center(w, h int) to easily center a window on screen.
301 lines
7.5 KiB
Go
301 lines
7.5 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()
|
|
}
|
|
}
|
|
|
|
// Header returns access to the ui.Frame that holds the tab buttons. Use
|
|
// at your own risk -- the UI arrangement in this Frame is not guaranteed
|
|
// stable.
|
|
func (w *TabFrame) Header() *Frame {
|
|
return w.header
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
}
|