Files
ZacharyZhang-NYandClaude Opus 5.5 03e1f91706 Upgrade MyGo to v0.5.5
Elements are now pass-scoped value handles: tests that read bounds
after a frame bind a ui.Handle. pixel tells frames apart by the build
pass's root, since the Tester clock no longer moves on its own.

Fix the regressions the upgrade brought:
- Clipboard reads in input handlers go through c.Services().
- MyGo notes an overlay's opener after building it, so AutoFocus
  inside lost it; pixel.AutoFocus focuses through a Handle, and
  archtest bans the raw call. Dialogs hand the focus back again.
- Elements around a press stay hovered: Tooltip tells a press-given
  focus by focus arriving under the pointer.
- The web host moves the Tester clock with real time, at most 500 ms
  a call, and gives components the real clock.

The gallery's source index also accepts CRLF checkouts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 01:31:08 -04:00

98 lines
2.6 KiB
Go

package text
import (
"fmt"
"math"
"strconv"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
type OdometerOptions struct {
// Digits is zero for as many as value has.
Digits int
Slot pixel.Slot
}
type odoKey struct{}
type odoState struct {
shown float64
last int
}
const odoCell = 10
// Odometer rolls its digits toward value, step by step.
func Odometer(c *ui.Context, value int, o OdometerOptions) ui.Element {
if value < 0 {
panic(fmt.Sprintf("text: Odometer value %d is negative", value))
}
k := pixel.K(c)
// Counted as text, so the largest int needs no float.
digits := pixel.Or("Odometer Digits", o.Digits, len(strconv.Itoa(value)))
if len(strconv.Itoa(value)) > digits {
panic(fmt.Sprintf("text: Odometer value %d needs more than %d digits", value, digits))
}
h := pixel.LineHeight + 2
e := ui.Box(c).Size(k.D(digits*odoCell), k.D(h)).Label(strconv.Itoa(value)).Role(ui.RoleText)
st := ui.Local(e, odoKey{}, func() odoState { return odoState{shown: float64(value), last: -1} })
target := float64(value)
switch {
case k.Reduce() || st.shown == target:
st.shown, st.last = target, -1
default:
// S5 caps steps at 15 fps; two rolls a step.
tick := k.Tick(c, 15)
if st.last >= 0 {
for range 2 * (tick - st.last) {
st.shown = roll(st.shown, target)
}
}
st.last = tick
}
// At rest digits come from the int; floats would round.
shown, settled, exact := st.shown, st.last < 0, fmt.Sprintf("%0*d", digits, value)
return e.Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
for i := range digits {
x := (digits - 1 - i) * odoCell
d, frac := int(exact[digits-1-i]-'0'), 0.0
if !settled {
d, frac = wheel(shown, i)
}
g.Frame(x, 0, odoCell-1, h, pixel.Sunken, pixel.Well)
g.Clip(x+2, 2, odoCell-5, h-4, func() {
y := 1 - int(frac*float64(h))
g.Text(x+2, y, strconv.Itoa(d), o.Slot, 1)
g.Text(x+2, y+h, strconv.Itoa((d+1)%10), o.Slot, 1)
})
}
})
}
// roll moves shown a step toward target, faster when far.
func roll(shown, target float64) float64 {
diff := target - shown
stride := max(0.25, math.Abs(diff)/6)
// Near 2^53 a step may not move; then land.
next := shown + math.Copysign(stride, diff)
if math.Abs(diff) <= stride || next == shown {
return target
}
return next
}
// wheel is digit i of shown and its roll.
func wheel(shown float64, i int) (d int, frac float64) {
place := math.Pow10(i)
v := shown / place
d = int(math.Floor(v)) % 10
if lower := math.Mod(shown, place); lower > place-1 {
frac = lower - (place - 1)
}
return d, frac
}