Files
MujicaUI/input/checkbox.go
T

152 lines
4.3 KiB
Go

package input
import (
"log/slog"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/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 := core.Tokens(c)
d := core.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(core.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(core.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) core.ChoiceResult {
if len(items) == 0 {
panic("mujica: CheckboxGroup needs items")
}
d := core.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 core.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 = core.NewChoiceResult(r.Element, 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 = core.NewChoiceResult(r.Element, true)
}
}
})
})
return r
}