This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 13:15:30 -04:00
1 parent 529aea6fa4
commit 4a0c3a789f
9 files changed
+84 -29

No files matched your search

+3
View File
@@ -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)
}
+6
View File
@@ -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 == "" {
+3
View File
@@ -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)
}
+19
View File
@@ -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())
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+37 -21
View File
@@ -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.
+7 -4
View File
@@ -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
}
+9 -4
View File
@@ -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)
})