Files
MujicaUI/avatar_group.go
T
ZacharyZhang-NY 43c96a7a81 Refactor: one colorFade transition replaces internal.Fade
textInputFade becomes colorFade in theme.go and serves AvatarGroup,
Image retry, Tag and EditableText, which used internal.Fade. MyGo already
skips element transitions under ReduceMotion, so dropping the Motion
check keeps behaviour.
2026-10-05 13:50:47 -04:00

131 lines
3.8 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"mujicaui/theme"
)
// AvatarItem is one person of an AvatarGroup.
type AvatarItem struct {
Name string
Image *ui.Bitmap
}
// AvatarGroupOptions configure an AvatarGroup.
type AvatarGroupOptions struct {
// Max avatars show before the "+N" button; zero means 4.
Max int
// Size is each avatar's diameter; zero means 32.
Size float32
}
type avatarGroupKey struct{}
// AvatarGroupView is a shown AvatarGroup and its full list's state.
type AvatarGroupView struct {
Element *ui.Element
// Open reports whether the full list shows.
Open bool
// Toggled reports the full list opened or closed this frame.
Toggled bool
}
// AvatarGroup overlaps avatars; "+N" opens the full list.
func AvatarGroup(c *ui.Context, items []AvatarItem, opts AvatarGroupOptions) AvatarGroupView {
limit := opts.Max
if limit < 0 {
panic("mujica: AvatarGroup Max must not be negative")
}
if limit == 0 {
limit = 4
}
size := opts.Size
if size < 0 {
panic("mujica: AvatarGroup Size must not be negative")
}
if size == 0 {
size = 32
}
k := Tokens(c)
shown := min(limit, len(items))
ring := float32(2)
row := ui.Row(c).AlignItems(ui.Center).Role(ui.RoleGroup)
open := ui.Local(row, avatarGroupKey{}, func() bool { return false })
was := *open
row.Children(func() {
for i, it := range items[:shown] {
cell := ui.Box(c).Key(i).Padding(ring).Radius(size/2 + ring).Background(k.Background).Shrink(0)
if i > 0 {
cell.Margin(0, 0, 0, -size/4)
}
cell.Children(func() { Avatar(c, it.Name, AvatarOptions{Image: it.Image, Size: size}) })
}
rest := len(items) - shown
if rest == 0 {
*open = false
return
}
more := avatarGroupMore(c, k, open, size, ring, rest, len(items))
ui.PopoverBase(c, more, open, func(panel *ui.Element) {
avatarGroupList(c, k, panel, items)
})
})
v := AvatarGroupView{Element: row, Open: *open, Toggled: *open != was}
if v.Toggled {
slog.Debug("mujica: avatargroup toggle", "open", v.Open)
}
return v
}
// avatarGroupMore is the "+N" disclosure toggling *open.
func avatarGroupMore(c *ui.Context, k theme.Tokens, open *bool, size, ring float32, rest, total int) *ui.Element {
hit := max(size+2*ring, theme.MinHitSize)
var b *ui.Element
ui.Box(c).Key("more").Margin(0, 0, 0, -size/4).Shrink(0).Children(func() {
b = ui.CollapsibleBase(c, open).Trigger.Size(hit, hit).Center().Radius(hit / 2).
Label(fmt.Sprintf(msg(c, "avatar_group.more"), total))
internal.Ring(b, k.Focus, hit/2)
bg := k.Surface
if b.Pressed() {
bg = k.Selection
} else if b.Hovered() {
bg = k.SurfaceHover
}
b.Children(func() {
ui.Box(c).Padding(ring).Radius(size/2 + ring).Background(k.Background).Children(func() {
disc := ui.Box(c).Size(size, size).Center().Radius(size/2).Background(bg).
Border(theme.BorderWidth, k.ControlBorder).TextColor(k.Text)
disc.Transition(colorFade)
disc.Children(func() {
ui.Text(c, fmt.Sprintf("+%d", rest)).FontSize(size * 0.36).FontWeight(600).SingleLine()
})
})
})
})
return b
}
func avatarGroupList(c *ui.Context, k theme.Tokens, panel *ui.Element, items []AvatarItem) {
u := Density(c).Unit()
panel.Margin(u, 0, 0, 0).Padding(u*2).Radius(theme.PopupRadius).Background(k.Surface).
Border(theme.BorderWidth, k.Border).MinWidth(200)
if d := Motion(c, theme.PopupDuration); d > 0 {
panel.Transition(ui.ElementTransition{Duration: d, Enter: &ui.Motion{Y: -4}})
}
ui.Scroll(c).MaxHeight(240).Children(func() {
ui.Column(c).Gap(u * 2).Role(ui.RoleList).Children(func() {
for i, it := range items {
ui.Row(c).Key(i).Gap(u * 2).AlignItems(ui.Center).Role(ui.RoleListItem).Children(func() {
Avatar(c, it.Name, AvatarOptions{Image: it.Image, Size: 24})
ui.Text(c, it.Name).SingleLine()
})
}
})
})
}