Merge branch 'worktree-agent-ab9ba373bc47c2352'

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 13:03:31 -04:00
commit bb5828da96
6 files changed
+122 -28

No files matched your search

+2 -2
View File
@@ -42,7 +42,7 @@ mujica.Meter(c, 93, mujica.MeterOptions{
| `Levels` | `*ui.MeterLevels` | `nil` | 阈值:达到 `Warning` 为偏高、达到 `Critical` 为严重;`Critical < Warning` 时低值为坏(如电量)。须在范围内。 |
| `Label` | `string` | `""` | 名称。 |
| `ShowValue` | `bool` | `false` | 显示“值 / 最大值”。 |
| `Width` | `float32` | 填满 | 宽度。 |
| `Width` | `float32` | 填满 | 宽度;负数、NaN 或无穷 panic。 |
## 状态
@@ -60,5 +60,5 @@ mujica.Meter(c, 93, mujica.MeterOptions{
## 限制
- 环形直径小于 44 DIP 时百分比显示在旁边,而不是环内。
- 百分比按缩放后的文字尺寸测量,放不进环内时显示在环旁边。
- MyGo 不提供自定义无障碍数值的公开接口,因此必须基于 `ui.Progress` / `ui.Meter` 元素绘制。
+75
View File
@@ -60,6 +60,9 @@ func TestFeedbackRejectsInvalidInput(t *testing.T) {
"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}) },
"meter negative width": func(c *ui.Context) { Meter(c, 1, MeterOptions{Max: 2, Width: -1}) },
"meter NaN width": func(c *ui.Context) { Meter(c, 1, MeterOptions{Max: 2, Width: float32(nan)}) },
"meter infinite width": func(c *ui.Context) { Meter(c, 1, MeterOptions{Max: 2, Width: float32(inf)}) },
} {
func() {
defer func() {
@@ -82,3 +85,75 @@ func TestNotificationTitleOnlyHitHeight(t *testing.T) {
t.Fatalf("title-only notification hit box %v %v", r, ok)
}
}
func TestNotificationHeaderWrapsAtDoubleText(t *testing.T) {
items := []Notification{{ID: "a", Title: "Build failed"}, {ID: "b", Title: "Backup complete", Read: true}}
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
ui.Column(c).Width(520).Height(700).Children(func() {
NotificationCenter(c, &items, NotificationCenterOptions{})
})
}, 600, 760)
tt.SetPreferences(ui.Preferences{TextScale: 2})
for _, s := range []string{"Notifications", "1 unread", "Mark all read", "Clear all"} {
if r, ok := tt.Find(s); !ok || r.W == 0 || r.H == 0 || r.X < 0 || r.X+r.W > 520 {
t.Errorf("%q at 200%%: %v %v", s, r, ok)
}
}
if err := tt.Click("Clear all"); err != nil {
t.Fatal(err)
}
if len(items) != 0 {
t.Fatalf("Clear all at 200%% left %d items", len(items))
}
}
func TestProgressRingCaptionFitsAtDoubleText(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
ui.Row(c).Children(func() {
Progress(c, 1, ProgressOptions{Shape: ProgressRing, ShowValue: true})
})
}, 400, 200)
for _, scale := range []float32{1, 2} {
tt.SetPreferences(ui.Preferences{TextScale: scale})
tt.Frame()
ring, ok := tt.Find("Loading")
text, ok2 := tt.Find("100%")
if !ok || !ok2 {
t.Fatalf("scale %v: ring %v text %v", scale, ok, ok2)
}
stroke := max(float32(3), ring.W/10)
inside := text.X >= ring.X+stroke && text.X+text.W <= ring.X+ring.W-stroke
outside := text.X >= ring.X+ring.W
if !inside && !outside {
t.Errorf("scale %v: caption %v clipped by ring %v", scale, text, ring)
}
if scale == 1 && !inside {
t.Errorf("caption left the ring at 100%%: %v", text)
}
}
}
func TestProgressResumesFromDisplayedValueAfterReducedMotion(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)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
v = 0.5
tt.Frame()
tt.SetPreferences(ui.Preferences{TextScale: 1})
if got, want := progressPixel(tt, bar.X+bar.W*0.45, bar.Y+bar.H/2), theme.Light().AccentText; got != want {
t.Fatalf("after motion returned the fill at 45%% is %v, want %v", got, want)
}
v = 1
tt.Frame()
if got := progressPixel(tt, bar.X+bar.W*0.95, bar.Y+bar.H/2); got == theme.Light().AccentText {
t.Fatal("restored motion did not animate the next change")
}
}
+21 -17
View File
@@ -67,24 +67,28 @@ func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationC
panel := ui.Column(c).FillWidth().Shrink(1).MinHeight(0).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(FontSize(c, theme.H3Size)).TextColor(k.Text)
if unread > 0 {
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() {
for i := range *items {
(*items)[i].Read = true
// Wraps so title and actions stay whole when text is enlarged.
ui.Row(c).Wrap().AlignItems(ui.Center).Justify(ui.SpaceBetween).Gap(u*2).Padding(u*3, u*3, u*3, u*4).Children(func() {
ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() {
ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text).SingleLine()
if unread > 0 {
ui.Text(c, fmt.Sprintf(msg(c, "notification_center.unread"), unread)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText).SingleLine()
}
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
}
})
ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() {
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)
+24 -9
View File
@@ -78,8 +78,8 @@ func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element {
}
bar := ui.Progress(c, acc).Label(name).FillWidth().Height(u * 1.5).Radius(theme.ControlRadius).Background(k.Border)
shown := float32(value)
if !opts.Indeterminate && !still {
shown = bar.Animate("progress.value", shown, theme.StateDuration)
if !opts.Indeterminate {
shown = progressAnimate(bar, shown, still)
}
bar.Draw(func(p *ui.Painter, r ui.Rect) {
switch {
@@ -110,10 +110,13 @@ 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 && !still {
shown = ring.Animate("progress.value", shown, theme.StateDuration)
if !opts.Indeterminate {
shown = progressAnimate(ring, shown, still)
}
stroke := max(3, d/10)
size := FontSize(c, theme.CaptionSize)
tw, th := c.MeasureText(0, ui.Span{Text: caption, Size: size})
inside := caption != "" && !opts.Indeterminate && tw+4 <= d-2*stroke && th <= d-2*stroke
ring.Draw(func(p *ui.Painter, r ui.Rect) {
cx, cy, rad := internal.Snap(p, r.X+r.W/2), internal.Snap(p, r.Y+r.H/2), internal.Snap(p, (r.W-stroke)/2)
var track ui.Path
@@ -132,19 +135,19 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c
p.StrokePath(progressArc(p, cx, cy, rad, start, start+0.25), stroke, k.AccentText)
}
})
if caption != "" && d >= 44 && !opts.Indeterminate {
if inside {
ring.Children(func() {
ui.Text(c, caption).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").TextColor(k.Text)
ui.Text(c, caption).FontSize(size).FontFeatures("tnum").TextColor(k.Text)
})
ring.Center()
}
if opts.Label != "" || (caption != "" && (d < 44 || opts.Indeterminate)) {
if opts.Label != "" || (caption != "" && !inside) {
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(FontSize(c, theme.CaptionSize))
if caption != "" && !inside {
ui.Text(c, caption).TextColor(k.TextMuted).FontSize(size).FontFeatures("tnum")
}
})
}
@@ -152,6 +155,15 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c
return row
}
// progressAnimate eases toward target; under reduced motion it lands there and keeps the state in step.
func progressAnimate(e *ui.Element, target float32, still bool) float32 {
if still {
// A changed target lands at once under reduced motion; the detour stops a running transition.
e.Animate("progress.value", target+1, theme.StateDuration)
}
return e.Animate("progress.value", target, theme.StateDuration)
}
// progressArc is the arc from turn a to turn b, clockwise from twelve o'clock.
func progressArc(pt *ui.Painter, cx, cy, r float32, a, b float32) *ui.Path {
var p ui.Path
@@ -230,6 +242,9 @@ func Meter(c *ui.Context, value float64, opts MeterOptions) *ui.Element {
}
frac := func(v float64) float32 { return float32((v - lo) / (hi - lo)) }
col := ui.Column(c).Gap(u * 1.5)
if !(opts.Width >= 0) || math.IsInf(float64(opts.Width), 1) {
panic(fmt.Sprintf("mujica: Meter Width %v invalid", opts.Width))
}
if opts.Width > 0 {
col.Width(opts.Width)
} else {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB