Calendars, date and time inputs, pickers, clocks, timers, a sun dial, a season wheel, an agenda and a day schedule. Kit.At rebuilds at a moment (relative times, midnight). Stepper and NumberInput arrows stop under Inert. DateInput parks a popup focus as its own.
102 lines
2.6 KiB
Go
102 lines
2.6 KiB
Go
package input
|
|
|
|
import (
|
|
"log/slog"
|
|
"math"
|
|
"strconv"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
type NumberInputOptions struct {
|
|
Min, Max float64
|
|
// Step is zero for 1.
|
|
Step float64
|
|
Width int
|
|
Disabled bool
|
|
}
|
|
|
|
type numberKey struct{}
|
|
|
|
type numberState struct {
|
|
text string
|
|
focused bool
|
|
// seen is the value last shown, to notice the caller's.
|
|
seen float64
|
|
}
|
|
|
|
type NumberInputResult = TextInputResult
|
|
|
|
// NumberInput edits *value as text; bad text reddens the edge.
|
|
func NumberInput(c *ui.Context, label string, value *float64, o NumberInputOptions) NumberInputResult {
|
|
lo, hi := bounds(o.Min, o.Max)
|
|
step := o.Step
|
|
if step <= 0 {
|
|
step = 1
|
|
}
|
|
wrap := ui.Box(c)
|
|
st := ui.Local(wrap, numberKey{}, func() numberState { return numberState{text: fmtValue(*value), seen: *value} })
|
|
var res TextInputResult
|
|
wrap.Children(func() {
|
|
k := pixel.K(c)
|
|
set := func(v float64) {
|
|
if v = max(lo, min(hi, v)); v != *value {
|
|
*value, res.Changed = v, true
|
|
}
|
|
st.text, st.seen = fmtValue(*value), *value
|
|
}
|
|
bad := func() bool {
|
|
v, err := strconv.ParseFloat(st.text, 64)
|
|
return err != nil || math.IsNaN(v) || math.IsInf(v, 0) || v < lo || v > hi
|
|
}
|
|
if *value != st.seen || !st.focused && !bad() && st.text != fmtValue(*value) {
|
|
st.text, st.seen = fmtValue(*value), *value
|
|
}
|
|
var ed pixel.Edit
|
|
ed, res.Element = field(c, &st.text, pixel.EditorOptions{Label: label, Disabled: o.Disabled}, o.Width, bad, nil, func() {
|
|
ui.Column(c).Children(func() {
|
|
if arrowBox(c, k, tinyUp, o.Disabled) {
|
|
set(*value + step)
|
|
}
|
|
if arrowBox(c, k, tinyDown, o.Disabled) {
|
|
set(*value - step)
|
|
}
|
|
})
|
|
})
|
|
if v, err := strconv.ParseFloat(st.text, 64); ed.Changed && !bad() && err == nil && v != *value {
|
|
*value, st.seen, res.Changed = v, v, true
|
|
}
|
|
st.focused = ed.Focused
|
|
if ed.Input.Shortcut(0, ui.KeyUp) {
|
|
set(*value + step)
|
|
}
|
|
if ed.Input.Shortcut(0, ui.KeyDown) {
|
|
set(*value - step)
|
|
}
|
|
res.Input, res.Submitted, res.Focused = ed.Input, ed.Submitted, ed.Focused
|
|
})
|
|
if res.Changed {
|
|
slog.Debug("8bit: number input", "label", label, "value", *value)
|
|
}
|
|
res.Element = wrap
|
|
return res
|
|
}
|
|
|
|
// arrowBox is a half-height step button the pointer presses.
|
|
func arrowBox(c *ui.Context, k pixel.Kit, sp *pixel.Sprite, disabled bool) bool {
|
|
disabled = disabled || pixel.Current(c).Inert
|
|
b := ui.Box(c).Size(k.D(10), k.D(pixel.LineHeight/2)).Role(ui.RoleNone)
|
|
down := b.Pressed() && !disabled
|
|
b.Draw(func(p *ui.Painter, r ui.Rect) {
|
|
g := k.Canvas(p, r)
|
|
dy := 0
|
|
if down {
|
|
dy = 1
|
|
}
|
|
g.Sprite((g.W-sp.W)/2, (g.H-sp.H)/2+dy, sp, 0, pixel.Faint, 1)
|
|
})
|
|
return b.Clicked() && !disabled
|
|
}
|