Split into category packages; module github.com/ZacharyZhang-NY/MujicaUI

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 16:18:48 -04:00
1 parent 5a7e921c7e
commit 22b53e9e9e
1822 files changed
+24568 -21314

No files matched your search

+201
View File
@@ -0,0 +1,201 @@
package datetime
import (
"fmt"
"log/slog"
"github.com/egoist/mygo/ui"
"github.com/ZacharyZhang-NY/MujicaUI/core"
"github.com/ZacharyZhang-NY/MujicaUI/input"
"github.com/ZacharyZhang-NY/MujicaUI/theme"
)
// DateRange is an inclusive span of days. The zero value means no range.
type DateRange struct {
Start, End core.Date
}
// IsZero reports whether r is the empty range.
func (r DateRange) IsZero() bool { return r == DateRange{} }
// Days returns how many days r covers, counting both ends.
func (r DateRange) Days() int {
if r.Start.IsZero() || r.End.IsZero() {
return 0
}
return int(r.End.Civil().Sub(r.Start.Civil()).Hours()/24) + 1
}
// DateRangePickerOptions configure a DateRangePicker.
type DateRangePickerOptions struct {
// Layout is the Go time layout shown; empty uses 2006-01-02.
Layout string
// Min and Max bound both ends; zero leaves a side open.
Min, Max core.Date
// DisableDate marks days that neither end nor the span may include.
DisableDate func(core.Date) bool
WeekStart WeekStart
// Today is marked in the calendar; zero uses the system clock.
Today core.Date
Disabled bool
// ReadOnly keeps the field focusable and copyable but blocks the popup.
ReadOnly bool
// Label names the field for assistive technology.
Label string
}
// DateRangePicker picks start and end; Confirm writes *value.
func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOptions) DateTimeResult {
core.DatetimeCheckDate("DateRangePicker Start", value.Start)
core.DatetimeCheckDate("DateRangePicker End", value.End)
if value.Start.IsZero() != value.End.IsZero() || !value.IsZero() && value.End.Compare(value.Start) < 0 {
panic(fmt.Sprintf("mujica: DateRangePicker value %v..%v is not a range", value.Start, value.End))
}
core.DatetimeCheckBounds("DateRangePicker", opts.Min, opts.Max)
layout := opts.Layout
if layout == "" {
layout = "2006-01-02"
}
d := core.Density(c)
k := core.Tokens(c)
today := core.DatetimeToday(c, opts.Today)
enabled := calendarEnabled(opts.Min, opts.Max, opts.DisableDate)
col := ui.Column(c).Width(d.ControlHeight() * 9).Gap(d.Unit()).AlignItems(ui.Stretch).Disabled(opts.Disabled)
open := ui.Local(col, "date-range-open", func() bool { return false })
wasOpen := ui.Local(col, "date-range-was-open", func() bool { return false })
draft := ui.Local(col, "date-range-draft", func() DateRange { return DateRange{} })
cursor := ui.Local(col, "date-range-cursor", func() core.Date { return core.Date{} })
hover := ui.Local(col, "date-range-hover", func() core.Date { return core.Date{} })
problem := ui.Local(col, "date-range-error", func() string { return "" })
changed, canceled, done := false, false, false
finish := func(to DateRange, how string) {
if to != *value {
slog.Debug("mujica: date_range_picker change", "from", *value, "to", to, "by", how)
*value = to
changed = true
}
*open, done = false, true
}
label := opts.Label
if label == "" {
label = core.Msg(c, "date_range_picker.label")
}
text := ""
if !value.IsZero() {
text = value.Start.Format(layout) + " – " + value.End.Format(layout)
}
var field *ui.Element
col.Children(func() {
field = core.DatetimeField(c, ui.Row(c), *problem != "", opts.ReadOnly)
field.Children(func() {
t := core.DatetimeTrigger(c, text, core.Msg(c, "date_range_picker.placeholder"), "calendar").Label(label).Description(text).Error(*problem)
if !opts.ReadOnly && (t.Clicked() || !*open && t.Shortcut(0, ui.KeyDown)) {
*open = !*open
if *open {
*draft, *problem, *hover = *value, "", core.Date{}
*cursor = value.Start
if cursor.IsZero() {
*cursor = today
}
slog.Debug("mujica: date_range_picker open")
}
}
})
core.DatetimePopup(c, field, open, opts.ReadOnly, func() {
ui.Text(c, dateRangeSummary(c, *draft, layout)).TextColor(k.TextMuted).
FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").Role(ui.RoleStatus)
end := draft.End
if end.IsZero() && !draft.Start.IsZero() {
end = *hover
if end.IsZero() {
end = *cursor
}
}
picked, hovered := calendarMonth(c, cursor, calendarConfig{
chosen: func(x core.Date) bool { return x == draft.Start || x == draft.End },
inRange: func(x core.Date) bool { return !draft.Start.IsZero() && x.Within(draft.Start, end) },
enabled: enabled,
today: today,
weekStart: opts.WeekStart,
autoFocus: true,
})
if hovered != *hover {
*hover = hovered
c.Invalidate()
}
if !picked.IsZero() {
*problem = dateRangePick(c, draft, picked, enabled, layout)
}
core.DatetimeError(c, *problem)
core.DatetimeFooter(c, func() {
if input.Button(c, core.Msg(c, "calendar.clear"), input.ButtonOptions{Variant: input.Ghost, Disabled: value.IsZero() && draft.IsZero()}).Clicked() {
finish(DateRange{}, "clear")
}
if input.Button(c, core.Msg(c, "calendar.cancel"), input.ButtonOptions{Variant: input.Secondary}).Clicked() {
*open = false
}
ready := !draft.Start.IsZero() && !draft.End.IsZero()
if input.Button(c, core.Msg(c, "calendar.confirm"), input.ButtonOptions{Disabled: !ready}).Clicked() {
if p := dateRangeProblem(c, *draft, opts.Min, opts.Max, enabled, layout); p != "" {
slog.Debug("mujica: date_range_picker invalid confirm", "draft", *draft)
*problem = p
} else {
finish(*draft, "confirm")
}
}
})
})
})
if *wasOpen && !*open && !done {
slog.Debug("mujica: date_range_picker cancel")
canceled = true
}
*wasOpen = *open
if !*open {
*problem = ""
}
return DateTimeResult{Element: col, changed: changed, canceled: canceled}
}
// dateRangeProblem checks a whole draft against the current bounds and filter.
func dateRangeProblem(c *ui.Context, r DateRange, lo, hi core.Date, enabled func(core.Date) bool, layout string) string {
format := func(d core.Date) string { return d.Format(layout) }
for _, d := range []core.Date{r.Start, r.End} {
if p := datePickerProblem(c, d, lo, hi, enabled, format); p != "" {
return p
}
}
for x := r.Start; x.Compare(r.End) <= 0; x = x.AddDays(1) {
if !enabled(x) {
return fmt.Sprintf(core.Msg(c, "date_range_picker.blocked"), format(x))
}
}
return ""
}
// dateRangePick applies a picked day to the draft and returns any problem.
func dateRangePick(c *ui.Context, draft *DateRange, day core.Date, enabled func(core.Date) bool, layout string) string {
if draft.Start.IsZero() || !draft.End.IsZero() || day.Compare(draft.Start) < 0 {
*draft = DateRange{Start: day}
return ""
}
for x := draft.Start; x.Compare(day) <= 0; x = x.AddDays(1) {
if !enabled(x) {
return fmt.Sprintf(core.Msg(c, "date_range_picker.blocked"), x.Format(layout))
}
}
draft.End = day
return ""
}
// dateRangeSummary describes the draft: what to pick next, or the span chosen.
func dateRangeSummary(c *ui.Context, r DateRange, layout string) string {
switch {
case r.Start.IsZero():
return core.Msg(c, "date_range_picker.pick_start")
case r.End.IsZero():
return fmt.Sprintf(core.Msg(c, "date_range_picker.pick_end"), r.Start.Format(layout))
}
return fmt.Sprintf(core.Msg(c, "date_range_picker.summary"), r.Start.Format(layout), r.End.Format(layout), r.Days())
}