Merge branch 'worktree-agent-acb1218b465c60e42'

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 13:04:28 -04:00
commit 2b1d6415e9
16 files changed
+196 -31

No files matched your search

+1
View File
@@ -135,6 +135,7 @@ func breadcrumbMore(c *ui.Context, hidden []BreadcrumbItem, choose func(int)) {
}
b.Children(func() { ui.Icon(c, icons.Must("ellipsis")).Size(16, 16) })
ui.PopoverBase(c, b, open, func(panel *ui.Element) {
internal.PopIn(c, panel, Motion(c, theme.PopupDuration))
panel.Margin(u, 0, 0, 0).Padding(u).Gap(u*0.5).Radius(theme.PopupRadius).Background(k.Surface).
Border(theme.BorderWidth, k.Border).Shadow(0, 4, 12, 0, ui.RGBA(0, 0, 0, 0.16)).MinWidth(160).MaxWidth(320)
for j, it := range hidden {
+2 -1
View File
@@ -81,6 +81,7 @@ func CommandPalette(c *ui.Context, open *bool, commands []Command, opts CommandP
res.Element = ui.DialogBase(c, open, func(back, panel *ui.Element) {
ww, wh := c.Size()
back.Background(ui.RGBA(0, 0, 0, 0.4)).Justify(ui.Start)
internal.PopIn(c, back, Motion(c, theme.PopupDuration))
panel.Margin(wh*0.12, 0, 0, 0).Width(560).MaxWidth(ww-32).MaxHeight(wh*0.76).
Radius(theme.PopupRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).
Shadow(0, 12, 32, 0, ui.RGBA(0, 0, 0, 0.3)).AlignItems(ui.Stretch).Label(msg(c, "command_palette.label"))
@@ -92,7 +93,7 @@ func CommandPalette(c *ui.Context, open *bool, commands []Command, opts CommandP
})
ui.Box(c).Height(1).Background(k.Ornament).Role(ui.RoleNone)
if in.Changed() {
st.hl = 0
st.hl, st.reveal = 0, true
}
n := len(found)
switch {
+23
View File
@@ -1,6 +1,7 @@
package mujica
import (
"fmt"
"testing"
"github.com/egoist/mygo/ui"
@@ -100,6 +101,28 @@ func TestCommandPaletteAnnouncesHighlight(t *testing.T) {
}
}
func TestCommandPaletteFilterRevealsHighlight(t *testing.T) {
var cmds []Command
for i := range 30 {
cmds = append(cmds, Command{ID: fmt.Sprint(i), Label: fmt.Sprintf("Command %02d", i), Keywords: []string{"royal"}})
}
open := true
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
CommandPalette(c, &open, cmds, CommandPaletteOptions{})
}, 800, 600)
tt.Key(0, ui.KeyUp) // wraps to the last, scrolling to the bottom
tt.Frame()
if r, _ := tt.Find("Command 00"); r.H > 0 {
t.Fatalf("list did not scroll to the end: first row at %+v", r)
}
tt.Type("royal")
tt.Frame()
if r, _ := tt.Find("Command 00"); r.H == 0 {
t.Fatal("highlighted first match is off screen after filtering")
}
}
func TestCommandPaletteEmptyClickEscape(t *testing.T) {
a := &paletteApp{open: true}
tt := ui.NewTester(a.view, 800, 600)
+1
View File
@@ -55,3 +55,4 @@ if i, ok := mujica.Breadcrumb(c, path, mujica.BreadcrumbOptions{}).Chosen(); ok
- 完整名称提示对每一项都提供,MyGo 不报告文字是否被截断。
- 未复用 MyGo `ui.Breadcrumbs`:它不支持折叠与提示,本组件用 `ButtonBase`、`PopoverBase` 组合实现。
- 弹层打开时在 160 ms 内淡入,减少动画时直接显示。关闭时立即消失:弹层关闭后 MyGo 不再构建其元素,公开接口无法为已移除的浮层做淡出。
+1
View File
@@ -51,3 +51,4 @@ if mujica.Pagination(c, &page, &size, mujica.PaginationOptions{Total: 420, PageS
## 限制
- 页容量选择基于 MyGo `SelectBase`,弹层由 MujicaUI 绘制。
- 弹层打开时在 160 ms 内淡入,减少动画时直接显示。关闭时立即消失:弹层关闭后 MyGo 不再构建其元素,公开接口无法为已移除的浮层做淡出。
+2
View File
@@ -59,3 +59,5 @@ router.View(c, func(r *ui.Route) { ui.Text(c, r.Path()) })
- MyGo 历史按钮的提示文字固定为英文 “Back” / “Forward”(`ui/router.go:713`);本组件覆盖了无障碍名称,但提示仍为英文。
- 历史按钮的外观来自 MyGo 主题按钮样式(经 `mujica.Use` 映射为 MujicaUI 颜色)。
- 弹层打开时在 160 ms 内淡入,减少动画时直接显示。关闭时立即消失:弹层关闭后 MyGo 不再构建其元素,公开接口无法为已移除的浮层做淡出。
- 下拉面板最高为锚点上方或下方可用空间,超出时在面板内滚动;方向键移动焦点时自动滚入视野。
+2
View File
@@ -72,3 +72,5 @@ if id, ok := res.Chosen(); ok && id == "wrap" {
- MyGo 没有 menu / menuitem 角色(`ui/access.go:25-79`),项以按钮呈现;勾选状态无法通过公开接口写入(`ui/base.go:56` 的 checked 为内部字段),因此写入名称。
- 指针点击非可聚焦元素会清除焦点(`ui/input.go:238`);选择后组件把焦点还给对应标题。
- 菜单打开期间,焦点保持在标题上,高亮项由组件管理。驱动菜单的按键注册在标题上,而 MyGo 的 active descendant 不公开,因此高亮变化时用 `c.Announce` 朗读当前项,并把它写入标题的 `Description`。
- 弹层打开时在 160 ms 内淡入,减少动画时直接显示。关闭时立即消失:弹层关闭后 MyGo 不再构建其元素,公开接口无法为已移除的浮层做淡出。
- 点击菜单内任意位置(包括禁用项)或用指针打开子菜单后,焦点回到所属标题,方向键、Enter、Escape 继续有效。
+2
View File
@@ -64,3 +64,5 @@ if id, ok := res.Chosen(); ok {
- MyGo `TextInputBase` 没有占位文字(`ui/editor.go:734`);为避免与输入法组合文字重叠,说明放在底部提示栏,搜索框以无障碍名称“搜索命令”标识。
- 搜索是子串匹配,不做模糊匹配或排序打分。
- 焦点留在搜索框以便继续输入;方向键移动高亮时用 `c.Announce` 朗读当前命令,并写入搜索框的 `Description`(MyGo 的 active descendant 不公开)。
- 弹层打开时在 160 ms 内淡入,减少动画时直接显示。关闭时立即消失:弹层关闭后 MyGo 不再构建其元素,公开接口无法为已移除的浮层做淡出。
- 搜索内容变化后高亮回到第一项,并滚入视野。
+12
View File
@@ -17,3 +17,15 @@ func Ease(e *ui.Element, d time.Duration, fg ui.Color) *ui.Element {
}
return e.TextColor(ui.Color{R: ch("mujica.fg.r", fg.R), G: ch("mujica.fg.g", fg.G), B: ch("mujica.fg.b", fg.B), A: ch("mujica.fg.a", fg.A)})
}
// PopIn fades popup e in over d from the frame it appeared and returns the progress, 0 to 1.
func PopIn(c *ui.Context, e *ui.Element, d time.Duration) float32 {
born := ui.Local(e, "mujica.popin", func() time.Time { return c.Now() })
p := float32(1)
if t := float32(c.Now().Sub(*born)) / float32(max(d, 1)); d > 0 && t < 1 {
p = ui.EaseOut(t)
c.AnimationFrame()
}
e.Opacity(p)
return p
}
+1
View File
@@ -285,6 +285,7 @@ func (m *menubarRun) activate(l, h int) {
}
if it.Items != nil {
m.st.hl = append(m.st.hl[:l+1], menubarStep(it.Items, -1, 1))
m.focus(m.st.open)
return
}
m.chosen = it.ID
+8
View File
@@ -64,6 +64,7 @@ func (m *menubarRun) panel(l int, x, y float32, parent ui.Rect) (*ui.Element, *u
panel := ui.Scroll(c).Absolute().Padding(u).MinWidth(200).MaxHeight(wh-8).AlignItems(ui.Stretch).Role(ui.RolePopup).
Radius(theme.PopupRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).
Shadow(0, 6, 16, 0, ui.RGBA(0, 0, 0, 0.18))
internal.PopIn(c, panel, Motion(c, theme.PopupDuration))
pb := panel.Bounds()
if x+pb.W > ww-4 {
x = ww - 4 - pb.W
@@ -75,6 +76,9 @@ func (m *menubarRun) panel(l int, x, y float32, parent ui.Rect) (*ui.Element, *u
y = max(4, wh-4-pb.H)
}
panel.Left(max(4, x)).Top(y)
if panel.Clicked() {
m.focus(st.open)
}
var anchor *ui.Element
panel.Children(func() {
for j, it := range items {
@@ -95,6 +99,10 @@ func (m *menubarRun) panel(l int, x, y float32, parent ui.Rect) (*ui.Element, *u
m.activate(l, j)
return
}
if row.Pressed() {
// A press takes the focus off the title, which drives the keys.
m.focus(st.open)
}
px, py, over := row.PointerPosition()
b := row.Bounds()
at := [2]float32{b.X + px, b.Y + py}
+28
View File
@@ -150,6 +150,34 @@ func TestMenubarKeyboard(t *testing.T) {
}
}
func TestMenubarKeysAfterClickInMenu(t *testing.T) {
a := &menubarApp{}
tt := ui.NewTester(a.view, 700, 500)
_ = tt.Click("File")
if err := tt.Click("Export"); err != nil {
t.Fatal(err)
}
if !tt.Focused("File") {
t.Fatal("click into the menu left the title without focus")
}
tt.Key(0, ui.KeyDown) // PDF to Vector, skipping disabled Image
tt.Key(0, ui.KeyEnter)
if len(a.chosen) != 1 || a.chosen[0] != "svg" {
t.Fatalf("arrows after a click chose %q", a.chosen)
}
_ = tt.Click("File")
_ = tt.Click("Export")
_ = tt.Click("Image") // disabled: the press still keeps keys working
tt.Key(0, ui.KeyEscape)
if tt.HasText("Vector") || !tt.HasText("Quit") {
t.Fatalf("Escape did not close only the submenu: %q", tt.Texts())
}
tt.Key(0, ui.KeyEscape)
if tt.HasText("Quit") {
t.Fatal("second Escape did not close the menu")
}
}
func TestMenubarLongMenuScrolls(t *testing.T) {
var items []MenubarItem
for i := range 30 {
+38 -26
View File
@@ -147,36 +147,48 @@ func navigationMenuTop(c *ui.Context, l NavigationLink, path string, push func(s
*open = true
}
ui.PopoverBase(c, b, open, func(panel *ui.Element) {
panel.Margin(u, 0, 0, 0).Padding(u*2).Gap(u).Radius(theme.PopupRadius).Background(k.Surface).
internal.PopIn(c, panel, Motion(c, theme.PopupDuration))
// Capped to the room beside the anchor; the links scroll inside.
_, wh := c.Size()
r := b.Bounds()
panel.Margin(u, 0, 0, 0).Padding(u*2).Radius(theme.PopupRadius).Background(k.Surface).MaxHeight(max(r.Y, wh-r.Y-r.H)-16).
Border(theme.BorderWidth, k.Border).Shadow(0, 4, 12, 0, ui.RGBA(0, 0, 0, 0.16)).MinWidth(240).MaxWidth(360)
panel.FocusGroup(ui.Vertical)
first := true
var list func(items []NavigationLink)
list = func(items []NavigationLink) {
for _, it := range items {
if it.Label == "" {
panic("mujica: NavigationLink needs a Label")
}
if it.Items != nil {
ui.Text(c, it.Label).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).
Padding(u*2, u*2, u, u*2).SingleLine()
list(it.Items)
continue
}
if it.Path == "" {
panic("mujica: NavigationLink needs a Path or Items")
}
if navigationMenuItem(c, it, it.Path == path, first).Clicked() {
*open = false
push(it.Path)
}
first = false
}
}
list(l.Items)
ui.Scroll(c).Shrink(1).Gap(u).FocusGroup(ui.Vertical).Children(func() {
navigationMenuPanel(c, l.Items, path, open, push)
})
})
}
// navigationMenuPanel lists a group's links, its subgroups under serif headings.
func navigationMenuPanel(c *ui.Context, items []NavigationLink, path string, open *bool, push func(string)) {
k := Tokens(c)
u := Density(c).Unit()
first := true
var list func(items []NavigationLink)
list = func(items []NavigationLink) {
for _, it := range items {
if it.Label == "" {
panic("mujica: NavigationLink needs a Label")
}
if it.Items != nil {
ui.Text(c, it.Label).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).
Padding(u*2, u*2, u, u*2).SingleLine()
list(it.Items)
continue
}
if it.Path == "" {
panic("mujica: NavigationLink needs a Path or Items")
}
if navigationMenuItem(c, it, it.Path == path, first).Clicked() {
*open = false
push(it.Path)
}
first = false
}
}
list(items)
}
func navigationMenuItem(c *ui.Context, l NavigationLink, on, first bool) *ui.Element {
k := Tokens(c)
u := Density(c).Unit()
+72
View File
@@ -1,13 +1,85 @@
package mujica
import (
"fmt"
"testing"
"time"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
)
// TestNavigationPopupsFadeIn checks each popup fades in over PopupDuration instead of appearing at once.
func TestNavigationPopupsFadeIn(t *testing.T) {
for _, tc := range []struct {
name, open, sample string
view func(c *ui.Context)
}{
{"breadcrumb", "Show hidden path", "Alpha", func(c *ui.Context) {
Breadcrumb(c, breadcrumbPath("Root", "Alpha", "Beta", "Gamma", "Omega", "Leaf"), BreadcrumbOptions{})
}},
{"pagination", "10 / page", "50 / page", func(c *ui.Context) {
page, size := 1, 10
Pagination(c, &page, &size, PaginationOptions{Total: 100, PageSizes: []int{10, 50}})
}},
{"navigation menu", "Library", "Letters", func(c *ui.Context) {
NavigationMenu(c, ui.NewRouter("/"), navigationMenuSample(), NavigationMenuOptions{})
}},
{"menubar", "File", "Quit", func(c *ui.Context) {
Menubar(c, (&menubarApp{}).menus(), MenubarOptions{})
}},
{"command palette", "", "Esc close", func(c *ui.Context) {
open := true
CommandPalette(c, &open, commandPaletteSample(), CommandPaletteOptions{})
}},
} {
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{Mode: Light})
ui.Column(c).Fill().Padding(16).AlignItems(ui.Start).Children(func() { tc.view(c) })
}, 700, 500)
if tc.open != "" {
if err := tt.Click(tc.open); err != nil {
t.Fatal(err)
}
}
r, ok := tt.Find(tc.sample)
if !ok {
t.Fatalf("%s: no %q", tc.name, tc.sample)
}
x, y := int(r.X-3), int(r.Y+r.H/2)
early := tt.Image().RGBAAt(x, y)
time.Sleep(200 * time.Millisecond)
tt.Frame()
if late := tt.Image().RGBAAt(x, y); early == late {
t.Errorf("%s: popup appeared at once (%v)", tc.name, late)
}
}
}
// TestNavigationMenuLongPanelScrolls checks a tall group stays in the window and follows the focus.
func TestNavigationMenuLongPanelScrolls(t *testing.T) {
var links []NavigationLink
for i := range 30 {
links = append(links, NavigationLink{Label: fmt.Sprintf("Link %02d", i), Path: fmt.Sprintf("/l/%d", i)})
}
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
NavigationMenu(c, ui.NewRouter("/"), []NavigationLink{{Label: "Group", Items: links}}, NavigationMenuOptions{})
}, 400, 300)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEnd)
tt.Frame()
if !tt.Focused("Link 29") {
t.Fatal("End did not focus the last link")
}
r, _ := tt.Find("Link 29")
if r.H == 0 || r.Y+r.H > 300 {
t.Fatalf("focused link off screen at %+v", r)
}
}
// TestNavigationCaptionsFollowTextScale checks small text grows with the desktop text scale.
func TestNavigationCaptionsFollowTextScale(t *testing.T) {
for _, tc := range []struct {
+2 -2
View File
@@ -36,8 +36,7 @@ type PaginationResult struct {
// Changed reports a new page or page size in this frame, from the user or a correction.
func (r PaginationResult) Changed() bool { return r.changed }
// Pagination moves *page (from 1) through Total records, *pageSize per page.
// A page past the end after Total shrinks is corrected to the last page.
// Pagination pages *page (from 1) through Total records, clamping it when Total shrinks.
func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) PaginationResult {
if opts.Total < 0 {
panic("mujica: Pagination Total is negative")
@@ -185,6 +184,7 @@ func paginationSizes(c *ui.Context, page, pageSize *int, sizes []int, res *Pagin
ui.Icon(c, icons.Must("chevron-down")).Size(14, 14).TextColor(k.TextMuted)
})
sel.Popup(func(panel *ui.Element) {
internal.PopIn(c, panel, Motion(c, theme.PopupDuration))
panel.Margin(u, 0, 0, 0).Padding(u).Radius(theme.PopupRadius).Background(k.Surface).
Border(theme.BorderWidth, k.Border).Shadow(0, 4, 12, 0, ui.RGBA(0, 0, 0, 0.16))
for _, s := range sizes {
+1 -2
View File
@@ -13,8 +13,7 @@ type ToolbarOptions struct {
Label string
}
// Toolbar is a row of actions built by fn, one Tab stop with arrow keys inside.
// Actions that do not fit move, from the last, into an overflow menu.
// Toolbar is one Tab stop of actions; those that do not fit go to an overflow menu.
func Toolbar(c *ui.Context, opts ToolbarOptions, fn func()) *ui.Element {
k := Tokens(c)
tb := ui.Toolbar(c, fn).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius).