Files
MujicaUI/tabs.go
T

175 lines
4.4 KiB
Go

package mujica
import (
"log/slog"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"mujicaui/theme"
)
// Tab is one tab of Tabs.
type Tab struct {
// Label is required; it names the tab and its panel.
Label string
Icon *ui.SVG
Disabled bool
// Panel builds the tab's content while it is chosen; nil shows none.
Panel func()
}
// TabsOptions configure Tabs.
type TabsOptions struct {
// Vertical stacks the tabs at the start with the panel beside them.
Vertical bool
// Label names the tab list for assistive technology.
Label string
}
// TabsResult is the built Tabs: the element holding tabs and panel.
type TabsResult struct {
*ui.Element
changed bool
}
// Changed reports a new choice made by the user in this frame.
func (r TabsResult) Changed() bool { return r.changed }
// Tabs shows tabs choosing *selected and the chosen tab's panel. Disabled tabs are skipped by the arrows.
func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) TabsResult {
if len(tabs) == 0 {
panic("mujica: Tabs needs at least one tab")
}
if *selected < 0 || *selected >= len(tabs) {
panic("mujica: Tabs selected index out of range")
}
if tabs[*selected].Disabled {
panic("mujica: Tabs selected tab is disabled")
}
k := Tokens(c)
d := Density(c)
u := d.Unit()
pad := theme.FocusGap + theme.FocusWidth
// TabsBase only sees enabled tabs, so the arrows skip disabled ones.
var enabled []int
at := 0
for i, t := range tabs {
if !t.Disabled {
if i == *selected {
at = len(enabled)
}
enabled = append(enabled, i)
}
}
root := ui.Column(c).AlignItems(ui.Stretch)
if opts.Vertical {
root.Row()
}
res := TabsResult{Element: root}
root.Children(func() {
var strip *ui.Element
if opts.Vertical {
strip = ui.Scroll(c).Shrink(0).Padding(pad)
} else {
strip = ui.ScrollHorizontal(c).Shrink(0).MinWidth(0).Padding(pad, pad, 0, pad)
}
strip.Draw(func(p *ui.Painter, r ui.Rect) {
if opts.Vertical {
p.Fill(ui.Rect{X: r.X + r.W - 1, Y: r.Y, W: 1, H: r.H}, k.Border, 0)
} else {
p.Fill(ui.Rect{X: r.X, Y: r.Y + r.H - 1, W: r.W, H: 1}, k.Border, 0)
}
})
strip.Children(func() {
base := ui.TabsBase(c, &at, len(enabled))
list := base.List.Gap(u)
if opts.Vertical {
list.Column().AlignItems(ui.Stretch)
}
if opts.Label != "" {
list.Label(opts.Label)
}
built := make([]*ui.Element, len(tabs))
list.Children(func() {
n := 0
for i, t := range tabs {
var tab *ui.Element
if t.Disabled {
tab = ui.Row(c).Shrink(0).Role(ui.RoleTab).Disabled(true)
} else {
tab = base.Tab(n)
n++
}
tabsStyle(c, tab, t, i == enabled[at], opts.Vertical)
built[i] = tab
}
})
if list.Changed() {
prev := *selected
*selected = enabled[at]
res.changed = true
built[*selected].ScrollIntoView()
slog.Debug("mujica: tabs change", "from", prev, "to", *selected)
}
})
panel := ui.Column(c).MinWidth(0).Padding(u * 4).Role(ui.RoleGroup).Label(tabs[*selected].Label)
if opts.Vertical {
panel.Grow(1)
}
if p := tabs[*selected].Panel; p != nil {
panel.Children(p)
}
})
return res
}
func tabsStyle(c *ui.Context, tab *ui.Element, t Tab, on, vertical bool) {
k := Tokens(c)
d := Density(c)
u := d.Unit()
tab.MinHeight(d.ControlHeight()).Padding(0, u*4).Gap(u * 2).AlignItems(ui.Center).Cursor(ui.CursorPointer)
if vertical {
tab.Radius(theme.ControlRadius, 0, 0, theme.ControlRadius)
} else {
tab.Radius(theme.ControlRadius, theme.ControlRadius, 0, 0)
}
if t.Disabled {
tab.Cursor(ui.CursorNotAllowed)
}
if t.Label == "" {
panic("mujica: Tab needs a Label")
}
fg := k.TextMuted
switch {
case on:
tab.Background(k.Selection).FontWeight(600)
fg = k.Text
case !t.Disabled && tab.Pressed():
tab.Background(k.Selection)
fg = k.Text
case !t.Disabled && tab.Hovered():
tab.Background(k.SurfaceHover)
fg = k.Text
}
internal.Ease(tab, Motion(c, theme.StateDuration), fg)
tab.FocusRing(false).DrawOver(func(p *ui.Painter, r ui.Rect) {
if on {
if vertical {
p.Fill(ui.Rect{X: r.X + r.W - 2, Y: r.Y, W: 2, H: r.H}, k.AccentText, 0)
} else {
p.Fill(ui.Rect{X: r.X, Y: r.Y + r.H - 2, W: r.W, H: 2}, k.AccentText, 0)
}
}
if tab.FocusVisible() {
internal.FocusRing(p, r, theme.ControlRadius, k.Focus)
}
})
tab.Children(func() {
if t.Icon != nil {
ui.Icon(c, t.Icon).Size(16, 16)
}
ui.Text(c, t.Label).SingleLine()
})
}