From 20aeb143ef916646be241766be644fa2b2d7b718 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 12:42:50 -0400 Subject: [PATCH] Feedback: review r1 fixes --- alert.go | 8 +- docs/components/093-progress.md | 8 +- docs/components/094-spinner.md | 2 +- docs/components/095-skeleton.md | 2 +- empty_state.go | 2 +- feedback_review_test.go | 84 +++++++++++++++++++ loading_overlay.go | 4 +- notification_center.go | 12 ++- progress.go | 40 ++++----- progress_test.go | 46 ++++++++++ result.go | 5 +- skeleton.go | 16 ++-- spinner.go | 9 +- status_indicator.go | 5 +- testdata/snapshots/darwin/progress-dark.png | Bin 23280 -> 23331 bytes testdata/snapshots/darwin/progress-light.png | Bin 22360 -> 22400 bytes toast.go | 38 ++++++--- toast_test.go | 52 ++++++++---- 18 files changed, 250 insertions(+), 83 deletions(-) create mode 100644 feedback_review_test.go diff --git a/alert.go b/alert.go index 2b5f116..8d78108 100644 --- a/alert.go +++ b/alert.go @@ -43,8 +43,10 @@ func severityLook(k theme.Tokens, s Severity) (ui.Color, *ui.SVG) { return k.Warning, icons.Must("triangle-alert") case SeverityError: return k.Danger, icons.Must("circle-x") + case SeverityInfo: + return k.Info, icons.Must("info") } - return k.Info, icons.Must("info") + panic(fmt.Sprintf("mujica: unknown Severity %d", int(s))) } // AlertOptions configure an Alert. @@ -66,9 +68,7 @@ type AlertResult struct { Closed bool } -// Alert shows an inline message marked by color, icon and accessible name. -// A non-nil open adds a dismiss button that sets *open to false; nothing -// shows while *open is false. +// Alert shows an inline message; a non-nil open adds a dismiss button that clears it. func Alert(c *ui.Context, open *bool, message string, opts AlertOptions) AlertResult { if message == "" { panic("mujica: Alert needs a message") diff --git a/docs/components/093-progress.md b/docs/components/093-progress.md index 8609972..36caa84 100644 --- a/docs/components/093-progress.md +++ b/docs/components/093-progress.md @@ -27,18 +27,18 @@ mujica.Meter(c, 93, mujica.MeterOptions{ | 字段 | 类型 | 默认值 | 含义 | | --- | --- | --- | --- | | `value` | `float64` | 必填 | 0 到 1;超出或 NaN 时 panic(不确定模式忽略)。 | -| `Shape` | `ProgressShape` | `ProgressLinear` | `ProgressLinear` 或 `ProgressRing`。 | +| `Shape` | `ProgressShape` | `ProgressLinear` | `ProgressLinear` 或 `ProgressRing`;其他值 panic。 | | `Indeterminate` | `bool` | `false` | 未知长度的进度。 | | `Label` | `string` | `""` | 显示在旁边并作为无障碍名称;为空时名称为“Loading”。 | | `ShowValue` | `bool` | `false` | 显示百分比。 | -| `Size` | `float32` | 环形 48、线形填满 | 环形直径或线形宽度;负数 panic。 | +| `Size` | `float32` | 环形 48、线形填满 | 环形直径或线形宽度;负数、NaN 或无穷 panic。 | `MeterOptions`: | 字段 | 类型 | 默认值 | 含义 | | --- | --- | --- | --- | | `value` | `float64` | 必填 | 须在范围内,否则 panic。 | -| `Min`, `Max` | `float64` | 0, 1(两者都为 0 时) | 范围;`Max <= Min` panic。 | +| `Min`, `Max` | `float64` | 0, 1(两者都为 0 时) | 范围;非有限值或 `Max <= Min` panic。 | | `Levels` | `*ui.MeterLevels` | `nil` | 阈值:达到 `Warning` 为偏高、达到 `Critical` 为严重;`Critical < Warning` 时低值为坏(如电量)。须在范围内。 | | `Label` | `string` | `""` | 名称。 | | `ShowValue` | `bool` | `false` | 显示“值 / 最大值”。 | @@ -47,7 +47,7 @@ mujica.Meter(c, 93, mujica.MeterOptions{ ## 状态 - 确定进度变化时以 120 ms 过渡,减少动画时直接跳到新值。 -- 不确定进度只在绘制时移动(只重绘、不重建视图);减少动画时显示静态填充和“In progress”文字。 +- 不确定进度只在绘制时移动(只重绘、不重建视图);减少动画时显示满格静态填充(对比度不低于 3:1)和“In progress”文字;切换到减少动画时进行中的过渡直接跳到目标值。 - 计量条按级别使用 Success / Warning / Danger 颜色,同时显示“Normal / High / Critical / Low”文字与阈值刻度,并把级别写入无障碍名称。 ## 事件 diff --git a/docs/components/094-spinner.md b/docs/components/094-spinner.md index 06d612f..f4698c3 100644 --- a/docs/components/094-spinner.md +++ b/docs/components/094-spinner.md @@ -16,7 +16,7 @@ mujica.Spinner(c, mujica.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: t | 字段 | 类型 | 默认值 | 含义 | | --- | --- | --- | --- | -| `Size` | `float32` | 16 | 标记边长(DIP)。 | +| `Size` | `float32` | 16 | 标记边长(DIP);负数、NaN 或无穷 panic。 | | `Label` | `string` | 内置“Loading / 加载中” | 无障碍名称;`ShowLabel` 时也显示在旁边。 | | `ShowLabel` | `bool` | `false` | 在标记旁显示说明。 | | `Color` | `ui.Color` | `AccentText` | 标记颜色。 | diff --git a/docs/components/095-skeleton.md b/docs/components/095-skeleton.md index 4e7e7e1..188319f 100644 --- a/docs/components/095-skeleton.md +++ b/docs/components/095-skeleton.md @@ -26,7 +26,7 @@ ui.Row(c).Gap(12).Children(func() { | `Height` | `float32` | 当前密度控件高度 | 矩形高度;文字形状由行数决定(每行 21 DIP)。 | | `Lines` | `int` | 1 | 文字行数,多行时最后一行为 60% 宽。 | -负值 panic。 +负值、NaN、无穷或未知形状 panic。文字行高随系统文本缩放。 ## 状态 diff --git a/empty_state.go b/empty_state.go index 67ca961..2951801 100644 --- a/empty_state.go +++ b/empty_state.go @@ -76,7 +76,7 @@ func EmptyState(c *ui.Context, opts EmptyStateOptions) EmptyStateResult { if !opts.NoIcon { ui.Icon(c, icon).Size(32, 32).TextColor(k.TextMuted).Margin(0, 0, u*2, 0) } - ui.Text(c, title).Font(theme.SerifFont).FontSize(theme.H3Size).TextColor(k.Text).TextAlign(ui.Center) + ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text).TextAlign(ui.Center) ui.Text(c, desc).TextColor(k.TextMuted).TextAlign(ui.Center).MaxWidth(360).LineHeight(theme.LineHeight) if opts.Action != "" { ui.Box(c).Margin(u*2, 0, 0, 0).Children(func() { diff --git a/feedback_review_test.go b/feedback_review_test.go new file mode 100644 index 0000000..d332dad --- /dev/null +++ b/feedback_review_test.go @@ -0,0 +1,84 @@ +package mujica + +import ( + "math" + "testing" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +func TestFeedbackTextFollowsTextScale(t *testing.T) { + items := []Notification{{ID: "n", Title: "Backup complete", Read: true}, {ID: "u", Title: "Unread one"}} + var empty, result float32 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Children(func() { + Progress(c, 0.5, ProgressOptions{ShowValue: true}) + empty = EmptyState(c, EmptyStateOptions{Title: "Empty title", Description: "Short", NoIcon: true}).Element.Bounds().H + result = Result(c, ResultOptions{Title: "Result title"}).Element.Bounds().H + StatusIndicator(c, StatusSuccess, StatusIndicatorOptions{}) + NotificationCenter(c, &items, NotificationCenterOptions{}) + }) + }, 900, 2400) + tt.Frame() + empty1, result1 := empty, result + tt.SetPreferences(ui.Preferences{TextScale: 2}) + tt.Frame() // Bounds report the previous layout + for text, min := range map[string]float32{ + "50%": theme.CaptionSize * 2, + "Healthy": theme.BodySize * 2, + "1 unread": theme.CaptionSize * 2, + } { + if r, ok := tt.Find(text); !ok || r.H < min { + t.Errorf("%q at 200%%: %v %v, want height >= %v", text, r, ok, min) + } + } + // Titles are found only through their labeled groups: the groups must grow by the titles too. + if grow := empty - empty1; grow < theme.BodySize*theme.LineHeight+theme.H3Size { + t.Errorf("EmptyState grew %v at 200%%; its title did not scale", grow) + } + if grow := result - result1; grow < theme.H2Size { + t.Errorf("Result grew %v at 200%%; its title did not scale", grow) + } +} + +func TestFeedbackRejectsInvalidInput(t *testing.T) { + nan, inf := math.NaN(), math.Inf(1) + for name, f := range map[string]func(c *ui.Context){ + "progress shape": func(c *ui.Context) { Progress(c, 0.5, ProgressOptions{Shape: 7}) }, + "progress NaN size": func(c *ui.Context) { Progress(c, 0.5, ProgressOptions{Size: float32(nan)}) }, + "meter infinite range": func(c *ui.Context) { Meter(c, 1, MeterOptions{Max: inf}) }, + "meter NaN min": func(c *ui.Context) { Meter(c, 1, MeterOptions{Min: nan, Max: 2}) }, + "meter NaN threshold": func(c *ui.Context) { + Meter(c, 1, MeterOptions{Max: 2, Levels: &ui.MeterLevels{Warning: nan, Critical: 2}}) + }, + "skeleton shape": func(c *ui.Context) { Skeleton(c, SkeletonOptions{Shape: 5}) }, + "skeleton NaN width": func(c *ui.Context) { Skeleton(c, SkeletonOptions{Width: float32(nan)}) }, + "spinner negative size": func(c *ui.Context) { Spinner(c, SpinnerOptions{Size: -4}) }, + "status unknown labeled": func(c *ui.Context) { StatusIndicator(c, 42, StatusIndicatorOptions{Label: "Custom"}) }, + "alert severity": func(c *ui.Context) { Alert(c, nil, "x", AlertOptions{Severity: 9}) }, + "toast severity": func(c *ui.Context) { ShowToast(c, ToastMessage{Message: "x", Severity: -1}) }, + } { + func() { + defer func() { + if recover() == nil { + t.Errorf("%s: no panic", name) + } + }() + ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); f(c) }, 300, 200) + }() + } +} + +func TestNotificationTitleOnlyHitHeight(t *testing.T) { + items := []Notification{{ID: "n", Title: "Backup complete", Read: true}} + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + NotificationCenter(c, &items, NotificationCenterOptions{}) + }, 600, 400) + if r, ok := tt.Find("Backup complete"); !ok || r.H < theme.MinHitSize { + t.Fatalf("title-only notification hit box %v %v", r, ok) + } +} diff --git a/loading_overlay.go b/loading_overlay.go index 6f8ba0d..5e826b4 100644 --- a/loading_overlay.go +++ b/loading_overlay.go @@ -25,9 +25,7 @@ type LoadingOverlayResult struct { Canceled bool } -// LoadingOverlay builds content and, while loading, covers it with a -// spinner and label. Covered content takes no pointer, keys or focus; -// the rest of the window stays usable. Focus inside moves to the overlay. +// LoadingOverlay covers content while loading, blocking input to that region only. func LoadingOverlay(c *ui.Context, loading bool, opts LoadingOverlayOptions, content func()) LoadingOverlayResult { if content == nil { panic("mujica: LoadingOverlay needs content") diff --git a/notification_center.go b/notification_center.go index 19a4f93..f712d4d 100644 --- a/notification_center.go +++ b/notification_center.go @@ -44,9 +44,7 @@ type NotificationCenterResult struct { ClearedAll bool } -// NotificationCenter lists *items and edits them in place: read, unread, -// remove, clear. The list is one Tab stop; arrows move among its buttons -// and Delete removes the focused notification. +// NotificationCenter lists *items and edits them in place: read, unread, remove, clear. func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationCenterOptions) NotificationCenterResult { if items == nil { panic("mujica: NotificationCenter needs items") @@ -70,9 +68,9 @@ func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationC Role(ui.RoleGroup).Label(title).Clip() panel.Children(func() { ui.Row(c).AlignItems(ui.Center).Gap(u*2).Padding(u*3, u*3, u*3, u*4).Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(theme.H3Size).TextColor(k.Text) + ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text) if unread > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "notification_center.unread"), unread)).FontSize(theme.CaptionSize).TextColor(k.AccentText) + ui.Text(c, fmt.Sprintf(msg(c, "notification_center.unread"), unread)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) } ui.Spacer(c) if Button(c, msg(c, "notification_center.mark_all_read"), ButtonOptions{Variant: Ghost, Disabled: unread == 0}).Clicked() { @@ -135,7 +133,7 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H/2, 8), k.AccentText) } }) - open := ui.ButtonBase(c).Grow(1).Shrink(1).Column().AlignItems(ui.Start).Justify(ui.Start).Gap(u). + open := ui.ButtonBase(c).MinHeight(theme.MinHitSize).Grow(1).Shrink(1).Column().AlignItems(ui.Start).Justify(ui.Start).Gap(u). Radius(theme.ControlRadius).Label(name) internal.Ring(open, k.Focus, theme.ControlRadius) open.Children(func() { @@ -145,7 +143,7 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, t.Bold() } if n.Time != "" { - ui.Text(c, n.Time).FontSize(theme.CaptionSize).TextColor(k.TextMuted).SingleLine() + ui.Text(c, n.Time).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) if n.Body != "" { diff --git a/progress.go b/progress.go index 85d09dc..08f60cd 100644 --- a/progress.go +++ b/progress.go @@ -31,14 +31,16 @@ type ProgressOptions struct { Size float32 } -// Progress shows value, from 0 to 1, of work done. It panics outside that range. -// Indeterminate indicators move only while painted and hold still under reduced motion. +// Progress shows work done, value 0 to 1; indeterminate ones hold still under reduced motion. func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element { + if opts.Shape != ProgressLinear && opts.Shape != ProgressRing { + panic(fmt.Sprintf("mujica: unknown ProgressShape %d", int(opts.Shape))) + } if !opts.Indeterminate && (math.IsNaN(value) || value < 0 || value > 1) { panic(fmt.Sprintf("mujica: Progress value %v outside 0..1", value)) } - if opts.Size < 0 { - panic("mujica: negative Progress Size") + if !(opts.Size >= 0) || math.IsInf(float64(opts.Size), 1) { + panic(fmt.Sprintf("mujica: Progress Size %v invalid", opts.Size)) } k := Tokens(c) u := Density(c).Unit() @@ -71,12 +73,12 @@ func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element { if opts.Label != "" || caption != "" { ui.Row(c).Gap(u * 2).Children(func() { ui.Text(c, opts.Label).TextColor(k.Text).Grow(1).Shrink(1) - ui.Text(c, caption).TextColor(k.TextMuted).FontSize(theme.CaptionSize).FontFeatures("tnum").SingleLine() + ui.Text(c, caption).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() }) } bar := ui.Progress(c, acc).Label(name).FillWidth().Height(u * 1.5).Radius(theme.ControlRadius).Background(k.Border) shown := float32(value) - if !opts.Indeterminate { + if !opts.Indeterminate && !still { shown = bar.Animate("progress.value", shown, theme.StateDuration) } bar.Draw(func(p *ui.Painter, r ui.Rect) { @@ -84,7 +86,7 @@ func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element { case !opts.Indeterminate: p.Fill(ui.Rect{X: r.X, Y: r.Y, W: r.W * shown, H: r.H}, k.AccentText, theme.ControlRadius) case still: - p.Fill(r, k.AccentText.Alpha(0.4), theme.ControlRadius) + p.Fill(r, k.AccentText, theme.ControlRadius) default: p.AnimationFrame() phase := float32(p.Now().UnixMilli()%1400) / 1400 @@ -108,7 +110,7 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c row.Children(func() { ring := ui.Progress(c, acc).Label(name).Size(d, d).Radius(0).Background(ui.Transparent) shown := float32(acc) - if !opts.Indeterminate { + if !opts.Indeterminate && !still { shown = ring.Animate("progress.value", shown, theme.StateDuration) } stroke := max(3, d/10) @@ -123,7 +125,7 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c p.StrokePath(progressArc(p, cx, cy, rad, 0, shown), stroke, k.AccentText) } case still: - p.StrokePath(&track, stroke, k.AccentText.Alpha(0.4)) + p.StrokePath(&track, stroke, k.AccentText) default: p.AnimationFrame() start := float32(p.Now().UnixMilli()%1200) / 1200 @@ -132,7 +134,7 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c }) if caption != "" && d >= 44 && !opts.Indeterminate { ring.Children(func() { - ui.Text(c, caption).FontSize(theme.CaptionSize).FontFeatures("tnum").TextColor(k.Text) + ui.Text(c, caption).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").TextColor(k.Text) }) ring.Center() } @@ -142,7 +144,7 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c ui.Text(c, opts.Label).TextColor(k.Text) } if caption != "" && (d < 44 || opts.Indeterminate) { - ui.Text(c, caption).TextColor(k.TextMuted).FontSize(theme.CaptionSize) + ui.Text(c, caption).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) } }) } @@ -171,8 +173,7 @@ func progressArc(pt *ui.Painter, cx, cy, r float32, a, b float32) *ui.Path { type MeterOptions struct { // Min and Max bound the value; both zero mean 0 to 1. Min, Max float64 - // Levels mark the value High from Warning and Critical from Critical; - // Critical below Warning flags low values instead. Nil marks none. + // Levels are the High and Critical thresholds; Critical below Warning flags low values. Levels *ui.MeterLevels Label string ShowValue bool @@ -195,21 +196,20 @@ func meterLevel(v float64, l *ui.MeterLevels) int { return 0 } -// Meter shows a level within a range, as disk use. Thresholds show as -// ticks and the level as a word as well as a color. It panics on a value outside the range. +// Meter shows a level within a range with threshold ticks and a level word, not color alone. func Meter(c *ui.Context, value float64, opts MeterOptions) *ui.Element { lo, hi := opts.Min, opts.Max if lo == 0 && hi == 0 { hi = 1 } - if !(hi > lo) { + if !finite(lo) || !finite(hi) || !(hi > lo) { panic(fmt.Sprintf("mujica: Meter range %v..%v is empty", lo, hi)) } if math.IsNaN(value) || value < lo || value > hi { panic(fmt.Sprintf("mujica: Meter value %v outside %v..%v", value, lo, hi)) } - if l := opts.Levels; l != nil && (l.Warning < lo || l.Warning > hi || l.Critical < lo || l.Critical > hi) { - panic("mujica: Meter levels outside the range") + if l := opts.Levels; l != nil && !(l.Warning >= lo && l.Warning <= hi && l.Critical >= lo && l.Critical <= hi) { + panic(fmt.Sprintf("mujica: Meter levels %+v outside %v..%v", *l, lo, hi)) } k := Tokens(c) u := Density(c).Unit() @@ -246,7 +246,7 @@ func Meter(c *ui.Context, value float64, opts MeterOptions) *ui.Element { if opts.Label != "" || caption != "" { ui.Row(c).Gap(u * 2).Children(func() { ui.Text(c, opts.Label).TextColor(k.Text).Grow(1).Shrink(1) - ui.Text(c, caption).TextColor(k.TextMuted).FontSize(theme.CaptionSize).FontFeatures("tnum").SingleLine() + ui.Text(c, caption).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() }) } name := opts.Label @@ -270,3 +270,5 @@ func Meter(c *ui.Context, value float64, opts MeterOptions) *ui.Element { }) return col } + +func finite(v float64) bool { return !math.IsNaN(v) && !math.IsInf(v, 0) } diff --git a/progress_test.go b/progress_test.go index 78c5bdf..4016a1e 100644 --- a/progress_test.go +++ b/progress_test.go @@ -5,8 +5,54 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "mujicaui/theme" ) +func TestProgressReducedMotionJumpsMidTransition(t *testing.T) { + v := 0.0 + var bar ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Mode: Light}) + ui.Column(c).Width(300).Children(func() { + bar = Progress(c, v, ProgressOptions{}).Bounds() + }) + }, 400, 100) + v = 1 + tt.Frame() + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + if got, want := progressPixel(tt, bar.X+bar.W*0.95, bar.Y+bar.H/2), theme.Light().AccentText; got != want { + t.Fatalf("fill at 95%% is %v, want %v at once", got, want) + } +} + +func progressPixel(tt *ui.Tester, x, y float32) ui.Color { + r, g, b, _ := tt.Image().At(int(x), int(y)).RGBA() + return ui.RGB(uint8(r>>8), uint8(g>>8), uint8(b>>8)) +} + +func TestProgressStaticFillContrast(t *testing.T) { + for _, m := range []Mode{Light, Dark} { + var bar ui.Rect + var k theme.Tokens + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Mode: m}) + k = Tokens(c) + ui.Column(c).Width(300).Children(func() { + bar = Progress(c, 0, ProgressOptions{Indeterminate: true}).Bounds() + }) + }, 400, 100) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + tt.Frame() // Bounds report the previous layout + fill := progressPixel(tt, bar.X+bar.W/2, bar.Y+bar.H-3) // the bar is the last row + for _, against := range []ui.Color{k.Border, k.Background} { + if cr := theme.Contrast(fill, against); cr < 3 { + t.Errorf("%v: static fill %v vs %v is %.2f:1", m, fill, against, cr) + } + } + } +} + func TestProgressValueText(t *testing.T) { v := 0.424 tt := ui.NewTester(func(c *ui.Context) { diff --git a/result.go b/result.go index 70814d0..537cc19 100644 --- a/result.go +++ b/result.go @@ -38,8 +38,7 @@ type ResultResult struct { Action int } -// Result reports the outcome of a finished task and offers next steps. -// It only reports the chosen step; the caller navigates. +// Result shows a task outcome and reports the chosen next step; the caller navigates. func Result(c *ui.Context, opts ResultOptions) ResultResult { k := Tokens(c) u := Density(c).Unit() @@ -61,7 +60,7 @@ func Result(c *ui.Context, opts ResultOptions) ResultResult { res.Element = ui.Column(c).FillWidth().AlignItems(ui.Center).Gap(u*3).Padding(u*10, u*6).Role(ui.RoleGroup).Label(title) res.Element.Children(func() { ui.Icon(c, icon).Size(48, 48).TextColor(tone) - ui.Text(c, title).Font(theme.SerifFont).FontSize(theme.H2Size).TextColor(k.Text).TextAlign(ui.Center) + ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)).TextColor(k.Text).TextAlign(ui.Center) if opts.Description != "" { ui.Text(c, opts.Description).TextColor(k.TextMuted).TextAlign(ui.Center).MaxWidth(420).LineHeight(theme.LineHeight) } diff --git a/skeleton.go b/skeleton.go index eefa1b5..3c4eb04 100644 --- a/skeleton.go +++ b/skeleton.go @@ -1,6 +1,7 @@ package mujica import ( + "fmt" "math" "github.com/egoist/mygo/ui" @@ -28,11 +29,13 @@ type SkeletonOptions struct { Lines int } -// Skeleton stands in for content that is loading. It pulses while painted -// and shows a static fill under reduced motion. +// Skeleton stands in for loading content; it pulses while painted, static under reduced motion. func Skeleton(c *ui.Context, opts SkeletonOptions) *ui.Element { - if opts.Width < 0 || opts.Height < 0 || opts.Lines < 0 { - panic("mujica: negative Skeleton size") + if opts.Shape != SkeletonText && opts.Shape != SkeletonCircle && opts.Shape != SkeletonRect { + panic(fmt.Sprintf("mujica: unknown SkeletonShape %d", int(opts.Shape))) + } + if !(opts.Width >= 0) || !(opts.Height >= 0) || opts.Lines < 0 || math.IsInf(float64(opts.Width+opts.Height), 1) { + panic(fmt.Sprintf("mujica: Skeleton size %vx%v, %d lines invalid", opts.Width, opts.Height, opts.Lines)) } k := Tokens(c) still := c.Preferences().ReduceMotion @@ -70,9 +73,10 @@ func Skeleton(c *ui.Context, opts SkeletonOptions) *ui.Element { e.Height(h).Draw(func(p *ui.Painter, r ui.Rect) { paint(p, r, theme.CardRadius) }) default: lines := max(opts.Lines, 1) - line := theme.BodySize * theme.LineHeight + size := FontSize(c, theme.BodySize) + line := size * theme.LineHeight e.Height(line * float32(lines)).Draw(func(p *ui.Painter, r ui.Rect) { - bar := theme.BodySize * 0.8 + bar := size * 0.8 for i := range lines { w := r.W if lines > 1 && i == lines-1 { diff --git a/spinner.go b/spinner.go index 2f1e9dd..280f968 100644 --- a/spinner.go +++ b/spinner.go @@ -1,6 +1,7 @@ package mujica import ( + "fmt" "math" "github.com/egoist/mygo/ui" @@ -18,12 +19,14 @@ type SpinnerOptions struct { Color ui.Color } -// Spinner draws four diamonds turning around a center while it shows. -// It repaints only while painted: hidden, it stops. Reduced motion holds it still. +// Spinner turns four diamonds only while painted; hidden or under reduced motion it stops. func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element { + if !(opts.Size >= 0) || math.IsInf(float64(opts.Size), 1) { + panic(fmt.Sprintf("mujica: Spinner Size %v invalid", opts.Size)) + } k := Tokens(c) size := opts.Size - if size <= 0 { + if size == 0 { size = 16 } label := opts.Label diff --git a/status_indicator.go b/status_indicator.go index 045c642..1ebcf5a 100644 --- a/status_indicator.go +++ b/status_indicator.go @@ -44,11 +44,12 @@ type StatusIndicatorOptions struct { // StatusIndicator shows a status by its own shape and a word, never by color alone. func StatusIndicator(c *ui.Context, status Status, opts StatusIndicatorOptions) *ui.Element { + key := "status_indicator." + status.String() // panics on an unknown Status k := Tokens(c) u := Density(c).Unit() label := opts.Label if label == "" { - label = msg(c, "status_indicator."+status.String()) + label = msg(c, key) } var tone ui.Color var icon *ui.SVG @@ -71,7 +72,7 @@ func StatusIndicator(c *ui.Context, status Status, opts StatusIndicatorOptions) } else { ui.Icon(c, icon).Size(16, 16).TextColor(tone) } - ui.Text(c, label).TextColor(k.Text).FontSize(theme.BodySize).SingleLine() + ui.Text(c, label).TextColor(k.Text).FontSize(FontSize(c, theme.BodySize)).SingleLine() }) return row } diff --git a/testdata/snapshots/darwin/progress-dark.png b/testdata/snapshots/darwin/progress-dark.png index 28c32d33af262b85f99a30cc900ee16c2a868524..cead596a4144482ac4b93a4ade3ea55bd91edfcf 100644 GIT binary patch delta 16049 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 delta 16033 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 JDQp<{{{U_8ZIu83 diff --git a/testdata/snapshots/darwin/progress-light.png b/testdata/snapshots/darwin/progress-light.png index ffa4b252ec8f0a39b4df66421b8658dff852d934..459659c65b9c729ef480596290b08b92c44a033d 100644 GIT binary patch delta 15785 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 Cl3e!y delta 15728 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 t2}%KA!f8FO2ABo7iGAv9xJI~mU{{^FjTU`JE diff --git a/toast.go b/toast.go index e15a6d4..872997a 100644 --- a/toast.go +++ b/toast.go @@ -44,7 +44,10 @@ type toastItem struct { } // toastQueue holds a window's toasts, visible ones first in order shown. -type toastQueue struct{ items []toastItem } +type toastQueue struct { + items []toastItem + wake time.Time // when Toaster last asked for a frame; zero for none +} type toastKey struct{} @@ -64,6 +67,7 @@ func (q *toastQueue) push(m ToastMessage, now time.Time) { if m.Message == "" { panic("mujica: toast needs a Message") } + _ = m.Severity.String() // panics on an unknown Severity if m.ID == "" { m.ID = m.Message } @@ -106,8 +110,8 @@ func (it *toastItem) elapsed(now time.Time) time.Duration { return it.spent + now.Sub(it.runFrom) } -// expire drops toasts whose time ran out and returns the wait until the next does. -func (q *toastQueue) expire(now time.Time) (next time.Duration) { +// expire drops toasts whose time ran out and shows queued ones. +func (q *toastQueue) expire(now time.Time) { live := q.items[:0] for _, it := range q.items { if it.visible && it.life > 0 && it.elapsed(now) >= it.life { @@ -118,6 +122,15 @@ func (q *toastQueue) expire(now time.Time) (next time.Duration) { } q.items = live q.promote(now) +} + +// settle pauses the toasts held, resumes the rest, and returns the wait until one expires. +func (q *toastQueue) settle(now time.Time, held map[string]bool) (next time.Duration) { + for _, it := range q.items { + if it.visible { + q.pause(it.msg.ID, held[it.msg.ID], now) + } + } for _, it := range q.items { if it.visible && it.life > 0 && !it.runFrom.IsZero() { left := it.life - it.elapsed(now) @@ -172,15 +185,12 @@ func DismissToast(c *ui.Context, id string) { c.Invalidate() } -// Toaster draws the window's toasts at its bottom right. Call it once per -// frame, last in the view. Toasts never take the focus; Tab reaches their -// buttons. Hover or focus inside a toast pauses its time. +// Toaster draws the window's toasts, never taking the focus; call it last in the view each frame. func Toaster(c *ui.Context) ToasterResult { q := toasts(c) now := c.Now() - if next := q.expire(now); next > 0 { - c.After(next) - } + q.expire(now) + held := map[string]bool{} var res ToasterResult if len(q.items) == 0 { return res @@ -201,8 +211,8 @@ func Toaster(c *ui.Context) ToasterResult { q.items[i].announced = true c.Announce(it.msg.Message) } - action, closed, held := toastCard(c, k, u, min(360, w-u*8), it.msg) - q.pause(it.msg.ID, held, now) + action, closed, hold := toastCard(c, k, u, min(360, w-u*8), it.msg) + held[it.msg.ID] = hold switch { case action: res.Action = it.msg.ID @@ -220,6 +230,12 @@ func Toaster(c *ui.Context) ToasterResult { c.Invalidate() } } + // Scheduled after resuming, so a toast released this frame still expires. + q.wake = time.Time{} + if next := q.settle(now, held); next > 0 { + q.wake = now.Add(next) + c.After(next) + } return res } diff --git a/toast_test.go b/toast_test.go index d4425ba..c0da1f0 100644 --- a/toast_test.go +++ b/toast_test.go @@ -28,7 +28,8 @@ func TestToastQueueLimitsVisible(t *testing.T) { t.Fatalf("visible %v waiting %v", v, w) } // d starts its 5 s only when it shows. - if next := q.expire(t0.Add(ToastDuration)); next != ToastDuration { + q.expire(t0.Add(ToastDuration)) + if next := q.settle(t0.Add(ToastDuration), nil); next != ToastDuration { t.Fatalf("next wake %v", next) } if v := toastIDs(&q, true); !slices.Equal(v, []string{"d", "e"}) { @@ -66,15 +67,16 @@ func TestToastQueueErrorsStayAndPauseHolds(t *testing.T) { t0 := time.Unix(0, 0) q.push(ToastMessage{ID: "e", Severity: SeverityError, Message: "Failed"}, t0) q.push(ToastMessage{ID: "w", Severity: SeverityWarning, Message: "Slow", Action: "Details"}, t0) - q.pause("w", true, t0.Add(4*time.Second)) - if next := q.expire(t0.Add(time.Hour)); next != 0 { + q.settle(t0.Add(4*time.Second), map[string]bool{"w": true}) + q.expire(t0.Add(time.Hour)) + if next := q.settle(t0.Add(time.Hour), map[string]bool{"w": true}); next != 0 { t.Fatalf("paused toast still counts down: %v", next) } if !slices.Equal(toastIDs(&q, true), []string{"e", "w"}) { t.Fatalf("visible %v", toastIDs(&q, true)) } - q.pause("w", false, t0.Add(time.Hour)) - if next := q.expire(t0.Add(time.Hour)); next != time.Second { + // Released: the wake-up is computed after resuming, in the same call. + if next := q.settle(t0.Add(time.Hour), nil); next != time.Second { t.Fatalf("resumed toast left %v, want 1s", next) } q.expire(t0.Add(time.Hour + time.Second)) @@ -84,7 +86,7 @@ func TestToastQueueErrorsStayAndPauseHolds(t *testing.T) { } func TestToastQueuePanics(t *testing.T) { - for _, m := range []ToastMessage{{}, {Message: "x", Duration: -1}} { + for _, m := range []ToastMessage{{}, {Message: "x", Duration: -1}, {Message: "x", Severity: 9}} { func() { defer func() { if recover() == nil { @@ -156,27 +158,41 @@ func TestToastKeepsFocusAndEscapeCloses(t *testing.T) { } func TestToastExpiresAndHoverPauses(t *testing.T) { - var res ToasterResult - tt := toastTester(ToastMessage{Message: "Copied", Duration: 40 * time.Millisecond}, &res) + var q *toastQueue + const life = 40 * time.Millisecond + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if Button(c, "Notify", ButtonOptions{}).Clicked() { + ShowToast(c, ToastMessage{Message: "Copied", Duration: life}) + } + q = toasts(c) + Toaster(c) + }, 600, 400) _ = tt.Click("Notify") - time.Sleep(60 * time.Millisecond) - tt.Frame() - if tt.HasText("Copied") { - t.Fatal("toast outlived its duration") + if q.wake.IsZero() { + t.Fatal("no wake-up scheduled for a running toast") } - _ = tt.Click("Notify") r, _ := tt.Find("Copied") tt.Move(r.X+r.W/2, r.Y+r.H/2) - time.Sleep(60 * time.Millisecond) - tt.Frame() + if !q.wake.IsZero() { + t.Fatal("hovered toast still schedules its expiry") + } + time.Sleep(2 * life) + released := time.Now() + tt.Move(5, 395) + done := time.Now() + // The frame that ends the hover must schedule the expiry itself. + if q.wake.IsZero() || q.wake.Before(released) || q.wake.After(done.Add(life)) { + t.Fatalf("release scheduled wake %v, released %v", q.wake, released) + } if !tt.HasText("Copied") { t.Fatal("hovered toast expired") } - tt.Move(5, 395) - time.Sleep(60 * time.Millisecond) + // Stands in for the frame MyGo's timer runs at q.wake. + time.Sleep(time.Until(q.wake) + 5*time.Millisecond) tt.Frame() if tt.HasText("Copied") { - t.Fatal("toast stayed after the pointer left") + t.Fatal("toast stayed after its wake-up") } }