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.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 23:56:10 -04:00
1 parent 5c3c883b7d
commit a2f2796455
38 files changed
+2651 -5

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
package datetime_test
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/datetime"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestCalendarMovesByKeysAndPicksByClick(t *testing.T) {
date := time.Date(2026, 1, 31, 8, 15, 0, 0, time.UTC)
picks := 0
tt := view(400, 300, newClock(), false, func(c *ui.Context) {
if datetime.Calendar(c, "Date", &date, datetime.CalendarOptions{}).Picked {
picks++
}
})
r := find(t, tt, "Date")
// The grid sits under the header and the day names.
grid := ui.Rect{X: r.X, Y: r.Y + r.H - 6*32, W: 7 * 32, H: 6 * 32}
tt.ClickAt(grid.X+3*32+4, grid.Y+4)
if day(date) != "2025-12-31" || picks != 1 {
t.Fatalf("a click on the first row's Wednesday chose %s", day(date))
}
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyPageDown)
if day(date) != "2026-02-01" {
t.Fatalf("Right, Page Down gave %s", day(date))
}
date = time.Date(2026, 1, 31, 8, 15, 0, 0, time.UTC)
tt.Frame()
tt.Key(0, ui.KeyPageDown)
if day(date) != "2026-02-28" {
t.Fatalf("Page Down from January 31 gave %s", day(date))
}
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyHome)
if day(date) != "2026-03-01" || date.Hour() != 8 {
t.Fatalf("Page Down, Down, Home gave %s %d:00", day(date), date.Hour())
}
tt.Key(0, ui.KeyEnd)
tt.Key(0, ui.KeyEnter)
if day(date) != "2026-03-31" || picks != 2 {
t.Fatalf("End, Enter gave %s, %d picks", day(date), picks)
}
keeps(t, tt.Image())
}
func TestCalendarMarksTodayAndTheChosenDay(t *testing.T) {
date := time.Date(2026, 10, 14, 0, 0, 0, 0, time.UTC)
tt := view(400, 300, newClock(), false, func(c *ui.Context) {
datetime.Calendar(c, "Date", &date, datetime.CalendarOptions{})
})
r := find(t, tt, "Date")
top := r.Y + r.H - 6*32
// October 2026 starts Thursday: the 11th is row 2.
today := ui.Rect{X: r.X + 6, Y: top + 2*32 + 28, W: 20, H: 2}
if n := count(t, tt.Image(), today, pixel.Accent); n == 0 {
t.Fatal("today is not marked")
}
chosen := ui.Rect{X: r.X + 3*32, Y: top + 2*32, W: 32, H: 32}
if n := count(t, tt.Image(), chosen, pixel.Shade); n == 0 {
t.Fatal("the chosen day is not sunken")
}
}
func TestOffCalendarStaysPut(t *testing.T) {
for _, inert := range []bool{false, true} {
date := time.Date(2026, 10, 14, 0, 0, 0, 0, time.UTC)
tt := view(400, 300, newClock(), inert, func(c *ui.Context) {
datetime.Calendar(c, "Date", &date, datetime.CalendarOptions{Disabled: !inert})
})
r := find(t, tt, "Date")
tt.ClickAt(r.X+4, r.Y+r.H-4)
tt.Key(0, ui.KeyRight)
click(t, tt, "NEXT")
if day(date) != "2026-10-14" {
t.Fatalf("inert %v: an off calendar moved to %s", inert, day(date))
}
}
}
func TestDateInputOpensACalendar(t *testing.T) {
date := time.Date(2026, 10, 11, 0, 0, 0, 0, time.UTC)
disabled, changes := false, 0
tt := view(400, 400, newClock(), false, func(c *ui.Context) {
if datetime.DateInput(c, "Birthday", &date, datetime.DateInputOptions{Disabled: disabled}).Changed {
changes++
}
})
click(t, tt, "Birthday")
if !shows(tt, "OCT 2026") {
t.Fatalf("no calendar: %q", tt.Texts())
}
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyEnter)
if day(date) != "2026-10-12" || shows(tt, "OCT 2026") || changes != 1 {
t.Fatalf("Right, Enter gave %s", day(date))
}
click(t, tt, "Birthday")
disabled = true
tt.Frame()
if shows(tt, "OCT 2026") {
t.Fatal("the calendar stayed open as its field turned off")
}
keeps(t, tt.Image())
}
func TestTimeInputTypesAndSteps(t *testing.T) {
tm := time.Date(2026, 10, 11, 0, 0, 0, 0, time.UTC)
tt := view(300, 100, newClock(), false, func(c *ui.Context) {
datetime.TimeInput(c, "Wake", &tm, datetime.TimeInputOptions{})
})
click(t, tt, "Wake")
tt.Key(0, ui.KeyLeft)
for _, k := range []ui.Key{ui.Key0, ui.Key7, ui.Key3, ui.Key0} {
tt.Key(0, k)
}
if got := tm.Format("15:04"); got != "07:30" {
t.Fatalf("0730 typed %s", got)
}
tt.Key(0, ui.KeyUp)
tt.Key(0, ui.KeyLeft)
tt.Key(0, ui.KeyDown)
if got := tm.Format("15:04"); got != "06:31" || day(tm) != "2026-10-11" {
t.Fatalf("Up, Left, Down gave %s", got)
}
tt.Key(0, ui.Key2)
tt.Key(0, ui.Key9)
if got := tm.Format("15:04"); got != "09:31" {
t.Fatalf("29 on hours gave %s", got)
}
keeps(t, tt.Image())
}
func TestDateRangePicksAndDithersBetween(t *testing.T) {
start := time.Date(2026, 10, 11, 0, 0, 0, 0, time.UTC)
end := start
tt := view(400, 300, newClock(), false, func(c *ui.Context) {
datetime.DateRange(c, "Trip", &start, &end, datetime.DateRangeOptions{})
})
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyEnter)
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyEnter)
if day(start) != "2026-10-11" || day(end) != "2026-10-14" {
t.Fatalf("Enter, 3×Right, Enter gave %s to %s", day(start), day(end))
}
r := find(t, tt, "Trip")
top := r.Y + r.H - 6*32
between := ui.Rect{X: r.X + 32, Y: top + 2*32, W: 32, H: 32}
if n := count(t, tt.Image(), between, pixel.Accent); n == 0 {
t.Fatal("the days between are not dithered")
}
tt.Key(0, ui.KeyEnter)
tt.Key(0, ui.KeyUp)
tt.Key(0, ui.KeyEnter)
if day(start) != "2026-10-07" || day(end) != "2026-10-14" {
t.Fatalf("a start, then an end before it, gave %s to %s", day(start), day(end))
}
}
func TestMonthAndYearPickers(t *testing.T) {
month, year := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), 2026
tt := view(400, 300, newClock(), false, func(c *ui.Context) {
datetime.MonthPicker(c, "Month", &month, datetime.MonthPickerOptions{})
datetime.YearPicker(c, "Year", &year, datetime.YearPickerOptions{})
})
if !shows(tt, "2026") || !shows(tt, "2020–2029") {
t.Fatalf("texts %q", tt.Texts())
}
r := find(t, tt, "Month")
tt.ClickAt(r.X+16, r.Y-20)
tt.ClickAt(r.X+2*r.W/4+4, r.Y+4)
if month.Format("2006-01") != "2025-03" {
t.Fatalf("◀ then MAR gave %s", month.Format("2006-01"))
}
y := find(t, tt, "Year")
tt.ClickAt(y.X+3*56+4, y.Y+32+4)
if year != 2028 {
t.Fatalf("a click on the second row's fourth year gave %d", year)
}
keeps(t, tt.Image())
}
func TestDateInputParksFocusAsItTurnsOff(t *testing.T) {
date := raid
disabled := false
tt := view(400, 400, newClock(), false, func(c *ui.Context) {
datetime.DateInput(c, "Due", &date, datetime.DateInputOptions{Disabled: disabled})
pixel.Text(c, "elsewhere", pixel.TextOptions{})
})
click(t, tt, "Due")
disabled = true
tt.Frame()
// Focused counts containers: root focus would light "elsewhere" too.
if tt.Focused("Due") && !tt.Focused("elsewhere") {
t.Fatal("the focus came back to the field as it turned off")
}
}
func TestDateInputGetsItsFocusBackAfterAVeil(t *testing.T) {
date, inert := raid, false
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: newClock().now, Inert: inert})
datetime.DateInput(c, "Due", &date, datetime.DateInputOptions{})
pixel.Text(c, "elsewhere", pixel.TextOptions{})
}, 400, 400)
click(t, tt, "Due")
inert = true
tt.Frame()
inert = false
tt.Frame()
if !tt.Focused("Due") || tt.Focused("elsewhere") {
t.Fatal("the field did not get its focus back after the veil")
}
tt.Key(0, ui.KeyEnter)
if !shows(tt, "OCT 2026") {
t.Fatal("Enter did not reopen the calendar")
}
}