From a4b2dbaf13a02f9c1e910cdd5d2a239393355e30 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:11:45 -0400 Subject: [PATCH] WIP feedback --- alert.go | 124 +++++++++ alert_test.go | 113 ++++++++ empty_state.go | 88 ++++++ empty_state_test.go | 81 ++++++ loading_overlay.go | 94 +++++++ messages_feedback.go | 44 +++ notification_center.go | 189 +++++++++++++ progress.go | 271 +++++++++++++++++++ progress_test.go | 127 +++++++++ result.go | 97 +++++++ result_test.go | 66 +++++ skeleton.go | 87 ++++++ skeleton_test.go | 83 ++++++ spinner.go | 6 +- spinner_test.go | 77 ++++++ status_indicator.go | 77 ++++++ status_indicator_test.go | 69 +++++ testdata/snapshots/darwin/alert-dark.png | Bin 0 -> 26555 bytes testdata/snapshots/darwin/alert-light.png | Bin 0 -> 25586 bytes testdata/snapshots/darwin/progress-dark.png | Bin 0 -> 23304 bytes testdata/snapshots/darwin/progress-light.png | Bin 0 -> 22394 bytes testdata/snapshots/darwin/toast-dark.png | Bin 0 -> 15914 bytes testdata/snapshots/darwin/toast-light.png | Bin 0 -> 17918 bytes toast.go | 257 ++++++++++++++++++ toast_test.go | 193 +++++++++++++ 25 files changed, 2141 insertions(+), 2 deletions(-) create mode 100644 alert.go create mode 100644 alert_test.go create mode 100644 empty_state.go create mode 100644 empty_state_test.go create mode 100644 loading_overlay.go create mode 100644 notification_center.go create mode 100644 progress.go create mode 100644 progress_test.go create mode 100644 result.go create mode 100644 result_test.go create mode 100644 skeleton.go create mode 100644 skeleton_test.go create mode 100644 spinner_test.go create mode 100644 status_indicator.go create mode 100644 status_indicator_test.go create mode 100644 testdata/snapshots/darwin/alert-dark.png create mode 100644 testdata/snapshots/darwin/alert-light.png create mode 100644 testdata/snapshots/darwin/progress-dark.png create mode 100644 testdata/snapshots/darwin/progress-light.png create mode 100644 testdata/snapshots/darwin/toast-dark.png create mode 100644 testdata/snapshots/darwin/toast-light.png create mode 100644 toast.go create mode 100644 toast_test.go diff --git a/alert.go b/alert.go new file mode 100644 index 0000000..2b5f116 --- /dev/null +++ b/alert.go @@ -0,0 +1,124 @@ +package mujica + +import ( + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// Severity is the meaning of a message. The zero value is SeverityInfo. +type Severity int + +const ( + SeverityInfo Severity = iota + SeveritySuccess + SeverityWarning + SeverityError +) + +func (s Severity) String() string { + switch s { + case SeverityInfo: + return "info" + case SeveritySuccess: + return "success" + case SeverityWarning: + return "warning" + case SeverityError: + return "error" + } + panic(fmt.Sprintf("mujica: unknown Severity %d", int(s))) +} + +// severityLook returns the color and icon that, with text, mark s. +func severityLook(k theme.Tokens, s Severity) (ui.Color, *ui.SVG) { + switch s { + case SeveritySuccess: + return k.Success, icons.Must("circle-check") + case SeverityWarning: + return k.Warning, icons.Must("triangle-alert") + case SeverityError: + return k.Danger, icons.Must("circle-x") + } + return k.Info, icons.Must("info") +} + +// AlertOptions configure an Alert. +type AlertOptions struct { + Severity Severity + // Title goes above the message in bold; empty shows none. + Title string + // Action labels a button at the end; empty shows none. + Action string +} + +// AlertResult is what an Alert built and reported this frame. +type AlertResult struct { + // Element is nil while the alert is closed. + Element *ui.Element + // Action reports a click on the action button. + Action bool + // Closed reports the user dismissing the alert. + 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. +func Alert(c *ui.Context, open *bool, message string, opts AlertOptions) AlertResult { + if message == "" { + panic("mujica: Alert needs a message") + } + if open != nil && !*open { + return AlertResult{} + } + k := Tokens(c) + u := Density(c).Unit() + tone, icon := severityLook(k, opts.Severity) + name := msg(c, "alert."+opts.Severity.String()) + var res AlertResult + box := ui.Row(c).FillWidth().AlignItems(ui.Start).Gap(u*3).Padding(u*3, u*3, u*3, u*4). + Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border). + Role(ui.RoleGroup).Label(name).Clip() + // A bar in the severity color along the leading edge. + box.Draw(func(p *ui.Painter, r ui.Rect) { + p.Fill(ui.Rect{X: r.X, Y: r.Y, W: 3, H: r.H}, tone, 0) + }) + // Warnings and errors are read out when they appear or change. + said := ui.Local(box, "alert.said", func() string { return "" }) + if news := name + ": " + message; opts.Severity >= SeverityWarning && *said != news { + *said = news + c.Announce(news) + } + box.Children(func() { + ui.Icon(c, icon).Size(20, 20).TextColor(tone).Margin(1, 0, 0, 0) + ui.Column(c).Grow(1).Shrink(1).Gap(u).Children(func() { + if opts.Title != "" { + ui.Text(c, opts.Title).Bold().TextColor(k.Text) + } + ui.Text(c, message).TextColor(k.Text).LineHeight(theme.LineHeight) + if opts.Action != "" { + ui.Row(c).Margin(u, 0, 0, 0).Children(func() { + if Button(c, opts.Action, ButtonOptions{Variant: Outline}).Clicked() { + slog.Debug("mujica: alert action", "severity", opts.Severity, "action", opts.Action) + res.Action = true + } + }) + } + }) + if open != nil { + dismiss := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("x"), Label: msg(c, "alert.close")}) + dismiss.TextColor(k.TextMuted).Padding(0).MinHeight(theme.MinHitSize).Margin(-u, -u, -u, 0) + if dismiss.Clicked() { + slog.Debug("mujica: alert closed", "severity", opts.Severity) + *open, res.Closed = false, true + } + } + }) + res.Element = box + return res +} diff --git a/alert_test.go b/alert_test.go new file mode 100644 index 0000000..af93006 --- /dev/null +++ b/alert_test.go @@ -0,0 +1,113 @@ +package mujica + +import ( + "slices" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestAlertActionAndClose(t *testing.T) { + open := true + actions, closes := 0, 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + r := Alert(c, &open, "Disk almost full", AlertOptions{Severity: SeverityWarning, Title: "Storage", Action: "Free space"}) + if r.Action { + actions++ + } + if r.Closed { + closes++ + } + }, 500, 300) + if err := tt.Click("Free space"); err != nil { + t.Fatal(err) + } + if actions != 1 || !open { + t.Fatalf("actions %d open %v", actions, open) + } + if err := tt.Click("Dismiss"); err != nil { + t.Fatal(err) + } + if open || closes != 1 { + t.Fatalf("after dismiss open %v closes %d", open, closes) + } + if tt.HasText("Disk almost full") { + t.Fatal("closed alert still shows") + } +} + +func TestAlertKeyboard(t *testing.T) { + open := true + actions := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if Alert(c, &open, "Sync failed", AlertOptions{Severity: SeverityError, Action: "Retry"}).Action { + actions++ + } + }, 500, 300) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if actions != 1 { + t.Fatalf("Enter on action: %d", actions) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeySpace) + if open { + t.Fatal("Space on dismiss left it open") + } +} + +func TestAlertWithoutOpenHasNoDismiss(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Alert(c, nil, "Read only", AlertOptions{}) + }, 500, 300) + if _, ok := tt.Find("Dismiss"); ok { + t.Fatal("dismiss button without open") + } + if _, ok := tt.Find("Information"); !ok { + t.Fatal("alert lacks its severity name") + } +} + +func TestAlertAnnouncesWarningsOnce(t *testing.T) { + sev := SeverityInfo + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Locale: ZhCN}) + Alert(c, nil, "磁盘已满", AlertOptions{Severity: sev}) + }, 500, 300) + if a := tt.Announcements(); len(a) != 0 { + t.Fatalf("info announced %q", a) + } + sev = SeverityError + tt.Frame() + tt.Frame() + if a := tt.Announcements(); !slices.Equal(a, []string{"错误: 磁盘已满"}) { + t.Fatalf("error announcements %q", a) + } +} + +func TestAlertPanicsWithoutMessage(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Alert(c, nil, "", AlertOptions{}) + }, 300, 200) +} + +func TestAlertSnapshot(t *testing.T) { + open := true + snapshot(t, "alert", 560, 400, func(c *ui.Context) { + ui.Column(c).Padding(20).Gap(12).Children(func() { + Alert(c, nil, "A new version is ready to install.", AlertOptions{}) + Alert(c, &open, "Your changes were saved.", AlertOptions{Severity: SeveritySuccess}) + Alert(c, nil, "The certificate expires in 3 days.", AlertOptions{Severity: SeverityWarning, Title: "Certificate"}) + Alert(c, &open, "The archive could not be read.", AlertOptions{Severity: SeverityError, Title: "Import failed", Action: "Retry"}) + }) + }) +} diff --git a/empty_state.go b/empty_state.go new file mode 100644 index 0000000..67ca961 --- /dev/null +++ b/empty_state.go @@ -0,0 +1,88 @@ +package mujica + +import ( + "cmp" + "fmt" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// EmptyKind tells why there is nothing to show. The zero value is EmptyNoData. +type EmptyKind int + +const ( + // EmptyNoData: the collection itself is empty. + EmptyNoData EmptyKind = iota + // EmptyNoResults: a search or filter matched nothing. + EmptyNoResults +) + +// EmptyStateOptions configure an EmptyState. Empty texts use the kind's built-in copy. +type EmptyStateOptions struct { + Kind EmptyKind + Title string + Description string + // Query, for EmptyNoResults, is named in the default description. + Query string + // Icon replaces the kind's icon (inbox or search); NoIcon shows none. + Icon *ui.SVG + NoIcon bool + // Action labels a button, such as "Clear search"; empty shows none. + Action string +} + +// EmptyStateResult is what an EmptyState built and reported this frame. +type EmptyStateResult struct { + Element *ui.Element + Action bool +} + +// EmptyState explains an empty view and offers a next step. +func EmptyState(c *ui.Context, opts EmptyStateOptions) EmptyStateResult { + if opts.NoIcon && opts.Icon != nil { + panic("mujica: EmptyState with both Icon and NoIcon") + } + k := Tokens(c) + u := Density(c).Unit() + title, desc, icon := opts.Title, opts.Description, opts.Icon + switch opts.Kind { + case EmptyNoData: + if opts.Query != "" { + panic("mujica: EmptyState Query needs EmptyNoResults") + } + title = cmp.Or(title, msg(c, "empty_state.no_data.title")) + desc = cmp.Or(desc, msg(c, "empty_state.no_data.description")) + if icon == nil { + icon = icons.Must("inbox") + } + case EmptyNoResults: + title = cmp.Or(title, msg(c, "empty_state.no_results.title")) + if desc == "" && opts.Query != "" { + desc = fmt.Sprintf(msg(c, "empty_state.no_results.query"), opts.Query) + } + desc = cmp.Or(desc, msg(c, "empty_state.no_results.description")) + if icon == nil { + icon = icons.Must("search") + } + default: + panic(fmt.Sprintf("mujica: unknown EmptyKind %d", int(opts.Kind))) + } + var res EmptyStateResult + res.Element = ui.Column(c).FillWidth().AlignItems(ui.Center).Gap(u*2).Padding(u*8, u*4).Role(ui.RoleGroup).Label(title) + res.Element.Children(func() { + 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, 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() { + res.Action = Button(c, opts.Action, ButtonOptions{Variant: Secondary}).Clicked() + }) + } + }) + return res +} diff --git a/empty_state_test.go b/empty_state_test.go new file mode 100644 index 0000000..246b611 --- /dev/null +++ b/empty_state_test.go @@ -0,0 +1,81 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" +) + +func TestEmptyStateKinds(t *testing.T) { + opts := EmptyStateOptions{} + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + EmptyState(c, opts) + }, 500, 400) + if !tt.HasText("Nothing here yet") || !tt.HasText("Items you add will appear here.") { + t.Fatalf("no data: %q", tt.Texts()) + } + opts = EmptyStateOptions{Kind: EmptyNoResults, Query: "invoice"} + tt.Frame() + if !tt.HasText("No results") || !tt.HasText("Nothing matches “invoice”.") { + t.Fatalf("no results: %q", tt.Texts()) + } + opts = EmptyStateOptions{Kind: EmptyNoResults} + tt.Frame() + if !tt.HasText("Try other words or clear the filters.") { + t.Fatalf("no results without query: %q", tt.Texts()) + } +} + +func TestEmptyStateActionKeyboard(t *testing.T) { + n := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Locale: ZhCN}) + if EmptyState(c, EmptyStateOptions{Kind: EmptyNoResults, Query: "发票", Action: "清除搜索"}).Action { + n++ + } + }, 500, 400) + if !tt.HasText("没有与“发票”匹配的内容。") { + t.Fatalf("texts %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if err := tt.Click("清除搜索"); err != nil { + t.Fatal(err) + } + if n != 2 { + t.Fatalf("actions %d", n) + } +} + +func TestEmptyStatePanics(t *testing.T) { + for name, o := range map[string]EmptyStateOptions{ + "icon and no icon": {Icon: icons.Must("inbox"), NoIcon: true}, + "query without kind": {Query: "x"}, + "unknown kind": {Kind: 9}, + } { + func() { + defer func() { + if recover() == nil { + t.Errorf("%s: no panic", name) + } + }() + ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); EmptyState(c, o) }, 300, 200) + }() + } +} + +func TestEmptyStateSnapshot(t *testing.T) { + snapshot(t, "empty_state", 640, 280, func(c *ui.Context) { + ui.Row(c).Padding(12).Gap(12).Children(func() { + ui.Column(c).Grow(1).Basis(0).Children(func() { + EmptyState(c, EmptyStateOptions{Action: "New project"}) + }) + ui.Column(c).Grow(1).Basis(0).Children(func() { + EmptyState(c, EmptyStateOptions{Kind: EmptyNoResults, Query: "quarterly", Action: "Clear search"}) + }) + }) + }) +} diff --git a/loading_overlay.go b/loading_overlay.go new file mode 100644 index 0000000..6f8ba0d --- /dev/null +++ b/loading_overlay.go @@ -0,0 +1,94 @@ +package mujica + +import ( + "cmp" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// LoadingOverlayOptions configure a LoadingOverlay. +type LoadingOverlayOptions struct { + // Label explains the wait; empty uses "Loading…". + Label string + // Cancel labels a button that reports Canceled; empty shows none. + Cancel string +} + +// LoadingOverlayResult is what a LoadingOverlay built and reported this frame. +type LoadingOverlayResult struct { + Element *ui.Element + // Canceled reports the cancel button or Escape while loading. + 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. +func LoadingOverlay(c *ui.Context, loading bool, opts LoadingOverlayOptions, content func()) LoadingOverlayResult { + if content == nil { + panic("mujica: LoadingOverlay needs content") + } + k := Tokens(c) + u := Density(c).Unit() + label := cmp.Or(opts.Label, msg(c, "loading_overlay.label")) + var res LoadingOverlayResult + box := ui.Box(c) + was := ui.Local(box, "loading_overlay.loading", func() bool { return false }) + if loading != *was { + slog.Debug("mujica: loading_overlay changed", "loading", loading) + if loading { + c.Announce(label) + } + *was = loading + } + box.Children(func() { + inner := ui.Column(c).FillWidth().Disabled(loading) + inner.Children(content) + if !loading { + return + } + held := inner.FocusWithin() + scrim := ui.Column(c).Absolute().Left(0).Top(0).Right(0).Bottom(0).Center().Padding(u * 4). + Background(k.Background.Alpha(0.5)).Radius(theme.CardRadius) + scrim.Transition(ui.ElementTransition{Duration: theme.StateDuration, Enter: &ui.Motion{}, Exit: &ui.Motion{}}) + scrim.Children(func() { + panel := ui.Column(c).AlignItems(ui.Center).Gap(u*3).Padding(u*4, u*6).Radius(theme.PopupRadius). + Background(k.Surface).Border(theme.BorderWidth, k.Border).Role(ui.RoleStatus).Label(label) + if opts.Cancel == "" { + // Without a button the panel keeps the focus in the region. + internal.Ring(panel.Focusable(), k.Focus, theme.PopupRadius) + if held { + panel.Focus() + } + } + panel.Children(func() { + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { + Spinner(c, SpinnerOptions{Size: 20, Label: label}) + ui.Text(c, label).TextColor(k.Text) + }) + if opts.Cancel == "" { + return + } + b := Button(c, opts.Cancel, ButtonOptions{Variant: Secondary}) + if held { + b.Focus() + } + if b.Clicked() { + res.Canceled = true + } + }) + if opts.Cancel != "" && panel.Shortcut(0, ui.KeyEscape) { + res.Canceled = true + } + }) + }) + if res.Canceled { + slog.Debug("mujica: loading_overlay canceled") + } + res.Element = box + return res +} diff --git a/messages_feedback.go b/messages_feedback.go index c7ec2d0..527754c 100644 --- a/messages_feedback.go +++ b/messages_feedback.go @@ -3,5 +3,49 @@ package mujica func init() { addMessages(map[string][2]string{ "loading": {"Loading", "加载中"}, + + "alert.close": {"Dismiss", "关闭"}, + "alert.info": {"Information", "信息"}, + "alert.success": {"Success", "成功"}, + "alert.warning": {"Warning", "警告"}, + "alert.error": {"Error", "错误"}, + + "toast.close": {"Dismiss notification", "关闭通知"}, + "toast.region": {"Notifications", "通知"}, + + "progress.indeterminate": {"In progress", "进行中"}, + "meter.normal": {"Normal", "正常"}, + "meter.warning": {"High", "偏高"}, + "meter.critical": {"Critical", "严重"}, + "meter.low_warning": {"Low", "偏低"}, + + "empty_state.no_data.title": {"Nothing here yet", "暂无内容"}, + "empty_state.no_data.description": {"Items you add will appear here.", "添加的内容会显示在这里。"}, + "empty_state.no_results.title": {"No results", "没有结果"}, + "empty_state.no_results.description": {"Try other words or clear the filters.", "请换个关键词,或清除筛选条件。"}, + "empty_state.no_results.query": {"Nothing matches “%s”.", "没有与“%s”匹配的内容。"}, + + "result.success.title": {"Done", "操作成功"}, + "result.failure.title": {"Something went wrong", "操作失败"}, + "result.forbidden.title": {"No access", "无访问权限"}, + + "status_indicator.success": {"Healthy", "正常"}, + "status_indicator.warning": {"Degraded", "警告"}, + "status_indicator.error": {"Failing", "错误"}, + "status_indicator.offline": {"Offline", "离线"}, + "status_indicator.busy": {"Working", "处理中"}, + + "notification_center.title": {"Notifications", "通知"}, + "notification_center.unread": {"%d unread", "%d 条未读"}, + "notification_center.unread_mark": {"Unread", "未读"}, + "notification_center.mark_read": {"Mark read", "标为已读"}, + "notification_center.mark_unread": {"Mark unread", "标为未读"}, + "notification_center.mark_all_read": {"Mark all read", "全部已读"}, + "notification_center.clear": {"Clear", "清除"}, + "notification_center.clear_all": {"Clear all", "全部清除"}, + "notification_center.empty": {"You're all caught up", "没有新通知"}, + "notification_center.empty_hint": {"New notifications will appear here.", "新的通知会显示在这里。"}, + + "loading_overlay.label": {"Loading…", "正在加载…"}, }) } diff --git a/notification_center.go b/notification_center.go new file mode 100644 index 0000000..7b2cc8c --- /dev/null +++ b/notification_center.go @@ -0,0 +1,189 @@ +package mujica + +import ( + "cmp" + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// Notification is one in-app message. The caller owns and persists the list. +type Notification struct { + // ID is required and unique within the list. + ID string + Title string + Body string + // Time shows as given, as "5 min ago". + Time string + Read bool + // Action labels a button; empty shows none. + Action string +} + +// NotificationCenterOptions configure a NotificationCenter. +type NotificationCenterOptions struct { + // Title uses the built-in "Notifications" when empty. + Title string +} + +// NotificationCenterResult reports the frame's events by notification ID. +type NotificationCenterResult struct { + Element *ui.Element + Action string + // Toggled changed its Read state; Opened was clicked and is now read. + Toggled string + Opened string + Removed string + // MarkedAllRead and ClearedAll report the header buttons. + MarkedAllRead bool + 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. +func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationCenterOptions) NotificationCenterResult { + if items == nil { + panic("mujica: NotificationCenter needs items") + } + seen := map[string]bool{} + unread := 0 + for _, n := range *items { + if n.ID == "" || seen[n.ID] { + panic(fmt.Sprintf("mujica: notification ID %q empty or repeated", n.ID)) + } + seen[n.ID] = true + if !n.Read { + unread++ + } + } + k := Tokens(c) + u := Density(c).Unit() + title := cmp.Or(opts.Title, msg(c, "notification_center.title")) + var res NotificationCenterResult + panel := ui.Column(c).FillWidth().Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border). + 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) + if unread > 0 { + ui.Text(c, fmt.Sprintf(msg(c, "notification_center.unread"), unread)).FontSize(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() { + for i := range *items { + (*items)[i].Read = true + } + res.MarkedAllRead = true + slog.Debug("mujica: notification_center mark all read", "count", unread) + } + if Button(c, msg(c, "notification_center.clear_all"), ButtonOptions{Variant: Ghost, Disabled: len(*items) == 0}).Clicked() { + slog.Debug("mujica: notification_center clear all", "count", len(*items)) + *items = (*items)[:0] + res.ClearedAll = true + } + }) + // Ornament hairline under the panel title. + ui.Box(c).FillWidth().Height(1).Background(k.Ornament).Role(ui.RoleNone) + if len(*items) == 0 && !res.ClearedAll { + EmptyState(c, EmptyStateOptions{ + Title: msg(c, "notification_center.empty"), + Description: msg(c, "notification_center.empty_hint"), + Icon: icons.Must("bell"), + }) + return + } + list := ui.Scroll(c).Grow(1).Shrink(1).FillWidth().Role(ui.RoleList).FocusGroup(ui.Vertical | ui.Horizontal) + list.Dividers(theme.BorderWidth, k.Border) + list.Children(func() { + for i := range *items { + notificationRow(c, k, u, &(*items)[i], &res) + } + }) + }) + if res.Removed != "" { + for i, n := range *items { + if n.ID == res.Removed { + *items = append((*items)[:i], (*items)[i+1:]...) + break + } + } + } + res.Element = panel + return res +} + +func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, res *NotificationCenterResult) { + name := n.Title + if !n.Read { + name = msg(c, "notification_center.unread_mark") + ": " + n.Title + } + row := ui.Row(c).Key(n.ID).FillWidth().AlignItems(ui.Start).Gap(u*2).Padding(u*3, u*3, u*3, u*2). + Role(ui.RoleListItem).Label(name) + if row.Hovered() { + row.Background(k.SurfaceHover) + } + row.Children(func() { + // Unread mark: a diamond and bold title, not color alone. + ui.Box(c).Size(12, 20).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { + if !n.Read { + 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). + Radius(theme.ControlRadius).Label(name) + internal.Ring(open, k.Focus, theme.ControlRadius) + open.Children(func() { + ui.Row(c).FillWidth().Gap(u * 2).Children(func() { + t := ui.Text(c, n.Title).TextColor(k.Text).Grow(1).Shrink(1) + if !n.Read { + t.Bold() + } + if n.Time != "" { + ui.Text(c, n.Time).FontSize(theme.CaptionSize).TextColor(k.TextMuted).SingleLine() + } + }) + if n.Body != "" { + ui.Text(c, n.Body).TextColor(k.TextMuted).LineHeight(theme.LineHeight) + } + }) + if open.Clicked() && !n.Read { + n.Read, res.Opened = true, n.ID + slog.Debug("mujica: notification_center opened", "id", n.ID) + } + ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() { + if n.Action != "" && Button(c, n.Action, ButtonOptions{Variant: Outline}).Clicked() { + res.Action = n.ID + slog.Debug("mujica: notification_center action", "id", n.ID) + } + toggle, icon := "notification_center.mark_read", icons.Must("check") + if n.Read { + toggle, icon = "notification_center.mark_unread", icons.Must("circle") + } + if notificationIconButton(c, k, u, icon, msg(c, toggle)).Clicked() { + n.Read, res.Toggled = !n.Read, n.ID + slog.Debug("mujica: notification_center toggled", "id", n.ID, "read", n.Read) + } + if notificationIconButton(c, k, u, icons.Must("x"), msg(c, "notification_center.clear")).Clicked() { + res.Removed = n.ID + } + }) + }) + if row.Shortcut(0, ui.KeyDelete) || row.Shortcut(0, ui.KeyBackspace) { + res.Removed = n.ID + } + if res.Removed == n.ID { + slog.Debug("mujica: notification_center removed", "id", n.ID) + } +} + +func notificationIconButton(c *ui.Context, k theme.Tokens, u float32, icon *ui.SVG, label string) *ui.Element { + b := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icon, Label: label}) + b.TextColor(k.TextMuted).Padding(0, u).Tooltip(label) + return b +} diff --git a/progress.go b/progress.go new file mode 100644 index 0000000..c0059af --- /dev/null +++ b/progress.go @@ -0,0 +1,271 @@ +package mujica + +import ( + "fmt" + "math" + "strconv" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// ProgressShape is how a progress indicator draws. The zero value is ProgressLinear. +type ProgressShape int + +const ( + ProgressLinear ProgressShape = iota + ProgressRing +) + +// ProgressOptions configure a Progress. +type ProgressOptions struct { + Shape ProgressShape + // Indeterminate shows work of unknown length; the value is ignored. + Indeterminate bool + // Label names the indicator and shows beside it; empty reads "Loading". + Label string + ShowValue bool + // Size is the ring diameter (48 DIP when zero) or the bar width (fills when zero). + 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. +func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element { + 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") + } + k := Tokens(c) + u := Density(c).Unit() + still := c.Preferences().ReduceMotion + name := opts.Label + if name == "" { + name = msg(c, "loading") + } + acc := value + if opts.Indeterminate { + acc = -1 + } + caption := "" + switch { + case opts.Indeterminate && still: + caption = msg(c, "progress.indeterminate") + case !opts.Indeterminate && opts.ShowValue: + caption = strconv.Itoa(int(math.Round(value*100))) + "%" + } + if opts.Shape == ProgressRing { + return progressRing(c, k, u, acc, name, caption, opts, still) + } + col := ui.Column(c).Gap(u * 1.5) + if opts.Size > 0 { + col.Width(opts.Size) + } else { + col.FillWidth() + } + col.Children(func() { + 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() + }) + } + bar := ui.Progress(c, acc).Label(name).FillWidth().Height(u * 1.5).Radius(theme.ControlRadius).Background(k.Border) + shown := float32(value) + if !opts.Indeterminate { + shown = bar.Animate("progress.value", shown, theme.StateDuration) + } + bar.Draw(func(p *ui.Painter, r ui.Rect) { + switch { + 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) + default: + p.AnimationFrame() + phase := float32(p.Now().UnixMilli()%1400) / 1400 + w := r.W * 0.3 + x := r.X - w + (r.W+w)*phase + p.Clip(r, theme.ControlRadius, func() { + p.Fill(ui.Rect{X: x, Y: r.Y, W: w, H: r.H}, k.AccentText, theme.ControlRadius) + }) + } + }) + }) + return col +} + +func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, caption string, opts ProgressOptions, still bool) *ui.Element { + d := opts.Size + if d == 0 { + d = 48 + } + row := ui.Row(c).AlignItems(ui.Center).Gap(u * 2) + row.Children(func() { + ring := ui.Progress(c, acc).Label(name).Size(d, d).Radius(0).Background(ui.Transparent) + shown := float32(acc) + if !opts.Indeterminate { + shown = ring.Animate("progress.value", shown, theme.StateDuration) + } + stroke := max(3, d/10) + ring.Draw(func(p *ui.Painter, r ui.Rect) { + cx, cy, rad := r.X+r.W/2, r.Y+r.H/2, (r.W-stroke)/2 + var track ui.Path + track.Circle(cx, cy, rad) + p.StrokePath(&track, stroke, k.Border) + switch { + case !opts.Indeterminate: + if shown > 0 { + p.StrokePath(progressArc(cx, cy, rad, 0, shown), stroke, k.AccentText) + } + case still: + p.StrokePath(&track, stroke, k.AccentText.Alpha(0.4)) + default: + p.AnimationFrame() + start := float32(p.Now().UnixMilli()%1200) / 1200 + p.StrokePath(progressArc(cx, cy, rad, start, start+0.25), stroke, k.AccentText) + } + }) + if caption != "" && d >= 44 && !opts.Indeterminate { + ring.Children(func() { + ui.Text(c, caption).FontSize(theme.CaptionSize).FontFeatures("tnum").TextColor(k.Text) + }) + ring.Center() + } + if opts.Label != "" || (caption != "" && (d < 44 || opts.Indeterminate)) { + ui.Column(c).Children(func() { + if opts.Label != "" { + ui.Text(c, opts.Label).TextColor(k.Text) + } + if caption != "" && (d < 44 || opts.Indeterminate) { + ui.Text(c, caption).TextColor(k.TextMuted).FontSize(theme.CaptionSize) + } + }) + } + }) + return row +} + +// progressArc is the arc from turn a to turn b, clockwise from twelve o'clock. +func progressArc(cx, cy, r float32, a, b float32) *ui.Path { + var p ui.Path + n := max(2, int(math.Ceil(float64((b-a)*64)))) + for i := 0; i <= n; i++ { + t := float64(a + (b-a)*float32(i)/float32(n)) + x := cx + r*float32(math.Sin(2*math.Pi*t)) + y := cy - r*float32(math.Cos(2*math.Pi*t)) + if i == 0 { + p.MoveTo(x, y) + } else { + p.LineTo(x, y) + } + } + return &p +} + +// MeterOptions configure a Meter. +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 *ui.MeterLevels + Label string + ShowValue bool + // Width is the bar width; zero fills. + Width float32 +} + +// meterLevel is 0 normal, 1 warning, 2 critical. +func meterLevel(v float64, l *ui.MeterLevels) int { + if l == nil || l.Warning == l.Critical { + return 0 + } + low := l.Critical < l.Warning + switch { + case !low && v >= l.Critical, low && v <= l.Critical: + return 2 + case !low && v >= l.Warning, low && v <= l.Warning: + return 1 + } + 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. +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) { + 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") + } + k := Tokens(c) + u := Density(c).Unit() + level := meterLevel(value, opts.Levels) + tone := []ui.Color{k.Success, k.Warning, k.Danger}[level] + var word string + if opts.Levels != nil { + low := opts.Levels.Critical < opts.Levels.Warning + word = msg(c, "meter.normal") + switch { + case level == 2: + word = msg(c, "meter.critical") + case level == 1 && low: + word = msg(c, "meter.low_warning") + case level == 1: + word = msg(c, "meter.warning") + } + } + frac := func(v float64) float32 { return float32((v - lo) / (hi - lo)) } + col := ui.Column(c).Gap(u * 1.5) + if opts.Width > 0 { + col.Width(opts.Width) + } else { + col.FillWidth() + } + col.Children(func() { + caption := word + if opts.ShowValue { + caption = strconv.FormatFloat(value, 'f', -1, 64) + " / " + strconv.FormatFloat(hi, 'f', -1, 64) + if word != "" { + caption += " · " + word + } + } + 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() + }) + } + name := opts.Label + if word != "" { + name += " (" + word + ")" + } + bar := ui.Meter(c, value, lo, hi, opts.Levels).Label(name).FillWidth().Height(u * 2). + Radius(theme.ControlRadius).Background(k.Border) + bar.Draw(func(p *ui.Painter, r ui.Rect) { + p.Fill(ui.Rect{X: r.X, Y: r.Y, W: r.W * frac(value), H: r.H}, tone, theme.ControlRadius) + }) + if l := opts.Levels; l != nil { + // Ticks at the thresholds, over the fill. + bar.DrawOver(func(p *ui.Painter, r ui.Rect) { + for _, v := range []float64{l.Warning, l.Critical} { + x := float32(math.Round(float64(r.X + r.W*frac(v)))) + p.Fill(ui.Rect{X: x - 1, Y: r.Y, W: 2, H: r.H}, k.Text.Alpha(0.6), 0) + } + }) + } + }) + return col +} diff --git a/progress_test.go b/progress_test.go new file mode 100644 index 0000000..de54799 --- /dev/null +++ b/progress_test.go @@ -0,0 +1,127 @@ +package mujica + +import ( + "math" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestProgressValueText(t *testing.T) { + v := 0.424 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Progress(c, v, ProgressOptions{Label: "Uploading", ShowValue: true}) + Progress(c, 1, ProgressOptions{Shape: ProgressRing, Label: "Indexing", ShowValue: true}) + }, 400, 300) + if !tt.HasText("42%") || !tt.HasText("100%") { + t.Fatalf("texts %q", tt.Texts()) + } + if r, ok := tt.Find("Indexing"); !ok || r.H < 48 { + t.Fatalf("ring %v %v", r, ok) + } + v = 0 + tt.Frame() + if !tt.HasText("0%") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestProgressIndeterminateReducedMotion(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Progress(c, math.NaN(), ProgressOptions{Indeterminate: true, Label: "Connecting"}) + }, 400, 300) + if tt.HasText("In progress") { + t.Fatal("static note shown while moving") + } + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + if !tt.HasText("In progress") { + t.Fatal("no static note under reduced motion") + } + a := encode(t, tt.Image()) + tt.Frame() + if string(a) != string(encode(t, tt.Image())) { + t.Fatal("indeterminate bar moved under reduced motion") + } +} + +func TestProgressRingSize(t *testing.T) { + var w float32 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Row(c).Children(func() { + w = Progress(c, 0.5, ProgressOptions{Shape: ProgressRing, Size: 24}).Bounds().W + }) + }, 400, 300) + tt.Frame() + if w != 24 { + t.Fatalf("ring width %v", w) + } +} + +func TestMeterLevels(t *testing.T) { + disk := &ui.MeterLevels{Warning: 80, Critical: 95} + battery := &ui.MeterLevels{Warning: 20, Critical: 10} + for _, tc := range []struct { + v float64 + levels *ui.MeterLevels + want int + }{ + {50, disk, 0}, {80, disk, 1}, {94.9, disk, 1}, {95, disk, 2}, {100, disk, 2}, + {50, battery, 0}, {20, battery, 1}, {10, battery, 2}, {0, battery, 2}, {99, nil, 0}, + } { + if got := meterLevel(tc.v, tc.levels); got != tc.want { + t.Errorf("meterLevel(%v, %+v) = %d, want %d", tc.v, tc.levels, got, tc.want) + } + } +} + +func TestMeterWords(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Locale: ZhCN}) + Meter(c, 90, MeterOptions{Max: 100, Levels: &ui.MeterLevels{Warning: 80, Critical: 95}, Label: "磁盘", ShowValue: true}) + Meter(c, 15, MeterOptions{Max: 100, Levels: &ui.MeterLevels{Warning: 20, Critical: 10}, Label: "电量"}) + }, 400, 300) + if !tt.HasText("90 / 100 · 偏高") || !tt.HasText("偏低") { + t.Fatalf("texts %q", tt.Texts()) + } + if _, ok := tt.Find("磁盘 (偏高)"); !ok { + t.Fatal("meter name lacks its level") + } +} + +func TestProgressAndMeterPanics(t *testing.T) { + for name, f := range map[string]func(c *ui.Context){ + "progress above 1": func(c *ui.Context) { Progress(c, 1.2, ProgressOptions{}) }, + "progress NaN": func(c *ui.Context) { Progress(c, math.NaN(), ProgressOptions{}) }, + "meter empty range": func(c *ui.Context) { Meter(c, 5, MeterOptions{Min: 10, Max: 10}) }, + "meter outside": func(c *ui.Context) { Meter(c, 2, MeterOptions{}) }, + "meter levels": func(c *ui.Context) { Meter(c, 5, MeterOptions{Max: 10, Levels: &ui.MeterLevels{Warning: 8, Critical: 12}}) }, + } { + 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 TestProgressSnapshot(t *testing.T) { + snapshot(t, "progress", 520, 300, func(c *ui.Context) { + ui.Column(c).Padding(20).Gap(16).Children(func() { + Progress(c, 0.62, ProgressOptions{Label: "Uploading 3 files", ShowValue: true}) + Progress(c, 0, ProgressOptions{Indeterminate: true, Label: "Connecting"}) + ui.Row(c).Gap(24).AlignItems(ui.Center).Children(func() { + Progress(c, 0.35, ProgressOptions{Shape: ProgressRing, ShowValue: true}) + Progress(c, 0.8, ProgressOptions{Shape: ProgressRing, Size: 24, Label: "Indexing", ShowValue: true}) + Progress(c, 0, ProgressOptions{Shape: ProgressRing, Indeterminate: true, Label: "Waiting"}) + }) + Meter(c, 42, MeterOptions{Max: 100, Label: "Memory", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}}) + Meter(c, 93, MeterOptions{Max: 100, Label: "Disk", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}}) + }) + }) +} diff --git a/result.go b/result.go new file mode 100644 index 0000000..70814d0 --- /dev/null +++ b/result.go @@ -0,0 +1,97 @@ +package mujica + +import ( + "cmp" + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// ResultKind is the outcome a Result page reports. The zero value is ResultSuccess. +type ResultKind int + +const ( + ResultSuccess ResultKind = iota + ResultFailure + ResultForbidden +) + +// ResultOptions configure a Result. +type ResultOptions struct { + Kind ResultKind + // Title uses the kind's built-in copy when empty. + Title string + Description string + // Actions label the next steps; the first is the primary button. + Actions []string +} + +// ResultResult is what a Result built and reported this frame. +type ResultResult struct { + Element *ui.Element + // Action is the index of the clicked action, -1 for none. + Action int +} + +// Result reports the outcome of a finished task and offers next steps. +// It only reports the chosen step; the caller navigates. +func Result(c *ui.Context, opts ResultOptions) ResultResult { + k := Tokens(c) + u := Density(c).Unit() + var tone ui.Color + var icon *ui.SVG + var key string + switch opts.Kind { + case ResultSuccess: + tone, icon, key = k.Success, icons.Must("circle-check"), "result.success.title" + case ResultFailure: + tone, icon, key = k.Danger, icons.Must("circle-x"), "result.failure.title" + case ResultForbidden: + tone, icon, key = k.Warning, icons.Must("shield-off"), "result.forbidden.title" + default: + panic(fmt.Sprintf("mujica: unknown ResultKind %d", int(opts.Kind))) + } + title := cmp.Or(opts.Title, msg(c, key)) + res := ResultResult{Action: -1} + 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) + if opts.Description != "" { + ui.Text(c, opts.Description).TextColor(k.TextMuted).TextAlign(ui.Center).MaxWidth(420).LineHeight(theme.LineHeight) + } + resultOrnament(c, k) + if len(opts.Actions) == 0 { + return + } + ui.Row(c).Gap(u * 3).Wrap().Justify(ui.Center).Children(func() { + for i, a := range opts.Actions { + v := Secondary + if i == 0 { + v = Primary + } + if Button(c, a, ButtonOptions{Variant: v}).Key(i).Clicked() { + slog.Debug("mujica: result action", "kind", opts.Kind, "index", i) + res.Action = i + } + } + }) + }) + return res +} + +// resultOrnament draws two hairlines meeting a diamond, in ornament gold. +func resultOrnament(c *ui.Context, k theme.Tokens) { + ui.Box(c).Size(160, 12).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { + cy := float32(int(r.Y+r.H/2)) + 0.5 + cx := r.X + r.W/2 + p.Line(r.X, cy, cx-9, cy, 1, k.Ornament) + p.Line(cx+9, cy, r.X+r.W, cy, 1, k.Ornament) + p.FillPath(internal.Diamond(cx, cy, 8), k.Ornament) + }) +} diff --git a/result_test.go b/result_test.go new file mode 100644 index 0000000..b7ec69f --- /dev/null +++ b/result_test.go @@ -0,0 +1,66 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestResultActions(t *testing.T) { + got := []int{} + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if a := Result(c, ResultOptions{Kind: ResultFailure, Description: "The payment was declined.", Actions: []string{"Try again", "Back to cart"}}).Action; a >= 0 { + got = append(got, a) + } + }, 600, 500) + if !tt.HasText("Something went wrong") { + t.Fatalf("texts %q", tt.Texts()) + } + if err := tt.Click("Back to cart"); err != nil { + t.Fatal(err) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if len(got) != 2 || got[0] != 1 || got[1] != 0 { + t.Fatalf("actions %v", got) + } +} + +func TestResultTitles(t *testing.T) { + for kind, want := range map[ResultKind]string{ResultSuccess: "操作成功", ResultFailure: "操作失败", ResultForbidden: "无访问权限"} { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Locale: ZhCN}) + Result(c, ResultOptions{Kind: kind}) + }, 500, 400) + if !tt.HasText(want) { + t.Errorf("%d: texts %q", kind, tt.Texts()) + } + } + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Result(c, ResultOptions{Title: "Order placed"}) + }, 500, 400) + if !tt.HasText("Order placed") || tt.HasText("Done") { + t.Fatalf("custom title: %q", tt.Texts()) + } +} + +func TestResultPanicsOnUnknownKind(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); Result(c, ResultOptions{Kind: 7}) }, 300, 200) +} + +func TestResultSnapshot(t *testing.T) { + snapshot(t, "result", 520, 340, func(c *ui.Context) { + Result(c, ResultOptions{ + Kind: ResultForbidden, + Description: "You need the Editor role to change billing settings.", + Actions: []string{"Request access", "Go back"}, + }) + }) +} diff --git a/skeleton.go b/skeleton.go new file mode 100644 index 0000000..eefa1b5 --- /dev/null +++ b/skeleton.go @@ -0,0 +1,87 @@ +package mujica + +import ( + "math" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// SkeletonShape is the placeholder's form. The zero value is SkeletonText. +type SkeletonShape int + +const ( + SkeletonText SkeletonShape = iota + SkeletonCircle + SkeletonRect +) + +// SkeletonOptions configure a Skeleton. Sizes are fixed so content does not shift. +type SkeletonOptions struct { + Shape SkeletonShape + // Width fills the parent when zero; a circle uses it as its diameter (40 DIP when zero). + Width float32 + // Height is the rect height (ControlHeight when zero); text derives it from Lines. + Height float32 + // Lines is the text line count, 1 when zero; the last of several is shorter. + Lines int +} + +// Skeleton stands in for content that is loading. It pulses while painted +// and shows a static fill 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") + } + k := Tokens(c) + still := c.Preferences().ReduceMotion + fill := k.SurfaceHover + if k.Dark { + fill = k.Border + } + paint := func(p *ui.Painter, r ui.Rect, radius float32) { + a := float32(1) + if !still { + p.AnimationFrame() + t := float64(p.Now().UnixMilli()%1600) / 1600 + a = 0.55 + 0.45*float32(0.5+0.5*math.Cos(2*math.Pi*t)) + } + p.Fill(r, fill.Alpha(a), radius) + } + e := ui.Box(c).Role(ui.RoleNone) + if opts.Width > 0 { + e.Width(opts.Width) + } else if opts.Shape != SkeletonCircle { + e.FillWidth() + } + switch opts.Shape { + case SkeletonCircle: + d := opts.Width + if d == 0 { + d = 40 + } + e.Size(d, d).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { paint(p, r, r.W/2) }) + case SkeletonRect: + h := opts.Height + if h == 0 { + h = Density(c).ControlHeight() + } + 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 + e.Height(line * float32(lines)).Draw(func(p *ui.Painter, r ui.Rect) { + bar := theme.BodySize * 0.8 + for i := range lines { + w := r.W + if lines > 1 && i == lines-1 { + w *= 0.6 + } + y := r.Y + float32(i)*line + (line-bar)/2 + paint(p, ui.Rect{X: r.X, Y: y, W: w, H: bar}, theme.ControlRadius) + } + }) + } + return e +} diff --git a/skeleton_test.go b/skeleton_test.go new file mode 100644 index 0000000..a9b2661 --- /dev/null +++ b/skeleton_test.go @@ -0,0 +1,83 @@ +package mujica + +import ( + "bytes" + "testing" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +func TestSkeletonSizes(t *testing.T) { + var text, circle, rect, fixed ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Density: theme.Compact}) + ui.Column(c).Width(300).AlignItems(ui.Start).Children(func() { + text = Skeleton(c, SkeletonOptions{Lines: 3}).Bounds() + circle = Skeleton(c, SkeletonOptions{Shape: SkeletonCircle}).Bounds() + rect = Skeleton(c, SkeletonOptions{Shape: SkeletonRect}).Bounds() + fixed = Skeleton(c, SkeletonOptions{Shape: SkeletonRect, Width: 120, Height: 80}).Bounds() + }) + }, 400, 400) + tt.Frame() + if text.W != 300 || text.H != 63 { + t.Errorf("text %v", text) + } + if circle.W != 40 || circle.H != 40 { + t.Errorf("circle %v", circle) + } + if rect.W != 300 || rect.H != 28 { + t.Errorf("rect %v", rect) + } + if fixed.W != 120 || fixed.H != 80 { + t.Errorf("fixed %v", fixed) + } +} + +func TestSkeletonStaticUnderReducedMotion(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Skeleton(c, SkeletonOptions{Shape: SkeletonRect}) + }, 200, 100) + a := encode(t, tt.Image()) + time.Sleep(300 * time.Millisecond) + tt.Frame() + if bytes.Equal(a, encode(t, tt.Image())) { + t.Fatal("skeleton did not pulse") + } + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + a = encode(t, tt.Image()) + time.Sleep(300 * time.Millisecond) + tt.Frame() + if !bytes.Equal(a, encode(t, tt.Image())) { + t.Fatal("skeleton moved under reduced motion") + } +} + +func TestSkeletonPanicsOnNegativeSize(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Skeleton(c, SkeletonOptions{Lines: -1}) + }, 200, 100) +} + +func TestSkeletonSnapshot(t *testing.T) { + snapshot(t, "skeleton", 420, 200, func(c *ui.Context) { + ui.Column(c).Padding(20).Gap(16).Children(func() { + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + Skeleton(c, SkeletonOptions{Shape: SkeletonCircle}) + ui.Column(c).Grow(1).Children(func() { + Skeleton(c, SkeletonOptions{Lines: 2}) + }) + }) + Skeleton(c, SkeletonOptions{Shape: SkeletonRect, Height: 72}) + }) + }) +} diff --git a/spinner.go b/spinner.go index b93ca02..bb4ef74 100644 --- a/spinner.go +++ b/spinner.go @@ -19,6 +19,7 @@ type SpinnerOptions struct { } // Spinner draws four diamonds turning around a center while it shows. +// It repaints only while painted: hidden, it stops. Reduced motion holds it still. func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element { k := Tokens(c) size := opts.Size @@ -34,9 +35,10 @@ func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element { color = k.AccentText } still := c.Preferences().ReduceMotion - row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Role(ui.RoleProgress).Label(label) + row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center) row.Children(func() { - ui.Box(c).Size(size, size).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { + // MyGo's spinner carries the indeterminate progress role. + ui.Spinner(c).Label(label).Size(size, size).Draw(func(p *ui.Painter, r ui.Rect) { turn := 0.0 if !still { turn = float64(p.Now().UnixMilli()%1200) / 1200 diff --git a/spinner_test.go b/spinner_test.go new file mode 100644 index 0000000..47a7abb --- /dev/null +++ b/spinner_test.go @@ -0,0 +1,77 @@ +package mujica + +import ( + "bytes" + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +func TestSpinnerLabelAndSize(t *testing.T) { + var mark ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Locale: ZhCN}) + Spinner(c, SpinnerOptions{}) + Spinner(c, SpinnerOptions{Size: 32, Label: "Fetching mail", ShowLabel: true}) + }, 400, 300) + r, ok := tt.Find("加载中") + if !ok || r.W != 16 || r.H != 16 { + t.Fatalf("default mark %v %v", r, ok) + } + if tt.HasText("加载中") { + t.Fatal("hidden label painted") + } + mark, ok = tt.Find("Fetching mail") + if !ok || mark.W != 32 { + t.Fatalf("sized mark %v %v", mark, ok) + } + n := 0 + for _, s := range tt.Texts() { + if s == "Fetching mail" { + n++ + } + } + if n != 2 { + t.Fatalf("label as name and text: %d of %q", n, tt.Texts()) + } +} + +func TestSpinnerMotion(t *testing.T) { + show := true + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if show { + Spinner(c, SpinnerOptions{Size: 40}) + } + }, 100, 100) + a := encode(t, tt.Image()) + time.Sleep(150 * time.Millisecond) + tt.Frame() + if bytes.Equal(a, encode(t, tt.Image())) { + t.Fatal("spinner did not turn") + } + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + a = encode(t, tt.Image()) + time.Sleep(150 * time.Millisecond) + tt.Frame() + if !bytes.Equal(a, encode(t, tt.Image())) { + t.Fatal("spinner turned under reduced motion") + } + show = false + tt.Frame() + if _, ok := tt.Find("Loading"); ok { + t.Fatal("hidden spinner still built") + } +} + +func TestSpinnerSnapshot(t *testing.T) { + snapshot(t, "spinner", 360, 100, func(c *ui.Context) { + ui.Row(c).Padding(24).Gap(24).AlignItems(ui.Center).Children(func() { + Spinner(c, SpinnerOptions{}) + Spinner(c, SpinnerOptions{Size: 24}) + Spinner(c, SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) + Button(c, "Saving", ButtonOptions{Loading: true}) + }) + }) +} diff --git a/status_indicator.go b/status_indicator.go new file mode 100644 index 0000000..045c642 --- /dev/null +++ b/status_indicator.go @@ -0,0 +1,77 @@ +package mujica + +import ( + "fmt" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// Status is a system or item state. The zero value is StatusSuccess. +type Status int + +const ( + StatusSuccess Status = iota + StatusWarning + StatusError + StatusOffline + StatusBusy +) + +func (s Status) String() string { + switch s { + case StatusSuccess: + return "success" + case StatusWarning: + return "warning" + case StatusError: + return "error" + case StatusOffline: + return "offline" + case StatusBusy: + return "busy" + } + panic(fmt.Sprintf("mujica: unknown Status %d", int(s))) +} + +// StatusIndicatorOptions configure a StatusIndicator. +type StatusIndicatorOptions struct { + // Label replaces the status's built-in word, as "Database online". + Label string +} + +// 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 { + k := Tokens(c) + u := Density(c).Unit() + label := opts.Label + if label == "" { + label = msg(c, "status_indicator."+status.String()) + } + var tone ui.Color + var icon *ui.SVG + switch status { + case StatusSuccess: + tone, icon = k.Success, icons.Must("circle-check") + case StatusWarning: + tone, icon = k.Warning, icons.Must("triangle-alert") + case StatusError: + tone, icon = k.Danger, icons.Must("circle-x") + case StatusOffline: + tone, icon = k.TextMuted, icons.Must("wifi-off") + case StatusBusy: + tone = k.Info + } + row := ui.Row(c).AlignItems(ui.Center).Gap(u * 1.5).Role(ui.RoleStatus).Label(label) + row.Children(func() { + if status == StatusBusy { + Spinner(c, SpinnerOptions{Size: 16, Color: tone, Label: label}) + } else { + ui.Icon(c, icon).Size(16, 16).TextColor(tone) + } + ui.Text(c, label).TextColor(k.Text).FontSize(theme.BodySize).SingleLine() + }) + return row +} diff --git a/status_indicator_test.go b/status_indicator_test.go new file mode 100644 index 0000000..b47df01 --- /dev/null +++ b/status_indicator_test.go @@ -0,0 +1,69 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestStatusIndicatorWords(t *testing.T) { + want := map[Status]string{ + StatusSuccess: "Healthy", StatusWarning: "Degraded", StatusError: "Failing", + StatusOffline: "Offline", StatusBusy: "Working", + } + for s, word := range want { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + StatusIndicator(c, s, StatusIndicatorOptions{}) + }, 300, 100) + if !tt.HasText(word) { + t.Errorf("%v: texts %q", s, tt.Texts()) + } + } + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Locale: ZhCN}) + StatusIndicator(c, StatusOffline, StatusIndicatorOptions{}) + StatusIndicator(c, StatusSuccess, StatusIndicatorOptions{Label: "Database online"}) + }, 300, 100) + if !tt.HasText("离线") || !tt.HasText("Database online") || tt.HasText("正常") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestStatusIndicatorShapesDiffer(t *testing.T) { + // The same tone would still differ by shape: render each in one color. + var imgs [][]byte + for _, s := range []Status{StatusSuccess, StatusWarning, StatusError, StatusOffline} { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + StatusIndicator(c, s, StatusIndicatorOptions{Label: "Same"}) + }, 120, 40) + imgs = append(imgs, encode(t, tt.Image())) + } + for i := range imgs { + for j := i + 1; j < len(imgs); j++ { + if string(imgs[i]) == string(imgs[j]) { + t.Errorf("statuses %d and %d draw alike", i, j) + } + } + } +} + +func TestStatusIndicatorPanicsOnUnknown(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); StatusIndicator(c, 42, StatusIndicatorOptions{}) }, 300, 100) +} + +func TestStatusIndicatorSnapshot(t *testing.T) { + snapshot(t, "status_indicator", 560, 80, func(c *ui.Context) { + ui.Row(c).Padding(24).Gap(20).Children(func() { + for _, s := range []Status{StatusSuccess, StatusWarning, StatusError, StatusOffline, StatusBusy} { + StatusIndicator(c, s, StatusIndicatorOptions{}) + } + }) + }) +} diff --git a/testdata/snapshots/darwin/alert-dark.png b/testdata/snapshots/darwin/alert-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ff9faed551ad829824f188ca0cf1ef85a1f74664 GIT binary patch literal 26555 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 sZV+kr(nnv+ER_F%BmP4o`xr$CvJ*|-_2|KEGR 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/progress-dark.png b/testdata/snapshots/darwin/progress-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9d537d3bc7843c10b4802619e9dc1d7861b01fc3 GIT binary patch literal 23304 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 Wc4kVadF`-&!>Oz2C^tN{i~cY65n0>- literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/progress-light.png b/testdata/snapshots/darwin/progress-light.png new file mode 100644 index 0000000000000000000000000000000000000000..9d380a6e83bf9583f1182d05b880191589156ece GIT binary patch literal 22394 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 X*V-kvU6}mOzrAUx>#CK)tit{Solq%v literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/toast-dark.png b/testdata/snapshots/darwin/toast-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b7888e80d5ce90c3b3e749a9428f72f3fe6780e8 GIT binary patch literal 15914 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 z 0: + return m.Duration + case m.Severity == SeverityError: + return 0 + } + return ToastDuration +} + +func (q *toastQueue) push(m ToastMessage, now time.Time) { + if m.Message == "" { + panic("mujica: toast needs a Message") + } + if m.ID == "" { + m.ID = m.Message + } + for i := range q.items { + it := &q.items[i] + if it.msg.ID != m.ID { + continue + } + it.msg, it.life, it.spent, it.announced = m, toastLife(m), 0, false + if it.visible && !it.runFrom.IsZero() { + it.runFrom = now + } + slog.Debug("mujica: toast replaced", "id", m.ID) + return + } + q.items = append(q.items, toastItem{msg: m, life: toastLife(m)}) + slog.Debug("mujica: toast queued", "id", m.ID) + q.promote(now) +} + +// promote shows queued toasts while fewer than ToastVisible show. +func (q *toastQueue) promote(now time.Time) { + n := 0 + for i := range q.items { + it := &q.items[i] + if !it.visible && n < ToastVisible { + it.visible, it.runFrom = true, now + slog.Debug("mujica: toast shown", "id", it.msg.ID) + } + if it.visible { + n++ + } + } +} + +func (it *toastItem) elapsed(now time.Time) time.Duration { + if it.runFrom.IsZero() { + return it.spent + } + 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) { + live := q.items[:0] + for _, it := range q.items { + if it.visible && it.life > 0 && it.elapsed(now) >= it.life { + slog.Debug("mujica: toast expired", "id", it.msg.ID) + continue + } + live = append(live, it) + } + q.items = live + q.promote(now) + for _, it := range q.items { + if it.visible && it.life > 0 && !it.runFrom.IsZero() { + left := it.life - it.elapsed(now) + if next == 0 || left < next { + next = left + } + } + } + return next +} + +func (q *toastQueue) pause(id string, paused bool, now time.Time) { + for i := range q.items { + it := &q.items[i] + if it.msg.ID != id || !it.visible { + continue + } + switch { + case paused && !it.runFrom.IsZero(): + it.spent, it.runFrom = it.elapsed(now), time.Time{} + slog.Debug("mujica: toast paused", "id", id) + case !paused && it.runFrom.IsZero(): + it.runFrom = now + slog.Debug("mujica: toast resumed", "id", id) + } + } +} + +func (q *toastQueue) remove(id string, now time.Time) { + for i, it := range q.items { + if it.msg.ID == id { + q.items = append(q.items[:i], q.items[i+1:]...) + break + } + } + q.promote(now) +} + +func toasts(c *ui.Context) *toastQueue { + return ui.Local(c.Root(), toastKey{}, func() toastQueue { return toastQueue{} }) +} + +// ShowToast queues m in the window. Call it on the UI thread. +func ShowToast(c *ui.Context, m ToastMessage) { + toasts(c).push(m, c.Now()) + c.Invalidate() +} + +// DismissToast closes the toast with id, shown or queued. +func DismissToast(c *ui.Context, id string) { + toasts(c).remove(id, c.Now()) + 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. +func Toaster(c *ui.Context) ToasterResult { + q := toasts(c) + now := c.Now() + if next := q.expire(now); next > 0 { + c.After(next) + } + var res ToasterResult + if len(q.items) == 0 { + return res + } + k := Tokens(c) + u := Density(c).Unit() + w, _ := c.Size() + ui.Overlay(c, func() { + stack := ui.Column(c).Absolute().Right(u * 4).Bottom(u * 4).Gap(u * 2).AlignItems(ui.End). + PassThrough().Role(ui.RoleGroup).Label(msg(c, "toast.region")) + stack.Children(func() { + for i := range q.items { + it := q.items[i] + if !it.visible { + continue + } + if !it.announced { + 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) + switch { + case action: + res.Action = it.msg.ID + slog.Debug("mujica: toast action", "id", it.msg.ID) + case closed: + res.Closed = it.msg.ID + slog.Debug("mujica: toast closed", "id", it.msg.ID) + } + } + }) + }) + for _, id := range []string{res.Action, res.Closed} { + if id != "" { + q.remove(id, now) + c.Invalidate() + } + } + return res +} + +// toastCard builds one toast and reports its events and whether hover or focus holds it. +func toastCard(c *ui.Context, k theme.Tokens, u, width float32, m ToastMessage) (action, closed, held bool) { + tone, icon := severityLook(k, m.Severity) + card := ui.Row(c).Key(m.ID).Width(width).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4). + Radius(theme.PopupRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder). + Role(ui.RoleStatus).Label(m.Message) + card.Shadow(0, 4, 12, 0, ui.RGBA(0, 0, 0, 0.14)) + card.Transition(ui.ElementTransition{ + Duration: theme.PopupDuration, + Enter: &ui.Motion{Y: 8}, + Exit: &ui.Motion{}, + }) + card.Draw(func(p *ui.Painter, r ui.Rect) { + p.Fill(ui.Rect{X: r.X, Y: r.Y + theme.PopupRadius, W: 3, H: r.H - 2*theme.PopupRadius}, tone, 0) + }) + card.Children(func() { + ui.Icon(c, icon).Size(20, 20).TextColor(tone) + ui.Text(c, m.Message).TextColor(k.Text).Grow(1).Shrink(1).LineHeight(theme.LineHeight) + if m.Action != "" && Button(c, m.Action, ButtonOptions{Variant: Ghost}).Clicked() { + action = true + } + x := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("x"), Label: msg(c, "toast.close")}) + x.TextColor(k.TextMuted).Padding(0, u) + if x.Clicked() { + closed = true + } + }) + if card.Shortcut(0, ui.KeyEscape) { + closed = true + } + return action, closed, card.Hovered() || card.FocusWithin() +} diff --git a/toast_test.go b/toast_test.go new file mode 100644 index 0000000..d4425ba --- /dev/null +++ b/toast_test.go @@ -0,0 +1,193 @@ +package mujica + +import ( + "slices" + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +func toastIDs(q *toastQueue, visible bool) []string { + var ids []string + for _, it := range q.items { + if it.visible == visible { + ids = append(ids, it.msg.ID) + } + } + return ids +} + +func TestToastQueueLimitsVisible(t *testing.T) { + var q toastQueue + t0 := time.Unix(0, 0) + for _, id := range []string{"a", "b", "c", "d", "e"} { + q.push(ToastMessage{ID: id, Message: id}, t0) + } + if v, w := toastIDs(&q, true), toastIDs(&q, false); !slices.Equal(v, []string{"a", "b", "c"}) || !slices.Equal(w, []string{"d", "e"}) { + 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 { + t.Fatalf("next wake %v", next) + } + if v := toastIDs(&q, true); !slices.Equal(v, []string{"d", "e"}) { + t.Fatalf("after expiry visible %v", v) + } + q.expire(t0.Add(2*ToastDuration - time.Millisecond)) + if len(q.items) != 2 { + t.Fatalf("expired early: %v", toastIDs(&q, true)) + } + q.expire(t0.Add(2 * ToastDuration)) + if len(q.items) != 0 { + t.Fatalf("left %v", toastIDs(&q, true)) + } +} + +func TestToastQueueDedupesByID(t *testing.T) { + var q toastQueue + t0 := time.Unix(0, 0) + q.push(ToastMessage{ID: "sync", Message: "Syncing 1 file"}, t0) + q.push(ToastMessage{Message: "Saved"}, t0) + q.push(ToastMessage{ID: "sync", Message: "Syncing 2 files"}, t0.Add(4*time.Second)) + q.push(ToastMessage{Message: "Saved"}, t0.Add(4*time.Second)) + if len(q.items) != 2 || q.items[0].msg.Message != "Syncing 2 files" || q.items[1].msg.ID != "Saved" { + t.Fatalf("items %+v", q.items) + } + // The replaced toast restarted its time. + q.expire(t0.Add(6 * time.Second)) + if len(q.items) != 2 { + t.Fatalf("replacement kept the old time: %d left", len(q.items)) + } +} + +func TestToastQueueErrorsStayAndPauseHolds(t *testing.T) { + var q toastQueue + 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 { + 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 { + t.Fatalf("resumed toast left %v, want 1s", next) + } + q.expire(t0.Add(time.Hour + time.Second)) + if !slices.Equal(toastIDs(&q, true), []string{"e"}) { + t.Fatalf("visible %v", toastIDs(&q, true)) + } +} + +func TestToastQueuePanics(t *testing.T) { + for _, m := range []ToastMessage{{}, {Message: "x", Duration: -1}} { + func() { + defer func() { + if recover() == nil { + t.Errorf("%+v: no panic", m) + } + }() + var q toastQueue + q.push(m, time.Now()) + }() + } +} + +func toastTester(m ToastMessage, res *ToasterResult) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if Button(c, "Notify", ButtonOptions{}).Clicked() { + ShowToast(c, m) + } + if r := Toaster(c); r != (ToasterResult{}) { + *res = r + } + }, 600, 400) +} + +func TestToastActionAndClose(t *testing.T) { + var res ToasterResult + tt := toastTester(ToastMessage{ID: "del", Message: "Note deleted", Action: "Undo"}, &res) + if err := tt.Click("Notify"); err != nil { + t.Fatal(err) + } + if !tt.HasText("Note deleted") { + t.Fatal("toast not shown") + } + if err := tt.Click("Undo"); err != nil { + t.Fatal(err) + } + if res.Action != "del" || tt.HasText("Note deleted") { + t.Fatalf("after undo %+v, still shown %v", res, tt.HasText("Note deleted")) + } + res = ToasterResult{} + _ = tt.Click("Notify") + if err := tt.Click("Dismiss notification"); err != nil { + t.Fatal(err) + } + if res.Closed != "del" || tt.HasText("Note deleted") { + t.Fatalf("after close %+v", res) + } +} + +func TestToastKeepsFocusAndEscapeCloses(t *testing.T) { + var res ToasterResult + tt := toastTester(ToastMessage{ID: "s", Message: "Saved"}, &res) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if !tt.HasText("Saved") || !tt.Focused("Notify") { + t.Fatalf("shown %v, focus kept %v", tt.HasText("Saved"), tt.Focused("Notify")) + } + if a := tt.Announcements(); !slices.Contains(a, "Saved") { + t.Fatalf("announcements %q", a) + } + tt.Key(0, ui.KeyTab) + if !tt.Focused("Dismiss notification") { + t.Fatal("Tab does not reach the toast") + } + tt.Key(0, ui.KeyEscape) + if res.Closed != "s" || tt.HasText("Saved") { + t.Fatalf("Escape: %+v", res) + } +} + +func TestToastExpiresAndHoverPauses(t *testing.T) { + var res ToasterResult + tt := toastTester(ToastMessage{Message: "Copied", Duration: 40 * time.Millisecond}, &res) + _ = tt.Click("Notify") + time.Sleep(60 * time.Millisecond) + tt.Frame() + if tt.HasText("Copied") { + t.Fatal("toast outlived its duration") + } + _ = 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 !tt.HasText("Copied") { + t.Fatal("hovered toast expired") + } + tt.Move(5, 395) + time.Sleep(60 * time.Millisecond) + tt.Frame() + if tt.HasText("Copied") { + t.Fatal("toast stayed after the pointer left") + } +} + +func TestToastSnapshot(t *testing.T) { + snapshot(t, "toast", 480, 300, func(c *ui.Context) { + if len(toasts(c).items) == 0 { + ShowToast(c, ToastMessage{ID: "1", Message: "Settings saved", Severity: SeveritySuccess}) + ShowToast(c, ToastMessage{ID: "2", Message: "Note moved to trash", Action: "Undo"}) + ShowToast(c, ToastMessage{ID: "3", Message: "Upload failed: network lost", Severity: SeverityError}) + ShowToast(c, ToastMessage{ID: "4", Message: "Queued and not shown"}) + } + Toaster(c) + }) +}