Files
MujicaUI/breadcrumb.go
T
ZacharyZhang-NY c22a09d327 Refactor: popups share overlayStyle and overlayEnter
internal.PopIn is gone. Breadcrumb, NavigationMenu, Pagination, Menubar
panels, CommandPalette, Select (and the other selectPanel users),
datetime popups, ColorPicker and the StatusBar overflow now style their
panel with overlayStyle(c, panel, overlayEnter(c, panel)): one shadow
(0 6 18, 16% black, scaled by entry) and one 160 ms entry fade.

Snapshots: color_picker and command_palette (light and dark) change only
in the panel shadow. ColorPicker loses its 0 4 12 18% shadow for the
softer shared one; CommandPalette loses its heavy 0 12 32 30% shadow,
and its backdrop now fades by alpha with the panel instead of through
the backdrop's opacity. Fade and reduced-motion tests pass unchanged.
2026-10-05 13:41:29 -04:00

164 lines
4.9 KiB
Go

package mujica
import (
"log/slog"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
"mujicaui/internal"
"mujicaui/theme"
)
// BreadcrumbItem is one level of a Breadcrumb path.
type BreadcrumbItem struct {
Label string
Icon *ui.SVG
}
// BreadcrumbOptions configure a Breadcrumb.
type BreadcrumbOptions struct {
// MaxItems shown before middle items fold into a menu; zero means 4, minimum 3.
MaxItems int
// MaxItemWidth cuts longer labels with an ellipsis; zero means 160 DIPs.
MaxItemWidth float32
// Label names the path for assistive technology; empty uses the built-in name.
Label string
}
// BreadcrumbResult is the built Breadcrumb.
type BreadcrumbResult struct {
Element *ui.Element
chosen int
}
// Chosen returns the index of the item the user activated in this frame.
func (r BreadcrumbResult) Chosen() (int, bool) { return r.chosen, r.chosen >= 0 }
// Breadcrumb shows a path whose last item is the current one; earlier items are links.
func Breadcrumb(c *ui.Context, items []BreadcrumbItem, opts BreadcrumbOptions) BreadcrumbResult {
if len(items) == 0 {
panic("mujica: Breadcrumb needs at least one item")
}
limit := opts.MaxItems
if limit == 0 {
limit = 4
}
if limit < 3 {
panic("mujica: Breadcrumb MaxItems must be at least 3")
}
width := opts.MaxItemWidth
if width == 0 {
width = 160
}
label := opts.Label
if label == "" {
label = msg(c, "breadcrumb.label")
}
k := Tokens(c)
u := Density(c).Unit()
row := ui.Row(c).AlignItems(ui.Center).Gap(u).MinWidth(0).Role(ui.RoleGroup).Label(label)
res := BreadcrumbResult{Element: row, chosen: -1}
choose := func(i int) {
res.chosen = i
slog.Debug("mujica: breadcrumb choose", "index", i, "label", items[i].Label)
}
// Folded items are those between the first and the last limit-2.
fold := len(items) > limit
hideFrom, hideTo := 1, len(items)-(limit-2)
row.Children(func() {
for i, it := range items {
if it.Label == "" {
panic("mujica: BreadcrumbItem needs a Label")
}
if fold && i >= hideFrom && i < hideTo {
if i == hideFrom {
breadcrumbSeparator(c)
breadcrumbMore(c, items[hideFrom:hideTo], func(j int) { choose(hideFrom + j) })
}
continue
}
if i > 0 {
breadcrumbSeparator(c)
}
if i == len(items)-1 {
cur := ui.Row(c).AlignItems(ui.Center).Gap(u).Padding(u, u*1.5).MinWidth(0).
FontWeight(600).TextColor(k.Text).Tooltip(it.Label).Label(it.Label + ", " + msg(c, "breadcrumb.current"))
cur.Children(func() { breadcrumbContent(c, it, width) })
continue
}
link := ui.ButtonBase(c).Role(ui.RoleLink).Gap(u).Padding(u, u*1.5).Radius(theme.ControlRadius).
Shrink(1).MinWidth(0).Cursor(ui.CursorPointer).Tooltip(it.Label).Label(it.Label)
internal.Ring(link, k.Focus, theme.ControlRadius)
fg := k.TextMuted
if link.Hovered() {
link.Background(k.SurfaceHover)
fg = k.Text
}
internal.Ease(link, Motion(c, theme.StateDuration), fg)
link.Children(func() { breadcrumbContent(c, it, width) })
if link.Clicked() {
choose(i)
}
}
})
return res
}
func breadcrumbContent(c *ui.Context, it BreadcrumbItem, width float32) {
if it.Icon != nil {
ui.Icon(c, it.Icon).Size(14, 14).Shrink(0)
}
ui.Text(c, it.Label).SingleLine().MaxWidth(width).MinWidth(0)
}
func breadcrumbSeparator(c *ui.Context) {
ui.Icon(c, icons.Must("chevron-right")).Size(12, 12).Shrink(0).TextColor(Tokens(c).Ornament).Role(ui.RoleNone)
}
// breadcrumbMore is the ellipsis button listing the folded items.
func breadcrumbMore(c *ui.Context, hidden []BreadcrumbItem, choose func(int)) {
k := Tokens(c)
u := Density(c).Unit()
b := ui.ButtonBase(c).Padding(u, u*1.5).Radius(theme.ControlRadius).MinWidth(theme.MinHitSize).
Label(msg(c, "breadcrumb.more")).Tooltip(msg(c, "breadcrumb.more")).Cursor(ui.CursorPointer)
internal.Ring(b, k.Focus, theme.ControlRadius)
open := ui.Local(b, "open", func() bool { return false })
fg := k.TextMuted
if b.Hovered() || *open {
b.Background(k.SurfaceHover)
fg = k.Text
}
internal.Ease(b, Motion(c, theme.StateDuration), fg)
if b.Clicked() {
*open = !*open
}
b.Children(func() { ui.Icon(c, icons.Must("ellipsis")).Size(16, 16) })
ui.PopoverBase(c, b, open, func(panel *ui.Element) {
overlayStyle(c, panel, overlayEnter(c, panel))
panel.Margin(u, 0, 0, 0).Padding(u).Gap(u * 0.5).MinWidth(160).MaxWidth(320)
for j, it := range hidden {
item := ui.ButtonBase(c).Role(ui.RoleLink).Justify(ui.Start).Gap(u*2).Padding(u*1.5, u*2).
Radius(theme.ControlRadius).MinHeight(Density(c).ControlHeight() - u*2).Label(it.Label).TextColor(k.Text)
if j == 0 {
item.AutoFocus()
}
internal.Ring(item, k.Focus, theme.ControlRadius)
if item.Hovered() {
item.Background(k.SurfaceHover)
}
internal.Ease(item, Motion(c, theme.StateDuration), k.Text)
item.Children(func() {
if it.Icon != nil {
ui.Icon(c, it.Icon).Size(14, 14).Shrink(0)
}
ui.Text(c, it.Label).SingleLine().MinWidth(0)
})
if item.Clicked() {
*open = false
choose(j)
}
}
})
}