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.
106 lines
3.0 KiB
Go
106 lines
3.0 KiB
Go
package mujica
|
|
|
|
import (
|
|
"log/slog"
|
|
"slices"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// RadioItem is one choice of a RadioGroup; Value is its stable identity.
|
|
type RadioItem[T comparable] struct {
|
|
Value T
|
|
Label string
|
|
Disabled bool
|
|
}
|
|
|
|
// RadioGroupOptions configure a RadioGroup.
|
|
type RadioGroupOptions struct {
|
|
// Horizontal lays the choices in a row; the default is a column.
|
|
Horizontal bool
|
|
// Label names the group for assistive technology.
|
|
Label string
|
|
Disabled bool
|
|
}
|
|
|
|
// RadioGroup chooses one item's Value into *value; arrows move the choice.
|
|
func RadioGroup[T comparable](c *ui.Context, value *T, items []RadioItem[T], opts RadioGroupOptions) ChoiceResult {
|
|
if value == nil {
|
|
panic("mujica: RadioGroup needs a value")
|
|
}
|
|
if len(items) == 0 {
|
|
panic("mujica: RadioGroup needs items")
|
|
}
|
|
seen := make(map[T]bool, len(items))
|
|
for _, it := range items {
|
|
if seen[it.Value] {
|
|
panic("mujica: RadioGroup has a duplicate value")
|
|
}
|
|
seen[it.Value] = true
|
|
}
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
var r ChoiceResult
|
|
// Disabled on a wrapper blocks the bases' clicks before they choose.
|
|
wrap := ui.Box(c).Shrink(0).Disabled(opts.Disabled)
|
|
// Keys by first appearance of each value: fmt keys would merge equal-looking values.
|
|
known := ui.Local(wrap, "values", func() []T { return nil })
|
|
wrap.Children(func() {
|
|
r.Element = ui.RadioGroup(c, func() {
|
|
for _, it := range items {
|
|
key := slices.Index(*known, it.Value)
|
|
if key < 0 {
|
|
key = len(*known)
|
|
*known = append(*known, it.Value)
|
|
}
|
|
if radioGroupItem(c, k, d, value, it, key) {
|
|
r.changed = true
|
|
slog.Debug("mujica: radio_group choose", "label", it.Label)
|
|
}
|
|
}
|
|
}).Label(opts.Label).Gap(0)
|
|
})
|
|
if opts.Horizontal {
|
|
r.Element.Row().Gap(d.Unit() * 6)
|
|
}
|
|
if wrap.IsDisabled() && wrap.FocusWithin() {
|
|
wrap.FocusRing(false).Focus()
|
|
}
|
|
return r
|
|
}
|
|
|
|
// radioGroupItem builds one radio and reports whether it was chosen.
|
|
func radioGroupItem[T comparable](c *ui.Context, k theme.Tokens, d theme.Density, value *T, it RadioItem[T], key int) bool {
|
|
// Keyed by value so focus follows the item, not its position.
|
|
var row *ui.Element
|
|
item := ui.Box(c).Key(key).Disabled(it.Disabled)
|
|
item.Children(func() {
|
|
row = ui.RadioBase(c, value, it.Value).Gap(d.Unit() * 2).MinHeight(d.ControlHeight()).FocusRing(false)
|
|
})
|
|
if item.IsDisabled() && item.FocusWithin() {
|
|
item.FocusRing(false).Focus()
|
|
}
|
|
on := *value == it.Value
|
|
hover, pressed := row.Hovered(), row.Pressed()
|
|
row.Children(func() {
|
|
bg, border := checkboxColors(k, on, hover, pressed)
|
|
dot := ui.Box(c).Size(checkboxSize, checkboxSize).Shrink(0).Radius(checkboxSize/2).
|
|
Background(bg).Border(theme.BorderWidth, border)
|
|
dot.Transition(colorFade)
|
|
dot.DrawOver(func(p *ui.Painter, r ui.Rect) {
|
|
if on {
|
|
s := r.W * 0.375
|
|
p.Fill(ui.Rect{X: r.X + (r.W-s)/2, Y: r.Y + (r.H-s)/2, W: s, H: s}, k.OnAccent, s/2)
|
|
}
|
|
if row.FocusVisible() {
|
|
internal.FocusRing(p, r, checkboxSize/2, k.Focus)
|
|
}
|
|
})
|
|
ui.Text(c, it.Label)
|
|
})
|
|
return row.Changed()
|
|
}
|