Date & Time: components 051-058 (wip)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 12:22:05 -04:00
1 parent cfc7ab023d
commit e61e38b656
45 files changed
+3943 -1

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
package mujica
import (
"fmt"
"log/slog"
"github.com/egoist/mygo/ui"
"mujicaui/theme"
)
// TimeRange is a start and an end time of day. End before Start means it ends the next day.
type TimeRange struct {
Start, End TimeOfDay
}
// Overnight reports whether r crosses midnight.
func (r TimeRange) Overnight() bool { return r.End.Compare(r.Start) < 0 }
// TimeRangePickerOptions configure a TimeRangePicker.
type TimeRangePickerOptions struct {
Seconds bool
Hour12 bool
// MinuteStep and SecondStep must divide 60; zero means 1.
MinuteStep, SecondStep int
// Min and Max bound both ends; a zero Max means 23:59:59.
Min, Max TimeOfDay
// AllowOvernight accepts an end before the start, meaning the next day.
AllowOvernight bool
Disabled bool
// Label names the pair for assistive technology.
Label string
}
// TimeRangePicker edits a start and an end time. It writes *value only when both ends are valid
// and the end follows the start, or crosses midnight with AllowOvernight.
func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOptions) DateTimeResult {
cfg := timePickerConfig("TimeRangePicker", TimePickerOptions{Seconds: opts.Seconds, Hour12: opts.Hour12,
MinuteStep: opts.MinuteStep, SecondStep: opts.SecondStep, Min: opts.Min, Max: opts.Max})
if !value.Start.Valid() || !value.End.Valid() {
panic(fmt.Sprintf("mujica: TimeRangePicker value %v is not a pair of times", *value))
}
if value.Overnight() && !opts.AllowOvernight {
panic(fmt.Sprintf("mujica: TimeRangePicker value %v crosses midnight without AllowOvernight", *value))
}
k := Tokens(c)
d := Density(c)
col := ui.Column(c).Gap(d.Unit()).AlignItems(ui.Start).Disabled(opts.Disabled).Role(ui.RoleGroup)
if opts.Label != "" {
col.Label(opts.Label)
}
draft := ui.Local(col, "time-range-draft", func() TimeRange { return *value })
seen := ui.Local(col, "time-range-seen", func() TimeRange { return *value })
problem := ui.Local(col, "time-range-error", func() string { return "" })
if *seen != *value {
*draft, *seen, *problem = *value, *value, ""
}
changed := false
col.Children(func() {
edited := false
ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
start, end := cfg, cfg
start.label, end.label = msg(c, "time_range_picker.start"), msg(c, "time_range_picker.end")
start.invalid, end.invalid = *problem != "", *problem != ""
if _, e := timeEditor(c, &draft.Start, start); e {
edited = true
}
ui.Text(c, "–").TextColor(k.TextMuted).Role(ui.RoleNone)
if _, e := timeEditor(c, &draft.End, end); e {
edited = true
}
if draft.Overnight() && opts.AllowOvernight {
ui.Text(c, msg(c, "time_range_picker.next_day")).FontSize(theme.CaptionSize).TextColor(k.AccentText)
}
})
if edited {
*problem = timeRangeProblem(c, *draft, cfg, opts.AllowOvernight)
if *problem == "" && *draft != *value {
slog.Debug("mujica: time_range_picker change", "from", *value, "to", *draft)
*value, *seen, changed = *draft, *draft, true
}
}
datetimeError(c, *problem)
})
return DateTimeResult{Element: col, changed: changed}
}
// timeRangeProblem explains why r cannot be written, or returns "".
func timeRangeProblem(c *ui.Context, r TimeRange, cfg timeConfig, overnight bool) string {
for _, t := range []TimeOfDay{r.Start, r.End} {
if p := timePickerProblem(c, t, cfg); p != "" {
return p
}
}
switch {
case r.Start == r.End:
return msg(c, "time_range_picker.same")
case r.Overnight() && !overnight:
return msg(c, "time_range_picker.order")
}
return ""
}