Files
8bitMyGoUI/datetime/timeinput.go
T
ZacharyZhang-NY a2f2796455 Add datetime components 196-215; Kit.At; inert stepper arrows
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.
2026-10-11 23:56:10 -04:00

122 lines
2.7 KiB
Go

package datetime
import (
"fmt"
"log/slog"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
type TimeInputOptions struct {
Disabled bool
}
type TimeInputResult struct {
Element *ui.Element
Changed bool
}
type timeKey struct{}
// timing is the segment edited, and a first digit typed.
type timing struct {
seg, first int
}
// TimeInput edits HH:MM; Up and Down step, digits type.
func TimeInput(c *ui.Context, text string, tm *time.Time, o TimeInputOptions) TimeInputResult {
k := pixel.K(c)
old := *tm
shell, base, st := pixel.Shell(c, func() *ui.Element {
return ui.Box(c).Focusable().Role(ui.RoleTextField).Label(text).Disabled(o.Disabled)
})
s := ui.Local(base, timeKey{}, func() timing { return timing{first: -1} })
h, m := tm.Hour(), tm.Minute()
set := func(hour, minute int) {
*tm = time.Date(tm.Year(), tm.Month(), tm.Day(), hour, minute, 0, 0, tm.Location())
h, m = hour, minute
}
step := func(d int) {
if s.seg == 0 {
set((h+d+24)%24, m)
} else {
set(h, (m+d+60)%60)
}
s.first = -1
}
if base.Shortcut(0, ui.KeyUp) {
step(1)
}
if base.Shortcut(0, ui.KeyDown) {
step(-1)
}
for _, mv := range []struct {
key ui.Key
seg int
}{{ui.KeyLeft, 0}, {ui.KeyRight, 1}} {
if base.Shortcut(0, mv.key) {
s.seg, s.first = mv.seg, -1
}
}
for d := range 10 {
if !base.Shortcut(0, ui.Key0+ui.Key(d)) {
continue
}
top := 23
if s.seg == 1 {
top = 59
}
v := d
if s.first >= 0 && s.first*10+d <= top {
v = s.first*10 + d
}
if s.seg == 0 {
set(v, m)
} else {
set(h, v)
}
// A second digit ends a segment; hours pass to minutes.
if s.first >= 0 {
s.first, s.seg = -1, 1
} else {
s.first = d
}
}
// Enter and Space are taken, so clicks come from pointers.
base.Shortcut(0, ui.KeyEnter)
base.Shortcut(0, ui.KeySpace)
if base.Clicked() {
x, _, _ := base.PointerPosition()
s.seg, s.first = 0, -1
if int(x/k.Dot()) >= 4+pixel.Measure("00:") {
s.seg = 1
}
}
shown := fmt.Sprintf("%02d:%02d", h, m)
base.Description(shown)
seg := s.seg
shell.Size(k.D(pixel.Measure("00:00")+10), k.D(pixel.LineHeight+6)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Frame(0, 0, g.W, g.H, pixel.Sunken, pixel.Well)
hh, mm, at := shown[:2], shown[3:], 5
for i, part := range []string{hh, ":", mm} {
ink, w := pixel.Ink, pixel.Measure(part)
if st.Focus && (i == 0 && seg == 0 || i == 2 && seg == 1) {
g.Rect(at-1, 2, w+2, g.H-4, pixel.Accent)
ink = pixel.AccentInk
}
g.Text(at, 3, part, ink, 1)
at += w
}
g.Finish(st)
})
res := TimeInputResult{Element: shell, Changed: !tm.Equal(old)}
if res.Changed {
slog.Debug("8bit: time input", "label", text, "time", shown)
}
return res
}