158 lines
4.1 KiB
Go
158 lines
4.1 KiB
Go
package mujica
|
|
|
|
import (
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/icons"
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// PathBarOptions configure a PathBar.
|
|
type PathBarOptions struct {
|
|
// Separator splits the path; empty is "/".
|
|
Separator string
|
|
// RootLabel defaults to the absolute path separator.
|
|
RootLabel string
|
|
Disabled bool
|
|
}
|
|
|
|
// PathBarResult exposes the toolbar and requested path.
|
|
type PathBarResult struct {
|
|
Element *ui.Element
|
|
// Navigated is the chosen ancestor path, or empty.
|
|
Navigated string
|
|
}
|
|
|
|
// pathSegment is a crumb and the path up to it.
|
|
type pathSegment struct{ label, path string }
|
|
|
|
// pathSegments splits p into crumbs with cumulative paths.
|
|
func pathSegments(p, sep, root string) []pathSegment {
|
|
var out []pathSegment
|
|
acc := ""
|
|
if strings.HasPrefix(p, sep) {
|
|
label := root
|
|
if label == "" {
|
|
label = sep
|
|
}
|
|
out = append(out, pathSegment{label, sep})
|
|
acc = sep
|
|
}
|
|
for _, part := range strings.Split(strings.Trim(p, sep), sep) {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
if acc != "" && !strings.HasSuffix(acc, sep) {
|
|
acc += sep
|
|
}
|
|
acc += part
|
|
out = append(out, pathSegment{part, acc})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// PathBar displays clickable segments and collapses overflowing middle segments.
|
|
func PathBar(c *ui.Context, p string, opts PathBarOptions) PathBarResult {
|
|
sep := opts.Separator
|
|
if sep == "" {
|
|
sep = "/"
|
|
}
|
|
segs := pathSegments(p, sep, opts.RootLabel)
|
|
if len(segs) == 0 {
|
|
panic("mujica: PathBar needs a path")
|
|
}
|
|
k := Tokens(c)
|
|
u := Density(c).Unit()
|
|
var res PathBarResult
|
|
e := ui.Row(c).Grow(1).MinWidth(0).Gap(u).AlignItems(ui.Center).Role(ui.RoleToolbar).Label(msg(c, "path_bar.label")).
|
|
FocusGroup(ui.Horizontal).Disabled(opts.Disabled)
|
|
res.Element = e
|
|
was := e.Bounds()
|
|
internal.Remeasure(c, e, was.W, was.H)
|
|
hidden := pathBarFold(c, segs, was.W)
|
|
disabled := e.IsDisabled()
|
|
open := ui.Local(e, "path_bar.open", func() bool { return false })
|
|
*open = *open && hidden > 0 && !disabled
|
|
goTo := func(s pathSegment) {
|
|
if !disabled {
|
|
res.Navigated, *open = s.path, false
|
|
slog.Debug("mujica: path_bar navigate", "depth", strings.Count(s.path, sep))
|
|
}
|
|
}
|
|
e.Children(func() {
|
|
for i, s := range segs {
|
|
if i > 0 && (i <= 1 || i > hidden) {
|
|
ui.Icon(c, icons.Must("chevron-right")).Size(12, 12).Shrink(0).TextColor(k.TextMuted)
|
|
}
|
|
if i >= 1 && i <= hidden {
|
|
if i == 1 {
|
|
more := pathBarCrumb(c, "…", false).Label(msg(c, "path_bar.more"))
|
|
if more.Clicked() && !disabled {
|
|
*open = !*open
|
|
}
|
|
Popover(c, more, open, PopoverOptions{Label: msg(c, "path_bar.more")}, func() {
|
|
for _, h := range segs[1 : hidden+1] {
|
|
if pathBarCrumb(c, h.label, false).Key(h.path).Clicked() {
|
|
goTo(h)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
if pathBarCrumb(c, s.label, i == len(segs)-1).Key(s.path).Clicked() {
|
|
goTo(s)
|
|
}
|
|
}
|
|
})
|
|
return res
|
|
}
|
|
|
|
// pathBarFold counts middle segments to collapse into the available width.
|
|
func pathBarFold(c *ui.Context, segs []pathSegment, room float32) int {
|
|
if room <= 0 {
|
|
return 0
|
|
}
|
|
u := Density(c).Unit()
|
|
width := func(s string) float32 {
|
|
w, _ := c.MeasureText(0, ui.Span{Text: s, Size: FontSize(c, theme.BodySize)})
|
|
return max(w+4*u, theme.MinHitSize) + 12 + 2*u
|
|
}
|
|
total := float32(0)
|
|
for _, s := range segs {
|
|
total += width(s.label)
|
|
}
|
|
hidden := 0
|
|
for total > room && hidden < len(segs)-2 {
|
|
hidden++
|
|
total -= width(segs[hidden].label)
|
|
if hidden == 1 {
|
|
total += width("…")
|
|
}
|
|
}
|
|
return hidden
|
|
}
|
|
|
|
// pathBarCrumb builds a button, emphasizing the current segment.
|
|
func pathBarCrumb(c *ui.Context, label string, current bool) *ui.Element {
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Padding(0, d.Unit()*2).
|
|
Radius(theme.ControlRadius).Shrink(0).TextColor(k.TextMuted)
|
|
internal.Ring(b, k.Focus, theme.ControlRadius)
|
|
switch {
|
|
case b.Pressed():
|
|
b.Background(k.Selection).TextColor(k.Text)
|
|
case b.Hovered():
|
|
b.Background(k.SurfaceHover).TextColor(k.Text)
|
|
}
|
|
if current {
|
|
b.TextColor(k.Text).FontWeight(600).Description(msg(c, "path_bar.current"))
|
|
}
|
|
return b.Children(func() { ui.Text(c, label).SingleLine() })
|
|
}
|