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.
164 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|