Navigation: components 059-068
This commit is contained in:
58 files changed
+4650
-1
No files matched your search
@@ -0,0 +1,206 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// NavigationLink is a page of a NavigationMenu, or with Items a group of pages.
|
||||
type NavigationLink struct {
|
||||
Label string
|
||||
// Path is pushed on the router; groups need none.
|
||||
Path string
|
||||
Icon *ui.SVG
|
||||
Description string
|
||||
Items []NavigationLink
|
||||
}
|
||||
|
||||
// NavigationMenuOptions configure a NavigationMenu.
|
||||
type NavigationMenuOptions struct {
|
||||
// History shows back and forward buttons for the router.
|
||||
History bool
|
||||
// Label names the menu for assistive technology.
|
||||
Label string
|
||||
}
|
||||
|
||||
// NavigationMenu shows links pushing pages on router; the router's path marks the current one.
|
||||
func NavigationMenu(c *ui.Context, router *ui.Router, links []NavigationLink, opts NavigationMenuOptions) *ui.Element {
|
||||
if router == nil {
|
||||
panic("mujica: NavigationMenu needs a router")
|
||||
}
|
||||
if len(links) == 0 {
|
||||
panic("mujica: NavigationMenu needs links")
|
||||
}
|
||||
k := Tokens(c)
|
||||
u := Density(c).Unit()
|
||||
path := router.Path()
|
||||
label := opts.Label
|
||||
if label == "" {
|
||||
label = msg(c, "navigation_menu.label")
|
||||
}
|
||||
push := func(p string) {
|
||||
if p != path {
|
||||
slog.Debug("mujica: navigation menu push", "from", path, "to", p)
|
||||
router.Push(p)
|
||||
}
|
||||
}
|
||||
row := ui.Row(c).AlignItems(ui.Center).Gap(u * 2)
|
||||
row.Children(func() {
|
||||
if opts.History {
|
||||
ui.Row(c).Gap(u).Children(func() {
|
||||
navigationMenuHistory(c, ui.BackButton(c, router), msg(c, "navigation_menu.back"))
|
||||
navigationMenuHistory(c, ui.ForwardButton(c, router), msg(c, "navigation_menu.forward"))
|
||||
})
|
||||
ui.Box(c).Size(1, 20).Background(k.Ornament).Role(ui.RoleNone)
|
||||
}
|
||||
ui.Row(c).AlignItems(ui.Center).Gap(u).FocusGroup(ui.Horizontal).Role(ui.RoleToolbar).Label(label).Children(func() {
|
||||
for _, l := range links {
|
||||
navigationMenuTop(c, l, path, push)
|
||||
}
|
||||
})
|
||||
})
|
||||
return row
|
||||
}
|
||||
|
||||
func navigationMenuHistory(c *ui.Context, b *ui.Element, name string) {
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
b.Label(name).MinHeight(d.ControlHeight()).MinWidth(d.ControlHeight()).Radius(theme.ControlRadius).Background(k.Surface)
|
||||
internal.Ring(b, k.Focus, theme.ControlRadius)
|
||||
}
|
||||
|
||||
// navigationMenuActive reports whether l or a link inside it is the page at path.
|
||||
func navigationMenuActive(l NavigationLink, path string) bool {
|
||||
// The root page is current only on itself, not under every path.
|
||||
if l.Path != "" && (path == l.Path || l.Path != "/" && strings.HasPrefix(path, strings.TrimSuffix(l.Path, "/")+"/")) {
|
||||
return true
|
||||
}
|
||||
for _, it := range l.Items {
|
||||
if navigationMenuActive(it, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func navigationMenuTop(c *ui.Context, l NavigationLink, path string, push func(string)) {
|
||||
if l.Label == "" {
|
||||
panic("mujica: NavigationLink needs a Label")
|
||||
}
|
||||
if l.Items == nil && l.Path == "" {
|
||||
panic("mujica: NavigationLink needs a Path or Items")
|
||||
}
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
u := d.Unit()
|
||||
on := navigationMenuActive(l, path)
|
||||
b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).Padding(0, u*3).Gap(u * 1.5).Radius(theme.ControlRadius).
|
||||
Label(l.Label).TextColor(k.TextMuted).Cursor(ui.CursorPointer)
|
||||
open := ui.Local(b, "open", func() bool { return false })
|
||||
if l.Items == nil {
|
||||
b.Role(ui.RoleLink)
|
||||
} else {
|
||||
b.Role(ui.RoleMenuButton)
|
||||
}
|
||||
if on {
|
||||
b.TextColor(k.Text).FontWeight(600)
|
||||
}
|
||||
if b.Hovered() || *open {
|
||||
b.Background(k.SurfaceHover).TextColor(k.Text)
|
||||
}
|
||||
b.FocusRing(false).DrawOver(func(p *ui.Painter, r ui.Rect) {
|
||||
if on {
|
||||
p.Fill(ui.Rect{X: r.X + u*3, Y: r.Y + r.H - 2, W: r.W - u*6, H: 2}, k.AccentText, 1)
|
||||
}
|
||||
if b.FocusVisible() {
|
||||
internal.FocusRing(p, r, theme.ControlRadius, k.Focus)
|
||||
}
|
||||
})
|
||||
b.Children(func() {
|
||||
if l.Icon != nil {
|
||||
ui.Icon(c, l.Icon).Size(16, 16)
|
||||
}
|
||||
ui.Text(c, l.Label).SingleLine()
|
||||
if l.Items != nil {
|
||||
ui.Icon(c, icons.Must("chevron-down")).Size(12, 12).TextColor(k.TextMuted)
|
||||
}
|
||||
})
|
||||
if l.Items == nil {
|
||||
if b.Clicked() {
|
||||
push(l.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
if b.Clicked() {
|
||||
*open = !*open
|
||||
} else if b.Shortcut(0, ui.KeyDown) {
|
||||
*open = true
|
||||
}
|
||||
ui.PopoverBase(c, b, open, func(panel *ui.Element) {
|
||||
panel.Margin(u, 0, 0, 0).Padding(u*2).Gap(u).Radius(theme.PopupRadius).Background(k.Surface).
|
||||
Border(theme.BorderWidth, k.Border).Shadow(0, 4, 12, 0, ui.RGBA(0, 0, 0, 0.16)).MinWidth(240).MaxWidth(360)
|
||||
panel.FocusGroup(ui.Vertical)
|
||||
first := true
|
||||
var list func(items []NavigationLink)
|
||||
list = func(items []NavigationLink) {
|
||||
for _, it := range items {
|
||||
if it.Label == "" {
|
||||
panic("mujica: NavigationLink needs a Label")
|
||||
}
|
||||
if it.Items != nil {
|
||||
ui.Text(c, it.Label).Font(theme.SerifFont).FontSize(theme.CaptionSize).TextColor(k.Ornament.Mix(k.Text, 0.35)).
|
||||
Padding(u*2, u*2, u, u*2).SingleLine()
|
||||
list(it.Items)
|
||||
continue
|
||||
}
|
||||
if it.Path == "" {
|
||||
panic("mujica: NavigationLink needs a Path or Items")
|
||||
}
|
||||
if navigationMenuItem(c, it, it.Path == path, first).Clicked() {
|
||||
*open = false
|
||||
push(it.Path)
|
||||
}
|
||||
first = false
|
||||
}
|
||||
}
|
||||
list(l.Items)
|
||||
})
|
||||
}
|
||||
|
||||
func navigationMenuItem(c *ui.Context, l NavigationLink, on, first bool) *ui.Element {
|
||||
k := Tokens(c)
|
||||
u := Density(c).Unit()
|
||||
e := ui.ButtonBase(c).Role(ui.RoleLink).Justify(ui.Start).AlignItems(ui.Start).Gap(u*3).
|
||||
Padding(u*2, u*3).Radius(theme.ControlRadius).Label(l.Label).TextColor(k.Text).Cursor(ui.CursorPointer)
|
||||
if first {
|
||||
e.AutoFocus()
|
||||
}
|
||||
internal.Ring(e, k.Focus, theme.ControlRadius)
|
||||
switch {
|
||||
case on:
|
||||
e.Background(k.Selection).FontWeight(600)
|
||||
e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
p.Fill(ui.Rect{X: r.X, Y: r.Y + 4, W: 3, H: r.H - 8}, k.AccentText, 1.5)
|
||||
})
|
||||
case e.Hovered():
|
||||
e.Background(k.SurfaceHover)
|
||||
}
|
||||
e.Children(func() {
|
||||
if l.Icon != nil {
|
||||
ui.Icon(c, l.Icon).Size(16, 16).Margin(2, 0, 0, 0)
|
||||
}
|
||||
ui.Column(c).Gap(2).MinWidth(0).Children(func() {
|
||||
ui.Text(c, l.Label).SingleLine()
|
||||
if l.Description != "" {
|
||||
ui.Text(c, l.Description).FontSize(theme.CaptionSize).TextColor(k.TextMuted).FontWeight(400)
|
||||
}
|
||||
})
|
||||
})
|
||||
return e
|
||||
}
|
||||
Reference in new issue
Block a user