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.
122 lines
2.7 KiB
Go
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
|
|
}
|