MyGo v0.5.5 returns ui.Element as a checked value handle instead of a
pointer. Migrated with `mygo migrate-ui`, then fixed what the codemod
leaves to review:
- Element nil checks become .Valid(); absent elements are ui.Element{}.
- Input handlers and paint callbacks run outside the build pass, where
elements expire and Context methods are inert: read IsDisabled into a
local during the build, keep a ui.Handle for Focus, use c.Services()
for clipboard access, create ui.Local state during the build.
- Add core.AutoFocus: v0.5.5 records an overlay's opener after its
content builds, so same-frame autofocus lost focus restore on close.
It focuses one frame later; cascader rows defer their focus likewise.
- core.OverlayLayer identifies build passes by the root element, not
c.Now(), which repeats across passes and under the Tester clock.
- Context menus survive the opening press counted as PressedOutside;
split button tooltip no longer reorders its open menu; disabled
menubar rows pass presses to the panel; Form defers blur-started
async checks so the growing message no longer moves Submit away
from the release; Combobox drops its disabled copy now that MyGo
blocks typing under a disabled wrapper (canary flipped to a guard).
- The Tester clock is virtual: tests use Advance/Now instead of
time.Sleep/time.Now and read element state inside the view. The web
mirror advances the clock to wall time before each frame and input.
- AGENTS.md and docs describe the value Element API and the new pin.
Snapshot goldens are darwin-only and were not re-verified here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
152 lines
4.3 KiB
Go
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
|
|
}
|