Files
MujicaUI/checkbox.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

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
}