Files
MujicaUI/accordion.go
T

133 lines
3.7 KiB
Go

package mujica
import (
"log/slog"
"slices"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
"mujicaui/internal"
"mujicaui/theme"
)
// AccordionItem is one section of an Accordion.
type AccordionItem struct {
// ID identifies the section in the open list; required, unique.
ID string
Title string
Content func()
Disabled bool
}
// AccordionOptions configure an Accordion.
type AccordionOptions struct {
// Multiple lets several sections stay open; otherwise opening one closes the rest.
Multiple bool
// KeepOne stops the last open section from closing.
KeepOne bool
}
// AccordionView is a built Accordion and what happened to it in this frame.
type AccordionView struct {
*ui.Element
changed bool
}
// Changed reports that a section opened or closed.
func (v AccordionView) Changed() bool { return v.changed }
// Accordion shows sections whose open IDs live in *open, which the caller controls.
func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) AccordionView {
seen := map[string]bool{}
for _, it := range items {
if it.ID == "" || seen[it.ID] || it.Content == nil {
panic("mujica: Accordion item needs a unique ID and Content: " + it.ID)
}
seen[it.ID] = true
}
k := Tokens(c)
d := Density(c)
u := d.Unit()
var v AccordionView
v.Element = ui.Column(c).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius)
focusTo := ui.Local(v.Element, "focus", func() int { return -1 })
v.Children(func() {
for i, it := range items {
ui.Column(c).Key(it.ID).Children(func() {
if i > 0 {
ui.Box(c).Height(1).FillWidth().Background(k.Border)
}
isOpen := slices.Contains(*open, it.ID)
was := isOpen
p := ui.CollapsibleBase(c, &isOpen)
tr := p.Trigger.Disabled(it.Disabled).AlignItems(ui.Center).Gap(u*2).MinHeight(d.ControlHeight()+u*2).
Padding(0, u*4).Label(it.Title)
internal.Ring(tr, k.Focus, theme.ControlRadius)
if tr.Hovered() && !it.Disabled {
tr.Background(k.SurfaceHover)
}
if *focusTo == i {
tr.Focus()
*focusTo = -1
}
if j := accordionKey(tr, items, i); j >= 0 {
*focusTo = j
}
if isOpen != was && !it.Disabled && accordionSet(open, it.ID, isOpen, opts) {
v.changed = true
}
isOpen = slices.Contains(*open, it.ID)
prog := tr.Animate("mujica.open", collapsibleTarget(isOpen), Motion(c, theme.StateDuration))
tr.Children(func() {
ui.Text(c, it.Title).FontWeight(600).Grow(1).MinWidth(0)
ui.Icon(c, icons.Must("chevron-down")).Size(16, 16).Rotate(180 * prog).TextColor(k.TextMuted).Shrink(0)
})
if panel := collapsiblePanel(c, isOpen, prog, it.Content); panel != nil {
panel.Padding(0, u*4, u*4).Gap(u * 2)
}
})
}
})
return v
}
// accordionKey returns the header the arrows, Home or End move to from header i, or -1.
func accordionKey(tr *ui.Element, items []AccordionItem, i int) int {
step, from := 0, i
switch {
case tr.Shortcut(0, ui.KeyDown):
step = 1
case tr.Shortcut(0, ui.KeyUp):
step = -1
case tr.Shortcut(0, ui.KeyHome):
step, from = 1, -1
case tr.Shortcut(0, ui.KeyEnd):
step, from = -1, len(items)
default:
return -1
}
for j := from + step; j >= 0 && j < len(items); j += step {
if !items[j].Disabled {
return j
}
}
return -1
}
// accordionSet applies a header toggle under the options and reports a change.
func accordionSet(open *[]string, id string, on bool, opts AccordionOptions) bool {
switch {
case on && opts.Multiple:
*open = append(slices.Clone(*open), id)
case on:
*open = []string{id}
case opts.KeepOne && len(*open) == 1:
return false
default:
*open = slices.DeleteFunc(slices.Clone(*open), func(s string) bool { return s == id })
}
slog.Debug("mujica: accordion toggle", "id", id, "open", on)
return true
}