From 4a0c3a789f655e6b54e717aa2eb55600b50f25dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 13:15:30 -0400 Subject: [PATCH 1/2] wip r3 --- calendar.go | 3 + datetime.go | 6 ++ datetime_grid.go | 3 + datetime_review2_test.go | 19 ++++++ testdata/snapshots/darwin/calendar-dark.png | Bin 17972 -> 18048 bytes testdata/snapshots/darwin/calendar-light.png | Bin 16715 -> 16799 bytes time_picker.go | 58 ++++++++++++------- time_picker_popup.go | 11 ++-- time_range_picker.go | 13 +++-- 9 files changed, 84 insertions(+), 29 deletions(-) diff --git a/calendar.go b/calendar.go index 7c96a59..2a8e691 100644 --- a/calendar.go +++ b/calendar.go @@ -245,6 +245,9 @@ func calendarDay(c *ui.Context, day, cur Date, cfg calendarConfig, size float32, } ring := cell.FocusVisible() || cell.Focused() && keyed cell.DrawOver(func(p *ui.Painter, r ui.Rect) { + if chosen { + datetimeSelectedMark(p, r, k.OnAccent) + } if day == cfg.today { p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H-5, 5), fg) } diff --git a/datetime.go b/datetime.go index e258df2..4fef3cc 100644 --- a/datetime.go +++ b/datetime.go @@ -196,6 +196,12 @@ func datetimePopup(c *ui.Context, anchor *ui.Element, open *bool, readOnly bool, }) } +// datetimeSelectedMark strokes the 1 DIP inset frame that marks a chosen cell. +func datetimeSelectedMark(p *ui.Painter, r ui.Rect, c ui.Color) { + const inset = 2 + p.Stroke(ui.Rect{X: r.X + inset, Y: r.Y + inset, W: r.W - 2*inset, H: r.H - 2*inset}, c, theme.ControlRadius/2, theme.BorderWidth) +} + // datetimeError shows a field error in Danger beneath a control. func datetimeError(c *ui.Context, text string) { if text == "" { diff --git a/datetime_grid.go b/datetime_grid.go index 7c35d05..f4e3136 100644 --- a/datetime_grid.go +++ b/datetime_grid.go @@ -125,6 +125,9 @@ func datetimeGridCell(c *ui.Context, o, start, cursor int, cfg datetimeGridConfi current := cfg.current(o) ring := cell.FocusVisible() || cell.Focused() && keyed cell.DrawOver(func(p *ui.Painter, r ui.Rect) { + if chosen { + datetimeSelectedMark(p, r, k.OnAccent) + } if current { p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H-6, 5), fg) } diff --git a/datetime_review2_test.go b/datetime_review2_test.go index fbc1241..88a74ba 100644 --- a/datetime_review2_test.go +++ b/datetime_review2_test.go @@ -140,3 +140,22 @@ func TestDateRangePickerConfirmChecksCurrentBounds(t *testing.T) { t.Fatalf("valid draft not committed: %v", v) } } + +func TestTimePickerPopupPickClearsSegmentError(t *testing.T) { + v, n := TimeOfDay{9, 30, 0}, 0 + tt := timePickerTester(&v, TimePickerOptions{}, &n) + _ = tt.Click("Alarm: Hours") + tt.Key(0, ui.Key2) + tt.Key(0, ui.Key9) + if v != (TimeOfDay{2, 30, 0}) || !tt.HasText("Hours must be from 0 to 23.") { + t.Fatalf("setup: %v %v", v, tt.Texts()) + } + n = 0 + _ = tt.Click("Choose time") + if err := tt.Click("03"); err != nil { + t.Fatal(err) + } + if v != (TimeOfDay{3, 30, 0}) || n != 1 || tt.HasText("Hours must be") { + t.Fatalf("popup pick: value %v, %d changes, %v", v, n, tt.Texts()) + } +} diff --git a/testdata/snapshots/darwin/calendar-dark.png b/testdata/snapshots/darwin/calendar-dark.png index 8a00542d15c18c635c025309e392e7dac381343e..164bbe60a81034b8814bc37f52a9ca815b23f688 100644 GIT binary patch delta 8351 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 YQnbBsx7h|Y{r8HNnx1NnvVG+L0p`%4WdHyG delta 8244 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 y%5JUZauSUt~ys>M3BU2wgh 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 SB9-#rlq$%mzAKS3{qi5Uw^OJ9 diff --git a/time_picker.go b/time_picker.go index b8dd80a..5ab13b3 100644 --- a/time_picker.go +++ b/time_picker.go @@ -66,8 +66,9 @@ func TimePicker(c *ui.Context, value *TimeOfDay, opts TimePickerOptions) DateTim draft := ui.Local(col, "time-picker-draft", func() TimeOfDay { return *value }) seen := ui.Local(col, "time-picker-seen", func() TimeOfDay { return *value }) problem := ui.Local(col, "time-picker-error", func() string { return "" }) + pending := ui.Local(col, "time-picker-pending", func() bool { return false }) if *seen != *value { - *draft, *seen = *value, *value + *draft, *seen, *pending = *value, *value, false } changed := false col.Children(func() { @@ -81,9 +82,13 @@ func TimePicker(c *ui.Context, value *TimeOfDay, opts TimePickerOptions) DateTim *problem = p c.Invalidate() } - if edited && p == "" && *draft != *value { - slog.Debug("mujica: time_picker change", "from", *value, "to", *draft) - *value, *seen, changed = *draft, *draft, true + *pending = *pending || edited + if *pending && p == "" { + if *draft != *value { + slog.Debug("mujica: time_picker change", "from", *value, "to", *draft) + *value, *seen, changed = *draft, *draft, true + } + *pending = false } datetimeError(c, *problem) }) @@ -191,6 +196,7 @@ func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field *ui.Elem field = datetimeField(c, ui.Row(c), cfg.problem != "", cfg.readOnly).Gap(d.Unit() * 2) open := ui.Local(field, "time-open", func() bool { return false }) fresh := ui.Local(field, "time-fresh", func() bool { return false }) + bads := ui.Local(field, "time-bads", func() [4]timeBad { return [4]timeBad{} }) field.Children(func() { segs := ui.Row(c).AlignItems(ui.Center).Gap(d.Unit() * 1.5).FocusGroup(ui.Horizontal).Role(ui.RoleGroup).Label(cfg.label) if segs.Shortcut(ui.Cmd, ui.KeyC) { @@ -205,14 +211,11 @@ func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field *ui.Elem if part == timePeriodPart { ui.Box(c).Width(d.Unit()) } - seg, done, segBad := timeSegment(c, draft, part, cfg, &edited) + seg, done := timeSegment(c, draft, part, cfg, &bads[part], &edited) if focusNext { seg.Focus() } focusNext = done - if bad == "" { - bad = segBad - } } }) if cfg.readOnly { @@ -227,14 +230,30 @@ func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field *ui.Elem *open, *fresh = true, true } datetimePopup(c, field, open, cfg.readOnly, func() { - if timeColumns(c, draft, cfg, *fresh) { + if picked, ok := timeColumns(c, draft, cfg, *fresh); ok { + bads[picked] = timeBad{} edited = true } *fresh = false }) + for _, part := range timeParts(cfg) { + if b := &bads[part]; b.text != "" && b.value != timePartValue(*draft, part, cfg) { + *b = timeBad{} + } + if bads[part].text != "" && bad == "" { + lo, hi := timeLimits(part, cfg) + bad = fmt.Sprintf(msg(c, "time_picker.part_range"), msg(c, timePartNames[part]), lo, hi) + } + } return field, edited, bad } +// timeBad is a segment's invalid typed text and the value it was typed over. +type timeBad struct { + text string + value int +} + // timeLimits returns the lowest and highest value a segment accepts. func timeLimits(part timePart, cfg timeConfig) (lo, hi int) { switch { @@ -247,7 +266,7 @@ func timeLimits(part timePart, cfg timeConfig) (lo, hi int) { } // timeSegment draws one segment; done reports a complete valid entry, bad an invalid one. -func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, edited *bool) (seg *ui.Element, done bool, bad string) { +func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, bad *timeBad, edited *bool) (seg *ui.Element, done bool) { k := Tokens(c) d := Density(c) name := msg(c, timePartNames[part]) @@ -258,16 +277,14 @@ func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, Padding(0, d.Unit()).Radius(theme.ControlRadius).MinWidth(theme.BodySize * 1.6).Shrink(0).Center() internal.Ring(seg, k.Focus, theme.ControlRadius) typed := ui.Local(seg, "time-typed", func() string { return "" }) - badText := ui.Local(seg, "time-bad", func() string { return "" }) - badFor := ui.Local(seg, "time-bad-for", func() int { return 0 }) if !seg.Focused() { *typed = "" } - if *badFor != timePartValue(*draft, part, cfg) { - *badText = "" + if bad.value != timePartValue(*draft, part, cfg) { + *bad = timeBad{} } set := func(t TimeOfDay) { - *badText = "" + *bad = timeBad{} if t != *draft { *draft = t *edited = true @@ -299,7 +316,7 @@ func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, set(timeWithPart(t, part, v, cfg)) done = complete case complete: - *badText, *badFor = fmt.Sprintf("%02d", v), timePartValue(*draft, part, cfg) + *bad = timeBad{fmt.Sprintf("%02d", v), timePartValue(*draft, part, cfg)} slog.Debug("mujica: time_picker invalid segment", "part", int(part), "typed", v) } } @@ -320,14 +337,13 @@ func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, case timePeriodPart: text = timePeriod(c, t.Hour) } - if *badText != "" { - text = *badText - bad = fmt.Sprintf(msg(c, "time_picker.part_range"), msg(c, timePartNames[part]), lo, hi) + if bad.text != "" { + text = bad.text } seg.Description(timeSegmentDescription(c, part, text, lo, hi)).Error(cfg.problem) fg := k.Text switch { - case *badText != "": + case bad.text != "": fg = k.Danger case seg.Focused() && !cfg.readOnly: seg.Background(k.Accent) @@ -338,7 +354,7 @@ func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, seg.Background(k.SurfaceHover) } seg.Children(func() { ui.Text(c, text).TextColor(fg).FontFeatures("tnum").SingleLine() }) - return seg, done, bad + return seg, done } // timeSegmentDescription gives assistive technology a segment's value and range. diff --git a/time_picker_popup.go b/time_picker_popup.go index e08dc9e..463afe7 100644 --- a/time_picker_popup.go +++ b/time_picker_popup.go @@ -10,7 +10,7 @@ import ( ) // timeColumns lists choosable values per segment and reports whether a choice edited *draft. -func timeColumns(c *ui.Context, draft *TimeOfDay, cfg timeConfig, fresh bool) (edited bool) { +func timeColumns(c *ui.Context, draft *TimeOfDay, cfg timeConfig, fresh bool) (picked timePart, edited bool) { k := Tokens(c) d := Density(c) h := d.ControlHeight() - 4 @@ -77,14 +77,17 @@ func timeColumns(c *ui.Context, draft *TimeOfDay, cfg timeConfig, fresh bool) (e item.Background(k.SurfaceHover) } internal.Ring(item, k.Focus, theme.ControlRadius) + if o == t { + item.Draw(func(p *ui.Painter, r ui.Rect) { datetimeSelectedMark(p, r, k.OnAccent) }) + } item.Children(func() { ui.Text(c, text).TextColor(fg).FontFeatures("tnum") }) - if item.Clicked() && o != *draft { - *draft, edited = o, true + if item.Clicked() { + *draft, picked, edited = o, part, true } } }) }) } }) - return edited + return picked, edited } diff --git a/time_range_picker.go b/time_range_picker.go index 3a03486..66b8a61 100644 --- a/time_range_picker.go +++ b/time_range_picker.go @@ -53,8 +53,9 @@ func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOption 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 "" }) + pending := ui.Local(col, "time-range-pending", func() bool { return false }) if *seen != *value { - *draft, *seen = *value, *value + *draft, *seen, *pending = *value, *value, false } changed := false col.Children(func() { @@ -82,9 +83,13 @@ func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOption *problem = p c.Invalidate() } - if edited && p == "" && *draft != *value { - slog.Debug("mujica: time_range_picker change", "from", *value, "to", *draft) - *value, *seen, changed = *draft, *draft, true + *pending = *pending || edited + if *pending && p == "" { + if *draft != *value { + slog.Debug("mujica: time_range_picker change", "from", *value, "to", *draft) + *value, *seen, changed = *draft, *draft, true + } + *pending = false } datetimeError(c, *problem) }) From e3eaef743779981ea28a0dc947f122ab7a8fe655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 13:17:52 -0400 Subject: [PATCH 2/2] Date & Time: review r3 fixes --- date_picker.go | 5 ++- datetime.go | 2 + datetime_review2_test.go | 60 +++++++++++++++++++++++++++++ docs/components/051-calendar.md | 2 +- docs/components/057-month-picker.md | 2 +- docs/components/058-year-picker.md | 2 +- examples/gallery/composite_form.go | 2 +- examples/gallery/composite_test.go | 2 +- 8 files changed, 70 insertions(+), 7 deletions(-) diff --git a/date_picker.go b/date_picker.go index bec7d78..57ae24a 100644 --- a/date_picker.go +++ b/date_picker.go @@ -102,11 +102,12 @@ func DatePicker(c *ui.Context, value *Date, opts DatePickerOptions) DateTimeResu if placeholder == "" { placeholder = msg(c, "date_picker.placeholder") } - var field *ui.Element + var field, input *ui.Element col.Children(func() { field = datetimeField(c, ui.Row(c), *problem != "", opts.ReadOnly) field.Children(func() { in := textInputEditor(c, field, text, opts.ReadOnly, ui.TextInputBase).Grow(1).Shrink(1).Placeholder(placeholder).FontFeatures("tnum") + input = in label := opts.Label if label == "" { label = placeholder @@ -149,7 +150,7 @@ func DatePicker(c *ui.Context, value *Date, opts DatePickerOptions) DateTimeResu }) datetimeError(c, *problem) }) - return DateTimeResult{Element: col, changed: changed} + return DateTimeResult{Element: col, Input: input, changed: changed} } // datePickerProblem explains why an existing day d cannot be chosen, or returns "". diff --git a/datetime.go b/datetime.go index 4fef3cc..6a9bf2c 100644 --- a/datetime.go +++ b/datetime.go @@ -69,6 +69,8 @@ func (d Date) within(lo, hi Date) bool { // DateTimeResult is a date or time component and what happened to it this frame. type DateTimeResult struct { *ui.Element + // Input is the text field inside, for labels and focus; nil without one. + Input *ui.Element changed bool cancelled bool } diff --git a/datetime_review2_test.go b/datetime_review2_test.go index 88a74ba..5654eb2 100644 --- a/datetime_review2_test.go +++ b/datetime_review2_test.go @@ -5,6 +5,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "mujicaui/theme" ) func TestTimePickerDisabledIgnoresSegmentKeys(t *testing.T) { @@ -159,3 +161,61 @@ func TestTimePickerPopupPickClearsSegmentError(t *testing.T) { t.Fatalf("popup pick: value %v, %d changes, %v", v, n, tt.Texts()) } } + +func TestFormFocusesDatePickerInput(t *testing.T) { + var st FormState + day := Date{} + var input *ui.Element + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Form(c, &st, FormOptions{}, func(f *FormScope) { + FormItem(f, "debut", &day, FormItemOptions[Date]{Label: "Debut date", Required: true}, func() *ui.Element { + r := DatePicker(c, &day, DatePickerOptions{Today: calendarToday}) + input = r.Input + return r.Input + }) + }) + }, 600, 500) + if err := tt.Click("Submit"); err != nil { + t.Fatal(err) + } + if input == nil || !input.Focused() || !tt.Focused("Debut date") { + t.Fatal("a failed submit should focus the DatePicker's text box") + } + tt.Type("2026-10-12") + tt.Key(0, ui.KeyEnter) + if day != oct(12) { + t.Fatalf("typing after the focus jump gave %v", day) + } +} + +func TestSelectedCellsHaveTheirOwnMark(t *testing.T) { + k := theme.Light() + check := func(name string, tt *ui.Tester, label string, shift int) { + t.Helper() + r, ok := tt.Find(label) + if !ok { + t.Fatalf("%s: no cell %q", name, label) + } + r.X += float32(shift) * r.W + img := tt.Image() + y := int(r.Y + r.H/2) + frame, fill := img.RGBAAt(int(r.X)+2, y), img.RGBAAt(int(r.X)+5, y) + if !datetimeNear(frame, k.OnAccent) || !datetimeNear(fill, k.Accent) { + t.Fatalf("%s: selected cell frame %v fill %v", name, frame, fill) + } + } + v := oct(14) + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Mode: Light}) + Calendar(c, &v, CalendarOptions{Today: calendarToday}) + }, 400, 400) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + check("calendar", tt, datetimeTestLabel(oct(14)), 0) + y := 2024 + ty := yearPickerTester(&y, YearPickerOptions{}, new(int)) + ty.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + _ = ty.Click("Vintage") + // The field also shows "2024", so locate the cell beside 2023. + check("year", ty, "2023", 1) +} diff --git a/docs/components/051-calendar.md b/docs/components/051-calendar.md index c891a73..e30d0cd 100644 --- a/docs/components/051-calendar.md +++ b/docs/components/051-calendar.md @@ -33,7 +33,7 @@ ui.Render(view, 400, 300, 1) ## 状态 -默认、悬停、按下、聚焦(键盘焦点环)、选中(酒红填充 + 单选语义)、今日(加粗 + 菱形)、不可选(删除线并由 MyGo 置灰)、非本月(弱化文字)、禁用。 +默认、悬停、按下、聚焦(键盘焦点环)、选中(酒红填充 + 内嵌细框 + 单选语义)、今日(加粗 + 菱形)、不可选(删除线并由 MyGo 置灰)、非本月(弱化文字)、禁用。 ## 事件 diff --git a/docs/components/057-month-picker.md b/docs/components/057-month-picker.md index 1a701c1..562bf37 100644 --- a/docs/components/057-month-picker.md +++ b/docs/components/057-month-picker.md @@ -35,7 +35,7 @@ ui.Render(view, 400, 300, 1) ## 状态 -默认、悬停、聚焦、选中(酒红填充)、当前月(加粗 + 菱形)、不可选(删除线)、禁用。 +默认、悬停、聚焦、选中(酒红填充 + 内嵌细框)、当前月(加粗 + 菱形)、不可选(删除线)、禁用。 ## 事件 diff --git a/docs/components/058-year-picker.md b/docs/components/058-year-picker.md index 2b10e5a..d832172 100644 --- a/docs/components/058-year-picker.md +++ b/docs/components/058-year-picker.md @@ -35,7 +35,7 @@ ui.Render(view, 400, 300, 1) ## 状态 -默认、悬停、聚焦、选中、当前年、不可选、相邻十年(弱化)、禁用。 +默认、悬停、聚焦、选中(酒红填充 + 内嵌细框)、当前年(加粗 + 菱形)、不可选、相邻十年(弱化)、禁用。 ## 事件 diff --git a/examples/gallery/composite_form.go b/examples/gallery/composite_form.go index 2894c3b..cb66bd3 100644 --- a/examples/gallery/composite_form.go +++ b/examples/gallery/composite_form.go @@ -152,7 +152,7 @@ func (g *gallery) formFields(c *ui.Context, f *mujica.FormScope, log func(string if r.Changed() { log("debut " + s.debut.Format("2006-01-02")) } - return r.Element + return r.Input }) mujica.FormItem(f, "roles", &s.tags, mujica.FormItemOptions[[]string]{Label: g.t("Roles", "担当"), Required: true, Description: g.t("Up to 4; press Enter after each.", "最多 4 个;每个后按 Enter。")}, func() *ui.Element { diff --git a/examples/gallery/composite_test.go b/examples/gallery/composite_test.go index 4b2be4e..1526d8e 100644 --- a/examples/gallery/composite_test.go +++ b/examples/gallery/composite_test.go @@ -102,7 +102,7 @@ func TestCompositeFormInvalidThenValid(t *testing.T) { tt.Type("Mortis") click(t, tt, "Password") tt.Type("hunter22secret") - click(t, tt, "Select date") // the date field itself; its FormItem label focuses the picker's column + click(t, tt, "Debut date") // the FormItem label now reaches the picker's text box tt.Type("2026-12-24") tt.Key(0, ui.KeyEnter) click(t, tt, "Roles")