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.
151 lines
4.1 KiB
Go
151 lines
4.1 KiB
Go
package mujica
|
|
|
|
import (
|
|
"log/slog"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// CheckboxOptions configure a Checkbox.
|
|
type CheckboxOptions struct {
|
|
// Indeterminate shows the mixed mark; a click then checks the box.
|
|
Indeterminate bool
|
|
Disabled bool
|
|
}
|
|
|
|
// checkboxSize is the box edge in DIPs.
|
|
const checkboxSize float32 = 16
|
|
|
|
// Checkbox toggles *value; a click on the box or its label toggles, as does Space.
|
|
func Checkbox(c *ui.Context, value *bool, label string, opts CheckboxOptions) *ui.Element {
|
|
if value == nil {
|
|
panic("mujica: Checkbox needs a value")
|
|
}
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
// Disabled on a wrapper blocks the base's click before it toggles.
|
|
var row *ui.Element
|
|
wrap := ui.Row(c).Shrink(0).Disabled(opts.Disabled)
|
|
wrap.Children(func() {
|
|
row = ui.CheckboxBase(c, value).Gap(d.Unit() * 2).MinHeight(d.ControlHeight()).FocusRing(false)
|
|
})
|
|
if wrap.IsDisabled() && wrap.FocusWithin() {
|
|
wrap.FocusRing(false).Focus()
|
|
}
|
|
if row.Changed() && opts.Indeterminate && !*value {
|
|
// From mixed, a click always checks.
|
|
*value = true
|
|
}
|
|
if opts.Indeterminate {
|
|
row.Description(msg(c, "checkbox.mixed"))
|
|
}
|
|
on, mixed := *value, opts.Indeterminate
|
|
hover, pressed := row.Hovered(), row.Pressed()
|
|
row.Children(func() {
|
|
bg, border := checkboxColors(k, on || mixed, hover, pressed)
|
|
box := ui.Box(c).Size(checkboxSize, checkboxSize).Shrink(0).Radius(theme.ControlRadius).
|
|
Background(bg).Border(theme.BorderWidth, border)
|
|
box.Transition(colorFade)
|
|
box.DrawOver(func(p *ui.Painter, r ui.Rect) {
|
|
switch {
|
|
case mixed:
|
|
p.Fill(ui.Rect{X: r.X + r.W*0.25, Y: r.Y + r.H/2 - 1, W: r.W * 0.5, H: 2}, k.OnAccent, 1)
|
|
case on:
|
|
var path ui.Path
|
|
path.MoveTo(r.X+r.W*0.24, r.Y+r.H*0.52).LineTo(r.X+r.W*0.42, r.Y+r.H*0.7).LineTo(r.X+r.W*0.77, r.Y+r.H*0.3)
|
|
p.StrokePath(&path, 2, k.OnAccent)
|
|
}
|
|
if row.FocusVisible() {
|
|
internal.FocusRing(p, r, theme.ControlRadius, k.Focus)
|
|
}
|
|
})
|
|
if label != "" {
|
|
ui.Text(c, label)
|
|
}
|
|
})
|
|
if row.Changed() {
|
|
slog.Debug("mujica: checkbox toggle", "label", label, "checked", *value)
|
|
}
|
|
return row
|
|
}
|
|
|
|
// checkboxColors gives a check box or radio mark its fill and edge.
|
|
func checkboxColors(k theme.Tokens, on, hover, pressed bool) (bg, border ui.Color) {
|
|
switch {
|
|
case on && pressed:
|
|
return k.AccentPressed, k.Text
|
|
case on && hover:
|
|
return k.AccentHover, k.Text
|
|
case on:
|
|
return k.Accent, k.AccentText
|
|
case pressed:
|
|
return k.Selection, k.AccentText
|
|
case hover:
|
|
return k.SurfaceHover, k.AccentText
|
|
}
|
|
return k.Surface, k.ControlBorder
|
|
}
|
|
|
|
// CheckboxItem is one box of a CheckboxGroup.
|
|
type CheckboxItem struct {
|
|
Label string
|
|
Value *bool
|
|
Disabled bool
|
|
}
|
|
|
|
// CheckboxGroupOptions configure a CheckboxGroup.
|
|
type CheckboxGroupOptions struct {
|
|
Disabled bool
|
|
}
|
|
|
|
// CheckboxGroup shows a select-all box over items: checked, mixed or clear.
|
|
func CheckboxGroup(c *ui.Context, label string, items []CheckboxItem, opts CheckboxGroupOptions) ChoiceResult {
|
|
if len(items) == 0 {
|
|
panic("mujica: CheckboxGroup needs items")
|
|
}
|
|
d := Density(c)
|
|
on, enabled, enabledOn := 0, 0, 0
|
|
for _, it := range items {
|
|
if it.Value == nil {
|
|
panic("mujica: CheckboxItem needs a Value")
|
|
}
|
|
if *it.Value {
|
|
on++
|
|
}
|
|
if !it.Disabled {
|
|
enabled++
|
|
if *it.Value {
|
|
enabledOn++
|
|
}
|
|
}
|
|
}
|
|
var r ChoiceResult
|
|
r.Element = ui.Column(c).Shrink(0).Disabled(opts.Disabled || enabled == 0)
|
|
r.Element.Children(func() {
|
|
all := on == len(items)
|
|
head := Checkbox(c, &all, label, CheckboxOptions{Indeterminate: on > 0 && !all})
|
|
if head.Changed() {
|
|
// Select-all leaves disabled items alone.
|
|
target := enabledOn < enabled
|
|
for _, it := range items {
|
|
if !it.Disabled {
|
|
*it.Value = target
|
|
}
|
|
}
|
|
r.changed = true
|
|
slog.Debug("mujica: checkbox_group select_all", "label", label, "checked", target)
|
|
}
|
|
ui.Column(c).Padding(0, 0, 0, checkboxSize+d.Unit()*2).Role(ui.RoleGroup).Label(label).Children(func() {
|
|
for _, it := range items {
|
|
if Checkbox(c, it.Value, it.Label, CheckboxOptions{Disabled: it.Disabled}).Changed() {
|
|
r.changed = true
|
|
}
|
|
}
|
|
})
|
|
})
|
|
return r
|
|
}
|