WIP components (as built)
This commit is contained in:
96 files changed
+6468
-16
No files matched your search
+157
@@ -0,0 +1,157 @@
|
||||
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() })
|
||||
}
|
||||
Reference in new issue
Block a user