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>
149 lines
4.1 KiB
Go
149 lines
4.1 KiB
Go
package input
|
||
|
||
import (
|
||
"fmt"
|
||
"log/slog"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
||
)
|
||
|
||
// CurrencyInputOptions configure a CurrencyInput.
|
||
type CurrencyInputOptions struct {
|
||
// Symbol goes before the amount, as "$" or "¥"; empty shows none.
|
||
Symbol string
|
||
// Decimals is the minor unit digits; zero is 2. NoFraction makes it 0, as for JPY.
|
||
Decimals int
|
||
NoFraction bool
|
||
Label string
|
||
Disabled bool
|
||
// Error marks the value invalid and shows the message below.
|
||
Error string
|
||
}
|
||
|
||
// currencyInputDigits resolves the decimals of opts.
|
||
func currencyInputDigits(opts CurrencyInputOptions) int {
|
||
switch {
|
||
case opts.Decimals < 0 || opts.Decimals > 6:
|
||
panic(fmt.Sprintf("mujica: CurrencyInput Decimals %d outside 0..6", opts.Decimals))
|
||
case opts.NoFraction && opts.Decimals != 0:
|
||
panic("mujica: CurrencyInput NoFraction with Decimals")
|
||
case opts.NoFraction:
|
||
return 0
|
||
case opts.Decimals == 0:
|
||
return 2
|
||
}
|
||
return opts.Decimals
|
||
}
|
||
|
||
// CurrencyFormat writes minor units as an amount with thousands separators.
|
||
func CurrencyFormat(minor int64, decimals int, group bool) string {
|
||
neg := minor < 0
|
||
u := uint64(minor)
|
||
if neg {
|
||
u = -u
|
||
}
|
||
p := uint64(math.Pow10(decimals))
|
||
whole := strconv.FormatUint(u/p, 10)
|
||
if group {
|
||
var b strings.Builder
|
||
for i, r := range whole {
|
||
if i > 0 && (len(whole)-i)%3 == 0 {
|
||
b.WriteByte(',')
|
||
}
|
||
b.WriteRune(r)
|
||
}
|
||
whole = b.String()
|
||
}
|
||
s := whole
|
||
if decimals > 0 {
|
||
s += "." + fmt.Sprintf("%0*d", decimals, u%p)
|
||
}
|
||
if neg {
|
||
s = "-" + s
|
||
}
|
||
return s
|
||
}
|
||
|
||
// currencyInputParse reads an amount into minor units; reason is a message key when it fails.
|
||
func currencyInputParse(text string, decimals int) (minor int64, reason string) {
|
||
t := strings.NewReplacer(",", "", " ", "", " ", "").Replace(strings.TrimSpace(text))
|
||
neg := strings.HasPrefix(t, "-")
|
||
t = strings.TrimPrefix(t, "-")
|
||
whole, frac, dot := strings.Cut(t, ".")
|
||
if whole == "" && frac == "" || strings.Trim(whole+frac, "0123456789") != "" || dot && strings.Contains(frac, ".") {
|
||
return 0, "currency_input.invalid"
|
||
}
|
||
if len(frac) > decimals {
|
||
return 0, "currency_input.precision"
|
||
}
|
||
frac += strings.Repeat("0", decimals-len(frac))
|
||
v, err := strconv.ParseInt(whole+frac, 10, 64)
|
||
if err != nil {
|
||
return 0, "currency_input.invalid"
|
||
}
|
||
if neg {
|
||
v = -v
|
||
}
|
||
return v, ""
|
||
}
|
||
|
||
// CurrencyInput edits *value, an amount in minor units (cents); it groups thousands while not focused.
|
||
func CurrencyInput(c *ui.Context, value *int64, opts CurrencyInputOptions) InputResult {
|
||
if value == nil {
|
||
panic("mujica: CurrencyInput needs a value")
|
||
}
|
||
dec := currencyInputDigits(opts)
|
||
k := core.Tokens(c)
|
||
var r InputResult
|
||
look := &textInputLook{disabled: opts.Disabled, err: opts.Error}
|
||
r.Element, r.Input = textInputFrame(c, look, func(frame ui.Element) ui.Element {
|
||
if opts.Symbol != "" {
|
||
ui.Text(c, opts.Symbol).TextColor(k.TextMuted).SingleLine()
|
||
}
|
||
text := ui.Local(frame, "text", func() string { return CurrencyFormat(*value, dec, false) })
|
||
bad := ui.Local(frame, "bad", func() string { return "" })
|
||
seen := ui.Local(frame, "seen", func() int64 { return *value })
|
||
was := ui.Local(frame, "was", func() bool { return false })
|
||
if *seen != *value {
|
||
*bad = ""
|
||
}
|
||
// Grouped while resting; plain digits once focused, for editing.
|
||
focused := frame.FocusWithin()
|
||
switch {
|
||
case *bad != "":
|
||
case !focused:
|
||
*text = CurrencyFormat(*value, dec, true)
|
||
case !*was || *seen != *value:
|
||
*text = CurrencyFormat(*value, dec, false)
|
||
}
|
||
*was = focused
|
||
in := TextInputEditor(c, frame, text, look.disabled, ui.TextInputBase).Grow(1).MinWidth(core.Density(c).Unit() * 16).Label(opts.Label)
|
||
if in.Changed() && !look.disabled {
|
||
v, why := currencyInputParse(*text, dec)
|
||
*bad = ""
|
||
switch {
|
||
case why == "currency_input.precision":
|
||
*bad = fmt.Sprintf(core.Msg(c, why), dec)
|
||
case why != "":
|
||
*bad = core.Msg(c, why)
|
||
case v != *value:
|
||
*value, r.changed = v, true
|
||
}
|
||
}
|
||
*seen = *value
|
||
if look.err == "" {
|
||
look.err = *bad
|
||
}
|
||
return in
|
||
}, nil)
|
||
if r.changed {
|
||
slog.Debug("mujica: currency_input change", "minor", *value)
|
||
}
|
||
return r
|
||
}
|