From 82471ab0bf0219286dab114bb2b0ea02ffc5de24 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 13:11:39 -0400 Subject: [PATCH] Choices: review r1 fixes --- cascader.go | 14 +- choice_blocked_test.go | 105 +++++++++ choice_review_test.go | 211 ++++++++++++++++++ combobox.go | 27 ++- docs/components/033-toggle-group.md | 2 +- docs/components/034-slider.md | 7 +- docs/components/035-range-slider.md | 9 +- docs/components/036-select.md | 5 +- docs/components/037-multi-select.md | 2 +- docs/components/038-combobox.md | 1 + docs/components/039-cascader.md | 1 + docs/components/040-tree-select.md | 3 +- examples/gallery/pages_choice.go | 13 +- examples/gallery/pages_choice_test.go | 57 +++++ internal/choice.go | 9 +- messages_choice.go | 5 + multi_select.go | 25 ++- range_slider.go | 48 ++-- range_slider_test.go | 16 +- select.go | 65 ++++-- slider.go | 146 ++++++++---- slider_keys_test.go | 83 +++++++ slider_test.go | 12 - switch.go | 21 +- testdata/snapshots/darwin/cascader-dark.png | Bin 5222 -> 5252 bytes testdata/snapshots/darwin/cascader-light.png | Bin 5011 -> 5046 bytes testdata/snapshots/darwin/combobox-dark.png | Bin 3925 -> 3873 bytes testdata/snapshots/darwin/combobox-light.png | Bin 3777 -> 3796 bytes .../snapshots/darwin/multi_select-dark.png | Bin 4274 -> 4287 bytes .../snapshots/darwin/multi_select-light.png | Bin 4090 -> 4141 bytes testdata/snapshots/darwin/select-dark.png | Bin 4900 -> 4940 bytes testdata/snapshots/darwin/select-light.png | Bin 4674 -> 4733 bytes testdata/snapshots/darwin/switch-dark.png | Bin 7291 -> 7388 bytes testdata/snapshots/darwin/switch-light.png | Bin 7120 -> 7220 bytes .../snapshots/darwin/tree_select-dark.png | Bin 4482 -> 4506 bytes .../snapshots/darwin/tree_select-light.png | Bin 4374 -> 4419 bytes toggle.go | 5 +- toggle_group.go | 10 +- toggle_test.go | 14 -- tree_select.go | 67 ++++-- 40 files changed, 774 insertions(+), 209 deletions(-) create mode 100644 choice_blocked_test.go create mode 100644 choice_review_test.go create mode 100644 examples/gallery/pages_choice_test.go create mode 100644 slider_keys_test.go diff --git a/cascader.go b/cascader.go index 9be885f..de1e38c 100644 --- a/cascader.go +++ b/cascader.go @@ -43,8 +43,7 @@ type CascaderLevel struct { type CascaderOptions struct { // Level returns the children under path; the empty path is the roots. Level func(path []string) CascaderLevel - // Load fetches the children under path, once while Idle and on retry; it - // should mark the level Loading and finish through Window.Update. + // Load fetches the children under path while Idle and on retry; it marks the level Loading. Load func(path []string) // AnyLevel lets branches be chosen too; by default only leaves are. AnyLevel bool @@ -66,9 +65,7 @@ type cascaderState struct { type cascaderKey struct{} -// Cascader chooses a path through levels the caller supplies, loading -// children on demand. Up and Down move in a column, Right opens a branch, -// Left goes back, Enter or Space chooses; Escape closes. +// Cascader chooses a path through levels the caller supplies, loading children on demand. func Cascader(c *ui.Context, path *[]string, opts CascaderOptions) ChoiceResult { if opts.Level == nil { panic("mujica: Cascader needs Level") @@ -79,6 +76,9 @@ func Cascader(c *ui.Context, path *[]string, opts CascaderOptions) ChoiceResult tr.Label(opts.Label) } st := *ui.Local(tr, cascaderKey{}, func() *cascaderState { return &cascaderState{requested: map[string]bool{}} }) + if opts.Disabled { + st.open = false + } if tr.Clicked() || (!st.open && tr.Shortcut(0, ui.KeyDown)) { st.open = !st.open if st.open { @@ -186,7 +186,7 @@ func cascaderRow(c *ui.Context, st *cascaderState, path *[]string, opts Cascader d := Density(c) full := append(slices.Clone(prefix), n.Value) depth := len(prefix) - b := ui.ButtonBase(c).Key(n.Value).Disabled(n.Disabled).MinHeight(d.ControlHeight()-4).Padding(0, d.Unit()*2). + b := ui.ButtonBase(c).Key(n.Value).Disabled(n.Disabled).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2). Gap(d.Unit() * 2).Justify(ui.Start).Radius(theme.ControlRadius).FillWidth().Role(ui.RoleListItem) internal.Ring(b, k.Focus, theme.ControlRadius) if st.focusSet && (slices.Equal(st.focus, full) || first && slices.Equal(st.focus, append(slices.Clone(prefix), ""))) { @@ -214,6 +214,7 @@ func cascaderRow(c *ui.Context, st *cascaderState, path *[]string, opts Cascader st.focus, st.focusSet = slices.Clone(prefix), true } chosen := len(*path) > depth && slices.Equal((*path)[:depth+1], full) + b.Selected(chosen).Background(ui.Transparent).TextColor(k.Text) switch { case on: b.Background(k.Selection) @@ -224,6 +225,7 @@ func cascaderRow(c *ui.Context, st *cascaderState, path *[]string, opts Cascader b.TextColor(k.AccentText) } b.Children(func() { + selectCheck(c, chosen) ui.Text(c, n.Label).SingleLine().Grow(1).Shrink(1) if !n.Leaf { ui.Box(c).Size(14, 14).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { diff --git a/choice_blocked_test.go b/choice_blocked_test.go new file mode 100644 index 0000000..c2a4ca8 --- /dev/null +++ b/choice_blocked_test.go @@ -0,0 +1,105 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +// choiceBetween builds control between two buttons, for Tab order checks. +func choiceBetween(control func(c *ui.Context)) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Padding(10).Gap(10).Width(300).Children(func() { + Button(c, "Before", ButtonOptions{}) + control(c) + Button(c, "After", ButtonOptions{}) + }) + }, 400, 400) +} + +// choiceTabSkips reports whether Tab from Before lands on After. +func choiceTabSkips(t *testing.T, tt *ui.Tester) bool { + t.Helper() + if err := tt.Click("Before"); err != nil { + t.Fatal(err) + } + tt.Key(0, ui.KeyTab) + return tt.Focused("After") +} + +func TestToggleDisabled(t *testing.T) { + for _, disabled := range []bool{false, true} { + on := false + tt := choiceBetween(func(c *ui.Context) { + Toggle(c, &on, "Italic", ToggleOptions{Disabled: disabled}) + }) + _ = tt.Click("Italic") + if on == disabled { + t.Fatalf("disabled=%v: click left on=%v", disabled, on) + } + if choiceTabSkips(t, tt) != disabled { + t.Fatalf("disabled=%v: Tab skip wrong", disabled) + } + } +} + +func TestSliderDisabled(t *testing.T) { + for _, disabled := range []bool{false, true} { + v := 30.0 + tt := choiceBetween(func(c *ui.Context) { + Slider(c, &v, SliderOptions{Step: 1, Disabled: disabled, Label: "Volume"}) + }) + _ = tt.Click("Volume") + if (v != 30) == disabled { + t.Fatalf("disabled=%v: click left %v", disabled, v) + } + if choiceTabSkips(t, tt) != disabled { + t.Fatalf("disabled=%v: Tab skip wrong", disabled) + } + } +} + +func TestRangeSliderDisabled(t *testing.T) { + for _, disabled := range []bool{false, true} { + low, high := 20.0, 40.0 + tt := choiceBetween(func(c *ui.Context) { + RangeSlider(c, &low, &high, RangeSliderOptions{Step: 1, Disabled: disabled, Label: "Price"}) + }) + _ = tt.Click("Price") + if (high != 40) == disabled || low != 20 { + t.Fatalf("disabled=%v: click left %v..%v", disabled, low, high) + } + if choiceTabSkips(t, tt) != disabled { + t.Fatalf("disabled=%v: Tab skip wrong", disabled) + } + } +} + +// Disabling a focused control in the frame a key arrives must block the key. +func TestChoiceDisabledWhileFocused(t *testing.T) { + disabled := false + on, pressed, group := false, false, []string{"left"} + items := []ToggleItem[string]{{Value: "left", Label: "Left"}, {Value: "right", Label: "Right"}} + for name, build := range map[string]func(c *ui.Context){ + "switch": func(c *ui.Context) { Switch(c, &on, "Sync", SwitchOptions{Disabled: disabled}) }, + "toggle": func(c *ui.Context) { Toggle(c, &pressed, "Bold", ToggleOptions{Disabled: disabled}) }, + "group": func(c *ui.Context) { + items[1].Disabled = disabled + ToggleGroup(c, &group, items, ToggleGroupOptions{}) + }, + } { + disabled, on, pressed, group = false, false, false, []string{"left"} + tt := choiceBetween(build) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyTab) + if name == "group" { + tt.Key(0, ui.KeyRight) + } + disabled = true + tt.Key(0, ui.KeySpace) + if on || pressed || group[0] != "left" { + t.Errorf("%s: Space changed a disabled control: %v %v %v", name, on, pressed, group) + } + } +} diff --git a/choice_review_test.go b/choice_review_test.go new file mode 100644 index 0000000..ddf5f24 --- /dev/null +++ b/choice_review_test.go @@ -0,0 +1,211 @@ +package mujica + +import ( + "image/color" + "slices" + "testing" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// A popup left open must stop taking choices once its control is disabled. +func TestChoicePopupsCloseWhenDisabled(t *testing.T) { + disabled := false + var multi []int + var paths, ids []string + f := newCascaderFake() + cases := []struct { + name, open, pick string + build func(c *ui.Context) + same func() bool + }{ + {"multi", "Select…", "Lily", func(c *ui.Context) { + MultiSelect(c, &multi, multiSelectTestOptions, MultiSelectOptions{Disabled: disabled}) + }, func() bool { return len(multi) == 0 }}, + {"cascader", "Select…", "Europe", func(c *ui.Context) { + Cascader(c, &paths, CascaderOptions{Level: f.level, Load: f.load, AnyLevel: true, Disabled: disabled}) + }, func() bool { return len(paths) == 0 }}, + {"tree", "Select…", "Guard", func(c *ui.Context) { + TreeSelect(c, &ids, treeSelectTestNodes, TreeSelectOptions{Disabled: disabled}) + }, func() bool { return len(ids) == 0 }}, + } + for _, tc := range cases { + disabled = false + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Padding(10).Children(func() { tc.build(c) }) + }, 700, 500) + if err := tt.Click(tc.open); err != nil { + t.Fatal(err) + } + if !tt.HasText(tc.pick) { + t.Fatalf("%s: popup did not open", tc.name) + } + disabled = true + tt.Frame() + _ = tt.Click(tc.pick) + if !tc.same() || tt.HasText(tc.pick) { + t.Errorf("%s: disabled control took a choice or kept its popup", tc.name) + } + } +} + +func TestTreeSelectSingleMarksOwnNode(t *testing.T) { + single := TreeSelectOptions{} + court := &treeSelectTestNodes[0] + if got := treeSelectCheck(court, []string{"court"}, single); got != internal.Checked { + t.Fatalf("chosen parent shows %v", got) + } + if got := treeSelectCheck(court, []string{"king", "queen"}, single); got != internal.Unchecked { + t.Fatalf("parent of chosen leaves shows %v", got) + } + var sel []string + changes := 0 + tt := treeSelectTester(&sel, single, &changes) + _ = tt.Click("Select…") + _ = tt.Click("Court") + if !slices.Equal(sel, []string{"court"}) { + t.Fatalf("sel %v", sel) + } +} + +func TestTreeSelectRightSkipsHiddenChildren(t *testing.T) { + var sel []string + changes := 0 + tt := treeSelectTester(&sel, TreeSelectOptions{Multiple: true}, &changes) + _ = tt.Click("Select…") + tt.Type("queen") + tt.Key(0, ui.KeyDown) + if !tt.Focused("Court") { + t.Fatal("Down did not reach Court") + } + tt.Key(0, ui.KeyRight) + if !tt.Focused("Queen") { + t.Fatal("Right did not reach the only shown child") + } +} + +// pixel reads the light theme color at (x, y) of the last frame. +func pixel(tt *ui.Tester, x, y float32) color.RGBA { + return tt.Image().RGBAAt(int(x), int(y)) +} + +func TestSelectChosenRowHasSelectionFill(t *testing.T) { + v, changes := "banana", 0 + tt := selectTester(&v, selectTestOptions, SelectOptions{}, &changes) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyDown) + tt.Move(0, 0) + tt.Key(0, ui.KeyDown) + blue, ok := tt.Find("Blueberry") + if !ok { + t.Fatal("popup closed") + } + h := theme.Default.ControlHeight() + got := pixel(tt, blue.X+blue.W+20, blue.Y+blue.H/2-h) + s := theme.Light().Selection + if got.R != s.R || got.G != s.G || got.B != s.B { + t.Fatalf("chosen row %v, want selection %v", got, s) + } +} + +func TestChoiceHitAreas(t *testing.T) { + v, changes := "apple", 0 + tt := selectTester(&v, selectTestOptions, SelectOptions{Clearable: true}, &changes) + if r, _ := tt.Find("Clear"); r.W < theme.MinHitSize || r.H < theme.MinHitSize { + t.Errorf("clear button %v", r) + } + var sel []string + tt = treeSelectTester(&sel, TreeSelectOptions{}, &changes) + _ = tt.Click("Select…") + if r, _ := tt.Find("Expand Court"); r.W < theme.MinHitSize || r.H < theme.MinHitSize { + t.Errorf("tree arrow %v", r) + } + font := "" + tt = ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Combobox(c, &font, comboboxTestFonts, ComboboxOptions{}) + }, 400, 300) + if r, _ := tt.Find("Show options"); r.W < theme.MinHitSize || r.H < theme.MinHitSize { + t.Errorf("combobox arrow %v", r) + } +} + +func TestChoiceRowsFollowDensity(t *testing.T) { + for _, d := range []theme.Density{theme.Compact, theme.Default, theme.Comfortable} { + v, on := "apple", false + var sw *ui.Element + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Density: d}) + ui.Column(c).Padding(10).AlignItems(ui.Start).Children(func() { + sw = Switch(c, &on, "Wi-Fi", SwitchOptions{}) + Select(c, &v, selectTestOptions, SelectOptions{Label: "Fruit"}) + }) + }, 400, 500) + if h := sw.Bounds().H; h < d.ControlHeight() { + t.Errorf("%v: switch %v high", d, h) + } + _ = tt.Click("Fruit") + a, _ := tt.Find("Cherry") + b, _ := tt.Find("Date") + if b.Y-a.Y < d.ControlHeight() { + t.Errorf("%v: option rows %v apart", d, b.Y-a.Y) + } + } +} + +func TestMultiSelectCaptionFollowsTextScale(t *testing.T) { + height := func(scale float32) float32 { + values := []int{1} + tt := multiSelectTester(&values, MultiSelectOptions{Max: 2}, new(int)) + tt.SetPreferences(ui.Preferences{TextScale: scale}) + _ = tt.Click("Rose") + r, ok := tt.Find("1 of 2 chosen") + if !ok { + t.Fatal("limit note missing") + } + return r.H + } + if one, two := height(1), height(2); two < one*1.5 { + t.Fatalf("caption %v high at 2x, %v at 1x", two, one) + } +} + +func TestComboboxCaretAfterChoice(t *testing.T) { + r := &comboboxRun{} + tt := comboboxTester(r, fixedFonts, ComboboxOptions{}) + tt.Key(0, ui.KeyTab) + tt.Type("di") + tt.Key(0, ui.KeyEnter) + if r.value != "Didot" { + t.Fatalf("value %q", r.value) + } + tt.Type("x") + if r.query != "Didotx" { + t.Fatalf("typing after a choice gave %q", r.query) + } +} + +// The first Tab enters a toggle group at its first enabled item; later at the last focused one. +func TestToggleGroupTabEntry(t *testing.T) { + value := []string{"right"} + tt := choiceBetween(func(c *ui.Context) { + ToggleGroup(c, &value, toggleGroupTestItems, ToggleGroupOptions{}) + }) + _ = tt.Click("Before") + tt.Key(0, ui.KeyTab) + if !tt.Focused("Left") { + t.Fatal("first Tab did not land on the first item") + } + tt.Key(0, ui.KeyRight) + tt.Key(0, ui.KeyTab) + _ = tt.Click("Before") + tt.Key(0, ui.KeyTab) + if !tt.Focused("Center") { + t.Fatal("Tab did not return to the last focused item") + } +} diff --git a/combobox.go b/combobox.go index 8322289..d970a51 100644 --- a/combobox.go +++ b/combobox.go @@ -41,9 +41,7 @@ func (r ComboboxResult) QueryChanged() bool { return r.queryChanged } // comboboxKey keeps this file's state apart from MyGo's own. type comboboxKey struct{ name string } -// Combobox chooses *value by typing to filter options and picking one, as -// an autocomplete does. Up and Down move, Enter or a click chooses, Escape -// closes. With AllowCustom, Enter or leaving the field keeps typed text. +// Combobox chooses *value by typing to filter options and picking one, or typed text with AllowCustom. func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptions) ComboboxResult { k := Tokens(c) d := Density(c) @@ -52,9 +50,21 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio Gap(d.Unit() * 2).Radius(theme.ControlRadius).Background(k.Surface).Cursor(ui.CursorText).Disabled(opts.Disabled) query := ui.Local(f, comboboxKey{"query"}, func() string { return *value }) wasFocused := ui.Local(f, comboboxKey{"focused"}, func() bool { return false }) + // MyGo keeps the caret on replaced text (ui/editor.go:117): a choice builds a fresh input. + gen := ui.Local(f, comboboxKey{"input"}, func() int { return 0 }) + refocus := ui.Local(f, comboboxKey{"refocus"}, func() bool { return false }) var p *ui.ComboboxParts f.Children(func() { - p = ui.ComboboxBase(c, query) + ui.Row(c).Key(*gen).Grow(1).Children(func() { + p = ui.ComboboxBase(c, choiceTarget(query, opts.Disabled)) + }) + if *refocus { + p.Input.Focus() + *refocus = false + } + if opts.Disabled { + p.SetOpen(false) + } pad := max(0, (d.ControlHeight()-2-theme.BodySize*theme.LineHeight)/2) p.Input.Grow(1).MinWidth(80).Padding(pad, d.Unit()*3).Placeholder(opts.Placeholder) if opts.Label != "" { @@ -63,13 +73,16 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio if opts.Loading { Spinner(c, SpinnerOptions{Size: 14}) } - arrow := ui.Box(c).Size(16, 16).Shrink(0).Cursor(ui.CursorPointer) + arrow := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0).Cursor(ui.CursorPointer). + Role(ui.RoleButton).Label(msg(c, "combobox.toggle")) if arrow.Clicked() { p.SetOpen(!p.Open()) p.Input.Focus() } open := p.Open() - arrow.Draw(func(pt *ui.Painter, r ui.Rect) { internal.Mark(pt, r, selectChevron(open), k.TextMuted) }) + arrow.Draw(func(pt *ui.Painter, r ui.Rect) { + internal.Mark(pt, selectCenter(r, 16), selectChevron(open), k.TextMuted) + }) }) in := p.Input if f.Clicked() { @@ -116,11 +129,13 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio focused := in.Focused() if v, ok := p.Chosen(); ok { *value, *query = v, v + *gen, *refocus = *gen+1, focused } else if in.Submitted() && opts.AllowCustom { *value = strings.TrimSpace(*query) } else if in.Submitted() && p.Open() && len(shown) > 0 { // Results came after typing left nothing highlighted: Enter takes the first. *value, *query = shown[0], shown[0] + *gen, *refocus = *gen+1, true p.SetOpen(false) } else if *wasFocused && !focused { if opts.AllowCustom { diff --git a/docs/components/033-toggle-group.md b/docs/components/033-toggle-group.md index f4acbce..4361ce2 100644 --- a/docs/components/033-toggle-group.md +++ b/docs/components/033-toggle-group.md @@ -54,7 +54,7 @@ ui.Render(view, 400, 300, 1) ## 键盘操作 -- Tab:进入组,落在按下项(或上次聚焦项);再按 Tab 离开。 +- Tab:进入组。首次落在第一个可用项(与哪项按下无关);之后回到组内上次聚焦的项。再按 Tab 离开。 - ←/→(纵向为 ↑/↓)、Home、End:在项之间移动焦点,跳过禁用项,首尾循环。 - Space:按下或松开当前项。 diff --git a/docs/components/034-slider.md b/docs/components/034-slider.md index f505676..2c26cec 100644 --- a/docs/components/034-slider.md +++ b/docs/components/034-slider.md @@ -25,7 +25,7 @@ ui.Render(view, 400, 300, 1) | `Step` | `float64` | 0 | 步长;值为 `Min + n·Step`。0 为连续。 | | `Marks` | `[]SliderMark` | nil | 刻度:`Value` 处画刻线,`Label` 非空时在下方显示文字。 | | `ShowValue` | `bool` | false | 悬停、拖动或聚焦时在滑块上方显示数值提示(预留高度,不改变布局)。 | -| `Format` | `func(float64) string` | 按步长精度 | 数值文字。 | +| `Format` | `func(float64) string` | 按 `Min`、`Step` 所需小数位(连续时至少 1 位) | 数值文字。 | | `Disabled` | `bool` | false | 禁用。 | | `Label` | `string` | "" | 无障碍名称。 | @@ -44,5 +44,6 @@ ui.Render(view, 400, 300, 1) ## 限制 -- 基于 `ui.SliderBase`。MyGo 的方向键步长固定为范围的 1%(ui/base.go:116),本组件把每次按键结果换算为一个 `Step`;当 `Step` 小于范围的 1% 且滑块距端点不足 1% 时,方向键会直接到端点。 -- `SliderBase` 在构建时就处理指针(ui/base.go:111),禁用时组件改为让它操作副本,保证值不变。 +- 基于 `ui.SliderBase`,它向辅助技术报告 Slider 角色、范围与当前值(ui/base.go:128)。 +- MyGo 只把方向键、Home、End 交给 `SliderBase` 本身(ui/base.go:118,按键路由见 ui/input.go:495),并固定以范围的 1% 移动。本组件让它操作副本,按方向判断按键后自己按 `Step` 移动;距端点不足 1% 时 MyGo 的方向键与 Home/End 结果相同,组件改看松开的是哪个键(`HandleInput` 能收到 KeyUp),在键松开前不改值。 +- `SliderBase` 在构建时就处理指针(ui/base.go:111),禁用时外层先禁用,且只操作副本,保证值不变。 diff --git a/docs/components/035-range-slider.md b/docs/components/035-range-slider.md index 9d0184f..1e70940 100644 --- a/docs/components/035-range-slider.md +++ b/docs/components/035-range-slider.md @@ -25,9 +25,9 @@ ui.Render(view, 400, 300, 1) | `Step` | `float64` | 0 | 步长,0 为连续。 | | `Marks` | `[]SliderMark` | nil | 刻度与文字。 | | `ShowValue` | `bool` | false | 悬停、拖动或聚焦时显示两端数值;低端提示向左、高端向右,互不遮挡。 | -| `Format` | `func(float64) string` | 按步长精度 | 数值文字。 | +| `Format` | `func(float64) string` | 按 `Min`、`Step` 所需小数位(连续时至少 1 位) | 数值文字。 | | `Disabled` | `bool` | false | 禁用。 | -| `Label` | `string` | "" | 区间名称;两个滑块分别读作“`Label` 最小值, 20”“`Label` 最大值, 80”。 | +| `Label` | `string` | "" | 区间名称;两个滑块分别名为“`Label` 最小值”“`Label` 最大值”,数值与范围由 Slider 角色报告。 | ## 状态 @@ -46,5 +46,6 @@ ui.Render(view, 400, 300, 1) ## 限制 -- MyGo 自带的 `ui.RangeSlider` 使用玻璃质感滑块(ui/widgets.go:419),不符合本库风格;MyGo 也没有双滑块 Base,故本组件用公开的 `Dragged`、`PointerPosition`、`Shortcut` 组合实现。 -- 滑块的无障碍角色为 Slider,但数值范围属性只能由 `SliderBase` 设置(ui/base.go:128),因此当前值写在名称里(如“Price 最小值, 20”),屏幕阅读器读名称而非数值属性。 +- MyGo 自带的 `ui.RangeSlider` 使用玻璃质感滑块(ui/widgets.go:419),不符合本库风格;MyGo 也没有双滑块 Base,故每个滑块各是一个 `SliderBase`(报告 Slider 角色、范围与当前值,ui/base.go:128),拖动由外层的 `Dragged`、`PointerPosition` 处理。 +- 两个滑块的范围都报告为整条轨道 `Min`–`Max`;“不能越过另一端”这一动态上下限无法告诉辅助技术。 +- 按键处理与 Slider 相同:距端点不足 1% 时等到按键松开再移动(见 034 限制)。 diff --git a/docs/components/036-select.md b/docs/components/036-select.md index 0d16a96..5e0745e 100644 --- a/docs/components/036-select.md +++ b/docs/components/036-select.md @@ -36,7 +36,7 @@ ui.Render(view, 400, 300, 1) ## 状态 -默认、悬停(边框加深)、展开(酒红边框、箭头朝上)、聚焦、禁用;弹层中高亮项(浅底)、已选项(勾号 + 酒红文字)、禁用项、空状态。 +默认、悬停(边框加深)、展开(酒红边框、箭头朝上)、聚焦、禁用;弹层中高亮项(浅底)、已选项(玫瑰底 + 勾号 + 酒红文字)、键盘高亮项另有左侧竖条、禁用项、空状态。 ## 事件 @@ -52,4 +52,5 @@ ui.Render(view, 400, 300, 1) ## 限制 - 基于 `ui.SelectBase`。其高亮索引没有公开的设置接口(ui/base.go:260),所以弹层打开时输入字母不移动高亮,只在弹层关闭时定位。 -- 清空按钮不占 Tab 位(与 MyGo 搜索框一致),键盘用 Backspace/Delete 清空。 +- 清空按钮不占 Tab 位(与 MyGo 搜索框一致),键盘用 Backspace/Delete 清空;它的命中区域为 28×28 DIP。 +- 选项行的角色为 ListItem,选中状态由 `SelectBase.Item` 报告。`SelectParts` 没有关闭弹层的接口(ui/base.go:310),所以选择器在弹层打开时被禁用后不再构建弹层;重新启用时弹层会再次显示。 diff --git a/docs/components/037-multi-select.md b/docs/components/037-multi-select.md index c02624f..ba56e64 100644 --- a/docs/components/037-multi-select.md +++ b/docs/components/037-multi-select.md @@ -49,6 +49,6 @@ ui.Render(view, 400, 300, 1) ## 限制 -- 选项行基于 `ui.CheckboxBase` 自绘方框;全选的“半选”只在画面上表示,MyGo 没有为自定义元素设置 mixed 状态的公开接口(ui/element.go:282 的 checked 字段仅由内部设置),屏幕阅读器读作未选中。 +- 选项行基于 `ui.CheckboxBase` 自绘方框,选中状态由 MyGo 报告。全选的“半选”无法作为 mixed 状态报告:MyGo 没有为自定义元素设置它的公开接口(ui/element.go:282 的 checked 字段仅由内部设置)。组件改用 `Description` 读出“部分选中”,屏幕阅读器仍把复选框本身读作未选中。 - 标签上的移除按钮不占 Tab 位,键盘用 Backspace 移除。 - 列表未虚拟化,适合数百项以内。 diff --git a/docs/components/038-combobox.md b/docs/components/038-combobox.md index 50ffaea..ac2a450 100644 --- a/docs/components/038-combobox.md +++ b/docs/components/038-combobox.md @@ -53,3 +53,4 @@ ui.Render(view, 400, 300, 1) - 基于 `ui.ComboboxBase`。它在没有选项时隐藏弹层(ui/combobox.go:191),所以“加载中”“无匹配”用一个不接收输入的独立浮层显示。 - 异步结果在打字后才到达时 MyGo 不会自动高亮首项,组件让 Enter 选择首个结果。 +- MyGo 替换输入框文字时保留原光标位置(ui/editor.go:117),且不公开光标设置;组件在选择后重建输入框,使光标落在新文字末尾并保持焦点。 diff --git a/docs/components/039-cascader.md b/docs/components/039-cascader.md index 82499ad..58ad106 100644 --- a/docs/components/039-cascader.md +++ b/docs/components/039-cascader.md @@ -58,3 +58,4 @@ ui.Render(view, 400, 300, 1) - `Load` 在视图构建中同步调用,不得阻塞;异步任务完成后请用 `Window.Update` 回到界面线程更新数据。 - 列表未虚拟化,每列适合数百项以内。 +- 已选路径上的节点通过 `Selected` 报告选中;当前展开的分支无法报告 expanded 状态(ui/base.go:297 等处仅内部设置),仅在画面上用底色表示。 diff --git a/docs/components/040-tree-select.md b/docs/components/040-tree-select.md index 89611cb..d49adee 100644 --- a/docs/components/040-tree-select.md +++ b/docs/components/040-tree-select.md @@ -55,5 +55,6 @@ ui.Render(view, 400, 300, 1) ## 限制 - 树行基于 `ui.CheckboxBase` / `ui.ButtonBase` 自绘。MyGo 的 `TreeItem` 只能显示文字标签,无法放入复选框(ui/tree.go:44),故未复用。 -- 展开状态与半选状态只在画面上表示:MyGo 不对自定义元素公开 expanded 与 mixed 的设置(ui/base.go:297、ui/element.go:282 仅内部设置),屏幕阅读器读不到这两个状态;展开箭头带“展开/收起 + 节点名”的名称。 +- MyGo 不对自定义元素公开 expanded 与 mixed 状态的设置(ui/tree.go:98、ui/element.go:282 仅内部设置)。组件改用 `Description` 读出“已展开/已收起”“部分选中”,展开箭头带“展开/收起 + 节点名”的名称;屏幕阅读器读不到真正的 expanded/mixed 状态属性。单选行通过 `Selected` 报告选中,并显示勾号。 +- `CollapsibleBase` 的触发器能报告 expanded,但它自身要占一个焦点位,会让方向键在箭头与节点间交替停留,故未采用。 - 列表未虚拟化,适合数千节点以内。 diff --git a/examples/gallery/pages_choice.go b/examples/gallery/pages_choice.go index fc64345..fbdd6a9 100644 --- a/examples/gallery/pages_choice.go +++ b/examples/gallery/pages_choice.go @@ -105,10 +105,10 @@ view := func(c *ui.Context) { func choiceDemo(c *ui.Context, controls, samples func()) { k := mujica.Tokens(c) ui.Column(c).Gap(20).AlignItems(ui.Start).Children(func() { - ui.Text(c, "Parameters").Font(theme.SerifFont).FontSize(theme.H3Size).TextColor(k.Text) + ui.Text(c, "Parameters").Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text) ui.Row(c).Gap(20).Wrap().AlignItems(ui.Center).Children(controls) ui.Box(c).Height(1).FillWidth().Background(k.Ornament) - ui.Text(c, "States").Font(theme.SerifFont).FontSize(theme.H3Size).TextColor(k.Text) + ui.Text(c, "States").Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text) ui.Column(c).Gap(16).AlignItems(ui.Start).Children(samples) }) } @@ -123,7 +123,7 @@ func choiceFlag(c *ui.Context, on *bool, label string, log func(string)) { // choiceLabeled shows a caption above a sample. func choiceLabeled(c *ui.Context, caption string, fn func()) { ui.Column(c).Gap(6).Children(func() { - ui.Text(c, caption).FontSize(theme.CaptionSize).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, caption).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(mujica.Tokens(c).TextMuted) fn() }) } @@ -288,7 +288,7 @@ var choiceFruits = []mujica.SelectOption[string]{ func selectDemo(c *ui.Context, log func(string)) { type state struct { - value string + value, picked string clearable, disabled, noData bool } s := *ui.Local(c.Root(), "036", func() *state { return &state{value: "banana", clearable: true} }) @@ -306,9 +306,10 @@ func selectDemo(c *ui.Context, log func(string)) { log(fmt.Sprintf("Fruit: %q", s.value)) } }) - empty := "" choiceLabeled(c, "Placeholder", func() { - mujica.Select(c, &empty, choiceFruits, mujica.SelectOptions{Placeholder: "Pick a fruit"}) + if mujica.Select(c, &s.picked, choiceFruits, mujica.SelectOptions{Placeholder: "Pick a fruit", Clearable: true}).Changed() { + log(fmt.Sprintf("Picked: %q", s.picked)) + } }) }) } diff --git a/examples/gallery/pages_choice_test.go b/examples/gallery/pages_choice_test.go new file mode 100644 index 0000000..9f22100 --- /dev/null +++ b/examples/gallery/pages_choice_test.go @@ -0,0 +1,57 @@ +package main + +import ( + "slices" + "testing" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +// choiceDemoTester runs the demo of page id at text scale and returns its tester and event log. +func choiceDemoTester(t *testing.T, id string, scale float32) (*ui.Tester, *[]string) { + t.Helper() + i := slices.IndexFunc(choicePages, func(p page) bool { return p.ID == id }) + if i < 0 { + t.Fatalf("no page %s", id) + } + events := &[]string{} + tt := ui.NewTester(func(c *ui.Context) { + mujica.Use(c, mujica.Settings{Mode: mujica.Light}) + ui.Scroll(c).Fill().Padding(24).Children(func() { + choicePages[i].Demo(c, func(s string) { *events = append(*events, s) }) + }) + }, 900, 700) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: scale}) + return tt, events +} + +func TestChoiceSelectDemoKeepsPlaceholderChoice(t *testing.T) { + tt, events := choiceDemoTester(t, "036", 1) + if err := tt.Click("Pick a fruit"); err != nil { + t.Fatal(err) + } + if err := tt.Click("Fig"); err != nil { + t.Fatal(err) + } + tt.Frame() + if !tt.HasText("Fig") || !slices.Contains(*events, `Picked: "fig"`) { + t.Fatalf("choice lost: texts %q, events %q", tt.Texts(), *events) + } +} + +func TestChoiceDemoHeadingsFollowTextScale(t *testing.T) { + height := func(scale float32) float32 { + tt, _ := choiceDemoTester(t, "031", scale) + tt.Frame() + r, ok := tt.Find("Parameters") + if !ok { + t.Fatal("heading missing") + } + return r.H + } + if one, two := height(1), height(2); two < one*1.5 { + t.Fatalf("heading %v high at 2x, %v at 1x", two, one) + } +} diff --git a/internal/choice.go b/internal/choice.go index d758776..e6dcd59 100644 --- a/internal/choice.go +++ b/internal/choice.go @@ -36,12 +36,11 @@ func CheckBox(p *ui.Painter, r ui.Rect, s CheckState, k theme.Tokens, hover bool if s == Mixed { mark = "minus" } - Mark(p, r, mark, k.OnAccent) + in := r.W * 0.12 + Mark(p, ui.Rect{X: r.X + in, Y: r.Y + in, W: r.W - 2*in, H: r.H - 2*in}, mark, k.OnAccent) } -// Mark paints the Lucide icon name inset in r. Icons sit on the pixel grid, -// so their cached masks never differ between frames or tests. +// Mark paints the Lucide icon name in r, on the pixel grid so masks never vary. func Mark(p *ui.Painter, r ui.Rect, name string, c ui.Color) { - in := r.W * 0.12 - p.Icon(icons.Must(name), ui.Rect{X: r.X + in, Y: r.Y + in, W: r.W - 2*in, H: r.H - 2*in}, c) + p.Icon(icons.Must(name), r, c) } diff --git a/messages_choice.go b/messages_choice.go index 06e86e3..5075a5a 100644 --- a/messages_choice.go +++ b/messages_choice.go @@ -13,6 +13,7 @@ func init() { "multi_select.limit": {"%d of %d chosen", "已选 %d / %d"}, "multi_select.more": {"+%d", "+%d"}, "multi_select.remove": {"Remove %s", "移除 %s"}, + "combobox.toggle": {"Show options", "显示选项"}, "combobox.loading": {"Loading…", "加载中…"}, "combobox.no_results": {"No matches", "无匹配项"}, "combobox.use": {"Use “%s”", "使用“%s”"}, @@ -24,5 +25,9 @@ func init() { "tree_select.no_results": {"No results", "无匹配结果"}, "tree_select.expand": {"Expand %s", "展开 %s"}, "tree_select.collapse": {"Collapse %s", "收起 %s"}, + "tree_select.expanded": {"expanded", "已展开"}, + "tree_select.collapsed": {"collapsed", "已收起"}, + "tree_select.mixed": {"partly selected", "部分选中"}, + "multi_select.mixed": {"partly selected", "部分选中"}, }) } diff --git a/multi_select.go b/multi_select.go index f141120..a0ca85d 100644 --- a/multi_select.go +++ b/multi_select.go @@ -28,9 +28,7 @@ type MultiSelectOptions struct { // multiSelectKey keeps this file's state apart from MyGo's own. type multiSelectKey struct{ name string } -// MultiSelect chooses several *values from options in a popup with a -// search field, "Select all" for the matches, and an optional limit. -// Down from the search moves into the list; Space checks; Escape closes. +// MultiSelect chooses several *values from options in a popup with search and "Select all". func MultiSelect[T comparable](c *ui.Context, values *[]T, options []SelectOption[T], opts MultiSelectOptions) ChoiceResult { if opts.Max < 0 || opts.MaxTags < 0 { panic("mujica: MultiSelect Max and MaxTags must not be negative") @@ -42,6 +40,9 @@ func MultiSelect[T comparable](c *ui.Context, values *[]T, options []SelectOptio tr.Label(opts.Label) } open := ui.Local(tr, multiSelectKey{"open"}, func() bool { return false }) + if opts.Disabled { + *open = false + } query := ui.Local(tr, multiSelectKey{"query"}, func() string { return "" }) if tr.Clicked() || (!*open && tr.Shortcut(0, ui.KeyDown)) { *open = !*open @@ -100,7 +101,7 @@ func multiSelectTag(c *ui.Context, label string, disabled bool) bool { removed := false ui.Row(c).Gap(2).Padding(1, 2, 1, 6).Radius(theme.ControlRadius).Background(k.Selection). Border(theme.BorderWidth, k.Border).Shrink(0).Children(func() { - ui.Text(c, label).FontSize(theme.CaptionSize).TextColor(k.Text).SingleLine() + ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() if !disabled { removed = selectClear(c, k, fmt.Sprintf(msg(c, "multi_select.remove"), label)) } @@ -170,7 +171,7 @@ func multiSelectPopup[T comparable](c *ui.Context, k theme.Tokens, values *[]T, }) if opts.Max > 0 { ui.Text(c, fmt.Sprintf(msg(c, "multi_select.limit"), len(*values), opts.Max)). - FontSize(theme.CaptionSize).TextColor(k.TextMuted).Padding(2, d.Unit()*2) + FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(2, d.Unit()*2) } }) } @@ -178,12 +179,16 @@ func multiSelectPopup[T comparable](c *ui.Context, k theme.Tokens, values *[]T, // multiSelectCheck is a CheckboxBase row drawing its own box. func multiSelectCheck(c *ui.Context, k theme.Tokens, on *bool, st internal.CheckState, label string, disabled bool) *ui.Element { d := Density(c) - scratch := *on - if disabled { - on = &scratch + var cb *ui.Element + // A disabled wrapper and a copy block the base, which toggles as it is built. + ui.Row(c).Grow(1).Disabled(disabled).Children(func() { + cb = ui.CheckboxBase(c, choiceTarget(on, disabled)).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2). + Gap(d.Unit() * 2).AlignItems(ui.Center).Radius(theme.ControlRadius).Grow(1) + }) + if st == internal.Mixed { + // MyGo sets the mixed state only inside its own widgets (ui/element.go:282). + cb.Description(msg(c, "multi_select.mixed")) } - cb := ui.CheckboxBase(c, on).Disabled(disabled).MinHeight(d.ControlHeight()-4).Padding(0, d.Unit()*2). - Gap(d.Unit() * 2).AlignItems(ui.Center).Radius(theme.ControlRadius).Grow(1) internal.Ring(cb, k.Focus, theme.ControlRadius) hover := cb.Hovered() if hover { diff --git a/range_slider.go b/range_slider.go index 26d62d2..7a3b088 100644 --- a/range_slider.go +++ b/range_slider.go @@ -22,11 +22,11 @@ type RangeSliderOptions struct { Label string } -// RangeSlider sets *low and *high, which never cross. A press moves the -// nearer thumb; each thumb takes the focus and the arrows, Home and End. +// RangeSlider sets *low and *high, which never cross; a press moves the nearer thumb, each takes the keys. func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) ChoiceResult { g, h := sliderSetup(opts.Min, opts.Max, opts.Step, opts.Marks, opts.ShowValue, c) - format := sliderFormat(opts.Format, opts.Step) + format := sliderFormat(opts.Format, g) + size := FontSize(c, theme.CaptionSize) k := Tokens(c) *low, *high = g.snap(math.Min(*low, *high)), g.snap(math.Max(*low, *high)) oldLow, oldHigh := *low, *high @@ -47,32 +47,24 @@ func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) Cho from, to = *low, g.hi } x := g.x(ui.Rect{W: r.W}, *v) - sliderThumb/2 - kb := ui.Box(c).Key(i).Absolute().Left(x).Top(g.trackY-sliderThumb/2).Size(sliderThumb, sliderThumb). - Focusable().FocusRing(false).Role(ui.RoleSlider) + var kb *ui.Element + var next float64 + var held bool + // Each thumb is a SliderBase, which reports the range and value to assistive technology. + ui.Box(c).Key(i).Absolute().Left(x).Top(g.trackY-sliderThumb/2).Size(sliderThumb, sliderThumb).Children(func() { + kb, next, held = sliderControl(c, *v, g, from, to) + }) + kb.Size(sliderThumb, sliderThumb).FocusRing(false) name := msg(c, []string{"range_slider.min", "range_slider.max"}[i]) if opts.Label != "" { name = opts.Label + " " + name } - kb.Label(name + ", " + format(*v)) - step := func(d float64) float64 { - if g.step > 0 { - return g.snap(*v + d*g.step) - } - return *v + d*(g.hi-g.lo)/100 - } - switch { - case kb.Shortcut(0, ui.KeyLeft), kb.Shortcut(0, ui.KeyDown): - *v = max(from, step(-1)) - case kb.Shortcut(0, ui.KeyRight), kb.Shortcut(0, ui.KeyUp): - *v = min(to, step(1)) - case kb.Shortcut(0, ui.KeyHome): - *v = from - case kb.Shortcut(0, ui.KeyEnd): - *v = to - } - if _, _, ok := kb.Dragged(); ok { + kb.Label(name) + if held { pressed = true *drag = i + } else if !opts.Disabled { + *v = next } knobs[i] = kb } @@ -104,12 +96,12 @@ func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) Cho hover := e.Hovered() e.Draw(func(p *ui.Painter, r ui.Rect) { x0, x1 := g.x(r, lo), g.x(r, hi) - sliderTrackPaint(p, r, g, k, opts.Marks, format, x0, x1) + sliderTrackPaint(p, r, g, k, opts.Marks, size, x0, x1) for i, x := range []float32{x0, x1} { hot := which == i || knobs[i].Hovered() sliderThumbPaint(p, k, x, r.Y+g.trackY, hot, knobs[i].FocusVisible()) if opts.ShowValue && (hot || hover || knobs[i].Focused()) { - rangeSliderBubble(p, k, x, r.Y+g.trackY, format([]float64{lo, hi}[i]), i) + rangeSliderBubble(p, k, x, r.Y+g.trackY, format([]float64{lo, hi}[i]), i, size) } } }) @@ -120,11 +112,11 @@ func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) Cho type rangeSliderDrag struct{} // rangeSliderBubble keeps the two tips apart: the low one leans left, the high one right. -func rangeSliderBubble(p *ui.Painter, k theme.Tokens, x, y float32, s string, i int) { - w, _ := p.MeasureText(1000, ui.Span{Text: s, Size: theme.CaptionSize}) +func rangeSliderBubble(p *ui.Painter, k theme.Tokens, x, y float32, s string, i int, size float32) { + w, _ := p.MeasureText(1000, ui.Span{Text: s, Size: size}) shift := (w+12)/2 - sliderThumb/2 if i == 0 { shift = -shift } - sliderBubblePaint(p, k, x+shift, y, s) + sliderBubblePaint(p, k, x+shift, y, s, size) } diff --git a/range_slider_test.go b/range_slider_test.go index d8ba558..d82bbf6 100644 --- a/range_slider_test.go +++ b/range_slider_test.go @@ -21,7 +21,7 @@ func TestRangeSliderKeysPerThumb(t *testing.T) { low, high, changes := 20.0, 80.0, 0 tt := rangeTester(&low, &high, RangeSliderOptions{Step: 10, Label: "Price"}, &changes) tt.Key(0, ui.KeyTab) - if !tt.Focused("Price minimum, 20") { + if !tt.Focused("Price minimum") { t.Fatal("first Tab did not focus the low thumb") } tt.Key(0, ui.KeyRight) @@ -37,7 +37,7 @@ func TestRangeSliderKeysPerThumb(t *testing.T) { t.Fatalf("low crossed high: %v", low) } tt.Key(0, ui.KeyTab) - if !tt.Focused("Price maximum, 80") { + if !tt.Focused("Price maximum") { t.Fatal("second Tab did not focus the high thumb") } tt.Key(0, ui.KeyLeft) @@ -77,18 +77,6 @@ func TestRangeSliderOrdersSwappedEnds(t *testing.T) { } } -func TestRangeSliderDisabled(t *testing.T) { - low, high, changes := 20.0, 40.0, 0 - tt := rangeTester(&low, &high, RangeSliderOptions{Disabled: true}, &changes) - tt.Key(0, ui.KeyTab) - tt.Key(0, ui.KeyRight) - tt.Press(150, 18) - tt.Release(150, 18) - if low != 20 || high != 40 || changes != 0 { - t.Fatalf("disabled moved: %v..%v", low, high) - } -} - func TestRangeSliderSnapshot(t *testing.T) { snapshot(t, "range_slider", 420, 170, func(c *ui.Context) { a, b := 20.0, 70.0 diff --git a/select.go b/select.go index 1a6e1ef..78068ab 100644 --- a/select.go +++ b/select.go @@ -41,19 +41,15 @@ type selectTypeahead struct { at time.Time } -// Select chooses *value from options in a popup. Enter, Space and the -// arrows open it; the arrows, Home and End move; Enter chooses; Escape -// closes. Typing on the closed select chooses the option starting with it. +// Select chooses *value from options in a popup; typing on the closed select jumps to a match. func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], opts SelectOptions) ChoiceResult { k := Tokens(c) old := *value - if opts.Disabled { - // SelectBase takes clicks as it is built: a disabled one works on a copy. - scratch := old - value = &scratch - } - s := ui.SelectBase(c, value) - tr := s.Trigger.Disabled(opts.Disabled) + value = choiceTarget(value, opts.Disabled) + var s *ui.SelectParts[T] + // SelectBase takes clicks as it is built: a disabled wrapper and a copy block them. + ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() { s = ui.SelectBase(c, value) }) + tr := s.Trigger if opts.Label != "" { tr.Label(opts.Label) } @@ -93,6 +89,10 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op } }) lastHi := ui.Local(tr, selectKey{"shown"}, func() int { return -1 }) + if opts.Disabled { + // SelectParts has no way to close it (ui/base.go:310): a disabled select builds no popup. + return ChoiceResult{tr, false} + } s.Popup(func(panel *ui.Element) { selectPanel(c, panel) ui.Scroll(c).MaxHeight(280).Children(func() { @@ -191,17 +191,33 @@ func selectChevron(open bool) string { return "chevron-down" } -// selectClear draws a small clear button that takes no Tab stop; it reports a click. +// selectClear draws a clear cross outside the Tab order, hit area 28 DIP laid out as 18; it reports a click. func selectClear(c *ui.Context, k theme.Tokens, name string) bool { - x := ui.Box(c).Size(18, 18).Shrink(0).Radius(theme.ControlRadius).Role(ui.RoleButton).Label(name) + x := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Margin(-5).Shrink(0).Radius(theme.ControlRadius). + Role(ui.RoleButton).Label(name).Cursor(ui.CursorPointer) clicked := x.Clicked() if x.Hovered() { x.Background(k.SurfaceHover) } - x.Draw(func(p *ui.Painter, r ui.Rect) { internal.Mark(p, r, "x", k.TextMuted) }) + x.Draw(func(p *ui.Painter, r ui.Rect) { internal.Mark(p, selectCenter(r, 14), "x", k.TextMuted) }) return clicked } +// selectCenter returns the size×size square centered in r. +func selectCenter(r ui.Rect, size float32) ui.Rect { + return ui.Rect{X: r.X + (r.W-size)/2, Y: r.Y + (r.H-size)/2, W: size, H: size} +} + +// selectCheck reserves room for the chosen mark and draws it when on. +func selectCheck(c *ui.Context, on bool) { + k := Tokens(c) + ui.Box(c).Size(16, 16).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { + if on { + internal.Mark(p, r, "check", k.AccentText) + } + }) +} + // selectPanel styles a popup panel. func selectPanel(c *ui.Context, panel *ui.Element) { k := Tokens(c) @@ -209,24 +225,25 @@ func selectPanel(c *ui.Context, panel *ui.Element) { Border(theme.BorderWidth, k.Border).Shadow(0, 4, 12, 0, ui.RGBA(0, 0, 0, 0.14)) } -// selectRow styles an option row: highlight background, check mark when chosen. +// selectRow styles an option row: chosen has the selection fill and a check, highlighted a side bar. func selectRow(c *ui.Context, row *ui.Element, highlighted, chosen bool, label string) { k := Tokens(c) d := Density(c) - row.MinHeight(d.ControlHeight()-4).Padding(0, d.Unit()*3, 0, d.Unit()*2).Gap(d.Unit() * 2). - AlignItems(ui.Center).Radius(theme.ControlRadius) - if highlighted { + row.MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3, 0, d.Unit()*2).Gap(d.Unit() * 2). + AlignItems(ui.Center).Radius(theme.ControlRadius).Role(ui.RoleListItem) + switch { + case chosen: + row.Background(k.Selection).TextColor(k.AccentText) + case highlighted: row.Background(k.SurfaceHover) } - if chosen { - row.TextColor(k.AccentText) + if highlighted { + row.Draw(func(p *ui.Painter, r ui.Rect) { + p.Fill(ui.Rect{X: r.X, Y: r.Y + 4, W: 2, H: r.H - 8}, k.AccentText, 1) + }) } row.Children(func() { - ui.Box(c).Size(14, 14).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { - if chosen { - internal.Mark(p, r, "check", k.AccentText) - } - }) + selectCheck(c, chosen) ui.Text(c, label).SingleLine() }) } diff --git a/slider.go b/slider.go index cea855b..1b31f97 100644 --- a/slider.go +++ b/slider.go @@ -4,6 +4,7 @@ import ( "log/slog" "math" "strconv" + "strings" "github.com/egoist/mygo/ui" @@ -25,7 +26,7 @@ type SliderOptions struct { Marks []SliderMark // ShowValue shows the value above the thumb while hovered, dragged or focused. ShowValue bool - // Format writes the value; nil prints it with the step's precision. + // Format writes the value; nil prints as many decimals as Min and Step need. Format func(float64) string Disabled bool // Label names the slider for assistive technology. @@ -63,20 +64,67 @@ func (g sliderGeom) snap(v float64) float64 { return math.Round(v*1e9) / 1e9 } -// keyed returns the value after a key moved old to raw, MyGo's 1% key step, -// as one step of the slider: Home and End land on the ends. -func (g sliderGeom) keyed(old, raw float64) float64 { - switch { - case g.step <= 0: - return raw - case raw == g.lo: - return g.lo - case raw == g.hi: - return g.snap(g.hi) - case raw > old: - return g.snap(min(g.hi, old+g.step)) +// sliderKey is the last key released on a slider, and a move waiting for it. +type sliderKey struct { + up ui.Key + pending int +} + +type sliderKeyID struct{} + +// sliderControl builds a SliderBase on a copy of v and returns where the pointer or a key moves v. +func sliderControl(c *ui.Context, v float64, g sliderGeom, from, to float64) (s *ui.Element, next float64, dragged bool) { + raw := v + s = ui.SliderBase(c, &raw, g.lo, g.hi) + ks := *ui.Local(s, sliderKeyID{}, func() *sliderKey { return &sliderKey{} }) + // MyGo gives key downs to the base alone (ui/base.go:118); releases still reach this handler. + s.HandleInput(func(ev ui.InputEvent) bool { + if ev.Kind != ui.InputKeyUp { + return false + } + ks.up = ev.Key + return true + }) + up, dir := ks.up, ks.pending + ks.up, ks.pending = 0, 0 + if _, _, dragged = s.Dragged(); dragged { + return s, max(from, min(to, g.snap(raw))), true } - return g.snap(old - g.step) + unsure := dir != 0 + switch { + case raw < v: + dir, unsure = -1, false + case raw > v: + dir, unsure = 1, false + } + if dir == 0 { + return s, v, false + } + end, limit := g.lo, from + if dir > 0 { + end, limit = g.hi, to + } + jump := raw == end + // An arrow within 1% of an end lands on it too: the released key tells which it was. + if unsure || jump && math.Abs(v-end) <= (g.hi-g.lo)/100*(1+1e-9) { + switch up { + case ui.KeyHome, ui.KeyEnd: + jump = true + case ui.KeyLeft, ui.KeyRight, ui.KeyUp, ui.KeyDown: + jump = false + default: + ks.pending = dir + return s, v, false + } + } + if jump { + return s, limit, false + } + step := g.step + if step <= 0 { + step = (g.hi - g.lo) / 100 + } + return s, max(from, min(to, g.snap(v+float64(dir)*step))), false } func sliderSetup(min, max, step float64, marks []SliderMark, showValue bool, c *ui.Context) (sliderGeom, float32) { @@ -101,66 +149,70 @@ func sliderSetup(min, max, step float64, marks []SliderMark, showValue bool, c * return g, h } -func sliderFormat(format func(float64) string, step float64) func(float64) string { +// sliderDecimals counts the decimals the shortest form of each value needs. +func sliderDecimals(vs ...float64) int { + n := 0 + for _, v := range vs { + s := strconv.FormatFloat(v, 'f', -1, 64) + if i := strings.IndexByte(s, '.'); i >= 0 { + n = max(n, len(s)-i-1) + } + } + return n +} + +func sliderFormat(format func(float64) string, g sliderGeom) func(float64) string { if format != nil { return format } - prec := 0 - if step > 0 && step < 1 { - prec = int(math.Ceil(-math.Log10(step) - 1e-9)) - } else if step == 0 { - prec = 1 + prec := sliderDecimals(g.lo, g.step) + if g.step == 0 { + prec = max(prec, 1) } return func(v float64) string { return strconv.FormatFloat(v, 'f', prec, 64) } } -// Slider sets *value between Min and Max by dragging anywhere on its box, -// the arrows (one Step), Home and End. The thumb is a diamond; the hit area -// is the whole rectangle. +// Slider sets *value by dragging anywhere on its box, the arrows (one Step), Home and End. func Slider(c *ui.Context, value *float64, opts SliderOptions) ChoiceResult { g, h := sliderSetup(opts.Min, opts.Max, opts.Step, opts.Marks, opts.ShowValue, c) - format := sliderFormat(opts.Format, opts.Step) + format := sliderFormat(opts.Format, g) + size := FontSize(c, theme.CaptionSize) k := Tokens(c) old := g.snap(*value) *value = old - target := value - if opts.Disabled { - // SliderBase takes the pointer as it is built: a disabled one moves a copy. - scratch := old - target = &scratch - } - s := ui.SliderBase(c, target, g.lo, g.hi).Height(h).MinWidth(120).PaddingX(sliderThumb / 2). - FocusRing(false).Disabled(opts.Disabled).Cursor(ui.CursorPointer) + var s *ui.Element + var next float64 + var held bool + // Disabled on a wrapper blocks the base before it takes input. + ui.Column(c).Disabled(opts.Disabled).Children(func() { + s, next, held = sliderControl(c, old, g, g.lo, g.hi) + }) + s.Height(h).MinWidth(120).PaddingX(sliderThumb / 2).FocusRing(false).Cursor(ui.CursorPointer) if opts.Label != "" { s.Label(opts.Label) } - if *value != old { - if _, _, drag := s.Dragged(); drag { - *value = g.snap(*value) - } else { - *value = g.keyed(old, *value) - } + if !opts.Disabled { + *value = next } changed := *value != old if changed { slog.Debug("mujica: slider changed", "label", opts.Label, "value", *value) } - _, _, held := s.Dragged() hover := s.Hovered() v := *value s.Draw(func(p *ui.Painter, r ui.Rect) { - sliderTrackPaint(p, r, g, k, opts.Marks, format, g.x(r, g.lo), g.x(r, v)) + sliderTrackPaint(p, r, g, k, opts.Marks, size, g.x(r, g.lo), g.x(r, v)) x := g.x(r, v) sliderThumbPaint(p, k, x, r.Y+g.trackY, hover || held, s.FocusVisible()) if opts.ShowValue && (hover || held || s.Focused()) { - sliderBubblePaint(p, k, x, r.Y+g.trackY, format(v)) + sliderBubblePaint(p, k, x, r.Y+g.trackY, format(v), size) } }) return ChoiceResult{s, changed} } // sliderTrackPaint draws the rail, the filled part from x0 to x1, and the marks. -func sliderTrackPaint(p *ui.Painter, r ui.Rect, g sliderGeom, k theme.Tokens, marks []SliderMark, format func(float64) string, x0, x1 float32) { +func sliderTrackPaint(p *ui.Painter, r ui.Rect, g sliderGeom, k theme.Tokens, marks []SliderMark, size, x0, x1 float32) { y := r.Y + g.trackY left, right := g.x(r, g.lo), g.x(r, g.hi) p.Fill(ui.Rect{X: left, Y: y - sliderTrack/2, W: right - left, H: sliderTrack}, k.Border, 0) @@ -170,8 +222,8 @@ func sliderTrackPaint(p *ui.Painter, r ui.Rect, g sliderGeom, k theme.Tokens, ma x := g.x(r, m.Value) p.Fill(ui.Rect{X: x - 0.5, Y: y + sliderTrack/2 + 2, W: 1, H: 5}, k.TextMuted, 0) if m.Label != "" { - w, _ := p.MeasureText(1000, ui.Span{Text: m.Label, Size: theme.CaptionSize}) - p.Text(x-w/2, y+sliderTrack/2+8, m.Label, theme.CaptionSize, k.TextMuted) + w, _ := p.MeasureText(1000, ui.Span{Text: m.Label, Size: size}) + p.Text(x-w/2, y+sliderTrack/2+8, m.Label, size, k.TextMuted) } } } @@ -191,10 +243,10 @@ func sliderThumbPaint(p *ui.Painter, k theme.Tokens, x, y float32, hot, focus bo } // sliderBubblePaint draws the value tip above the thumb at (x, y). -func sliderBubblePaint(p *ui.Painter, k theme.Tokens, x, y float32, s string) { - w, h := p.MeasureText(1000, ui.Span{Text: s, Size: theme.CaptionSize}) +func sliderBubblePaint(p *ui.Painter, k theme.Tokens, x, y float32, s string, size float32) { + w, h := p.MeasureText(1000, ui.Span{Text: s, Size: size}) b := ui.Rect{X: x - w/2 - 6, Y: y - sliderThumb/2 - h - 10, W: w + 12, H: h + 4} p.Fill(b, k.Surface, theme.ControlRadius) p.Stroke(b, k.ControlBorder, theme.ControlRadius, theme.BorderWidth) - p.Text(b.X+6, b.Y+2, s, theme.CaptionSize, k.Text) + p.Text(b.X+6, b.Y+2, s, size, k.Text) } diff --git a/slider_keys_test.go b/slider_keys_test.go new file mode 100644 index 0000000..0b27069 --- /dev/null +++ b/slider_keys_test.go @@ -0,0 +1,83 @@ +package mujica + +import ( + "bytes" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestSliderFineStepNearEnds(t *testing.T) { + v, changes := 99.2, 0 + tt := sliderTester(&v, SliderOptions{Step: 0.1, Label: "Gain"}, &changes) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyRight) + if v != 99.3 { + t.Fatalf("Right from 99.2: %v", v) + } + tt.Key(0, ui.KeyEnd) + if v != 100 { + t.Fatalf("End: %v", v) + } + v = 0.5 + tt.Frame() + tt.Key(0, ui.KeyLeft) + if v != 0.4 { + t.Fatalf("Left from 0.5: %v", v) + } + tt.Key(0, ui.KeyHome) + if v != 0 { + t.Fatalf("Home: %v", v) + } +} + +func TestRangeSliderFineStepNearEnds(t *testing.T) { + low, high, changes := 0.3, 99.8, 0 + tt := rangeTester(&low, &high, RangeSliderOptions{Step: 0.1, Label: "Band"}, &changes) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyLeft) + if low != 0.2 { + t.Fatalf("Left from 0.3: %v", low) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyRight) + if high != 99.9 { + t.Fatalf("Right from 99.8: %v", high) + } +} + +func TestSliderFormatKeepsStepDecimals(t *testing.T) { + for _, tc := range []struct { + g sliderGeom + v float64 + want string + }{ + {sliderGeom{lo: 0, hi: 1, step: 0.25}, 0.75, "0.75"}, + {sliderGeom{lo: 0.5, hi: 10, step: 1}, 2.5, "2.5"}, + {sliderGeom{lo: 0, hi: 100, step: 5}, 35, "35"}, + } { + if got := sliderFormat(nil, tc.g)(tc.v); got != tc.want { + t.Errorf("%+v: %q, want %q", tc.g, got, tc.want) + } + } +} + +func TestSliderMarksFollowTextScale(t *testing.T) { + render := func(scale float32) []byte { + v, low, high := 50.0, 20.0, 80.0 + marks := []SliderMark{{Value: 0, Label: "Low"}, {Value: 100, Label: "High"}} + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Width(300).Children(func() { + Slider(c, &v, SliderOptions{Marks: marks}) + RangeSlider(c, &low, &high, RangeSliderOptions{Marks: marks}) + }) + }, 320, 200) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: scale}) + tt.Frame() + return encode(t, tt.Image()) + } + if bytes.Equal(render(1), render(2)) { + t.Fatal("mark captions ignore the text scale") + } +} diff --git a/slider_test.go b/slider_test.go index c45a28d..2ebaf2e 100644 --- a/slider_test.go +++ b/slider_test.go @@ -70,18 +70,6 @@ func TestSliderClampsOutOfRangeValue(t *testing.T) { } } -func TestSliderDisabled(t *testing.T) { - v, changes := 30.0, 0 - tt := sliderTester(&v, SliderOptions{Disabled: true, Step: 1}, &changes) - tt.Key(0, ui.KeyTab) - tt.Key(0, ui.KeyRight) - tt.Press(100, 18) - tt.Release(100, 18) - if v != 30 { - t.Fatalf("disabled slider moved to %v", v) - } -} - func TestSliderBadRange(t *testing.T) { defer func() { if recover() == nil { diff --git a/switch.go b/switch.go index 3d1a860..e2f2d22 100644 --- a/switch.go +++ b/switch.go @@ -24,13 +24,17 @@ const ( switchK float32 = 14 ) -// Switch turns *on over when clicked or with Space. Changed reports the -// new value; the caller applies it and may set Loading while it does. +// Switch turns *on over by click or Space; Changed reports it, the caller applies it. func Switch(c *ui.Context, on *bool, label string, opts SwitchOptions) *ui.Element { k := Tokens(c) d := Density(c) - sw := ui.SwitchBase(c, on).Gap(d.Unit() * 2).AlignItems(ui.Center).MinHeight(theme.MinHitSize). - FocusRing(false).Disabled(opts.Disabled || opts.Loading) + blocked := opts.Disabled || opts.Loading + var sw *ui.Element + // Disabled on a wrapper, and a copy, block the base before it takes input. + ui.Row(c).Shrink(0).Disabled(blocked).Children(func() { + sw = ui.SwitchBase(c, choiceTarget(on, blocked)).Gap(d.Unit() * 2).AlignItems(ui.Center). + MinHeight(d.ControlHeight()).FocusRing(false) + }) if opts.Description != "" { sw.Description(opts.Description) } @@ -78,6 +82,15 @@ func Switch(c *ui.Context, on *bool, label string, opts SwitchOptions) *ui.Eleme return sw } +// choiceTarget is v, or a throwaway copy of it while input is blocked. +func choiceTarget[T any](v *T, blocked bool) *T { + if !blocked { + return v + } + cp := *v + return &cp +} + func switchFloat(b bool) float32 { if b { return 1 diff --git a/testdata/snapshots/darwin/cascader-dark.png b/testdata/snapshots/darwin/cascader-dark.png index fbfab039cc6beb1533cf6f02f54092fed7bdcefc..f1ddf9a3dc63b68d3432fdef9c95dc4e73a7ba52 100644 GIT binary patch literal 5252 zcmd^jXHXMdur?|PN{e(U0wN$?O6WxpkX}P4Aku`;dkuoTgx*5$2uSZGR3jpU-g`*| zq!W~Rb_cukWbcb_7g*LgBjGm)gv;c*#;&HZF$^KC4jtR{$2=ObCgcP zys?a`Bi6>T($mfntW|Cp=s49%$>C%O(s~Oj1ihtt@IZm8ixWWhGsSA%Kg{YSZiBsf z)OpU`@7KD2sJhhy}=>v+o6 zD^L=%G3F8shy4gHANe+hGmQfwn^vKxR;8V&JJJeCj zVMARcZU2KybLudQl=Z-#SdYNzW|(1!;do>bHMPTC-R)S(y|FM&yZ}ydC?`S4Yo6-$ zE%(>>|5|eI`G1zu<~j$f7Sfm8aUb97br7}3PvuGEh2>9=!;eE_| zOGFVK{E%OZ>G9KFZ~q&W4Nreb!fOrHRHLAQ>gr{X7b%gPf84|xi|JxqsA=h*E24UI zNdQVb`1leG3=Cxo#tI60dU_*wDtqs_(3B!V^pYMSZXOd;iHUWWmygS)9&Jyc{=k#> zZ(jKF0(r_8pwlxEsw#|H{#B3aYo0`hP`?io6z; z&+Vdm8b(xyMedK*3TWebJ|7hf*C<8;o!K%gsZ-PBFGdabwd=oKydaUe_fzd-tvsP^ z^$BnZ>AbTig>T8nnz-6%5dt)a9?wzJ4fOZNF+Nd6CQJGJeM<+qUA!3B-tJ}$YM4~t z{JC5sSnqar=f=%f9p7caC_|Rw1Dxv{L!;464Iq#8-R^Lu6Bo_jD>U>%noKTb6=kte z$nSR>mXnT*ed?Pb02R%V+l8vKy{TH&9yUcTIrNKBOj&6QtCZALo4SOx`Ve`KKk_ap z%94*gmhLJnqE_3>SEu^kunLz@kvqO6ReZAG-S;4xFY8Nr}zqZ^aEv02wF z?^)n?Y6W{2N^<$C8X8a&6S_66@_gCE^yDbOr64QV00y)@7204XIg3)2F=`nc8Zy!} zI5X4kn|AbfKYH45$=Lj}Hf6^TS}4Y{h{-o^z;sni6Pi;Y+1A zcYrdTwdcu-h@#(lG@YaU$7ld2CvQw!MxhpG43Y0ZDtBG∈o2d9AjvWq+ZF`B~wx zF61C&&WuHJk79ACJ$r+hf*VZn*&?krVfkvGoA!fc^0k5S)}w^8-sK1NHxzE&Q*<1T zZobxf>MJoxkCjE^Pd)CNGR80#JT&+cd`s+8b@LqPKzkG#(_d%8RdKZ|=)l z8nYOWV)bSztR7iD8WM95hW9lz?1}`=%NObRt~*4_zg%jv#va>|@1wai+B0^Sr9Eb1 zV{B{!^bR0)vq_3d9H6$=7s>yBfdy)dY{Q_hB|MaP7RODLdva@%~KGlSTv zMzXe#j2S2LhhraKUSw_HsUMLhySq>An$N3|<)7g$p5N1cMEuADRPwsc`Ym|xjSO(L!-J^1 zYDy;X!KU;j^<%?e8OFPq>)f2kYwP1o7KFo9-;6C$YB^)wN_ioj$TK;}Oa=WYCQ`AB zYVeGYha8X3Xl`-Q?Q*ai6N^n3vOC_Z`C{DiQ19^;U6=ILIr}HFIOoye=jQ@BAOOi-DE#JI2>5p^{ z2DmjK>KitTGwhi|xi4-i&Z&sWA|h)D^5Gl2TDG5WAvy{dyIb&Qa>(2msBg&kn!mSm z?))G^N(%IhMQw?`h)PR(@T!jl-s6{{yyp7Fg=AHWTCXWcNW3_E*;tnx99})qG1E?l z!)1EWDbXFBUbwQ{fg&|GL{^`A1%beIm)WwcV2Qh=Ej-EPgP-ex( z8j*>5m(-IZbDzWelDH`D<0j7kDgl5SY(e8m_L0Bug|JfLxT4!0hAAP*05m-nwdIlV z^&NLe2UC=)n!TN!0s(7Lq0aXl1G{u3;P;8%{=@*9iOmKlLNP9iZ9IA^cm%*6qKlLc zUsWm6=_`D(&0x;u03S#vI{d%@PLa#6sHkA;ASe|P=bLSnf`+DL2X55s(Ut0pfN33M z{*DGS5IjQ&xJ5<{44Un{V~{h3M2Y*B^?enfAu7_5I7}yc920iW(;3j5D;E7n-Z3pt zvX2g*W@cn?pjap=s1y{s6X8GoT8sio>(D&e616($K7#$x`wZ9&-ikZe{1R1QW#BwoT|$X9799vvopH^o*lWQ%0U0*%31p z{ndBtMbZ*YfB)P5{OJJMMECb^fBW{07_;}omA7q>ngl ziyRGltHs)}8^Q1)P?%E@Xn4DF6mQ|gJ2|{JVD}T+i>mL^cb$nk+E?SXmS)wZrH*!vfskcxds}u&G!JL1d8_Cr-Y>G% z)y{^1Hw8sSzyo`?D-9u+4|k)L5V+Igakfy;XuP*rY!Cv z+efa!fT%7_v(dpKLIIL#LpQNFw$;jjYVsFnn zes|Dq>vnK~`CMU|Pe6Pn=Ce$%=S9l=inDq#=v5X|xPbNr-+PU7^&CL^XVcmaf8_b7 z>!Gr0d>@zFdBo~@)!j91yGtx4DS}A6L-ysxTJMvoP0zdK)%)n<;SKhuGC(=-PT$$|&gjSWbE3x` zo6xN=XYWNWPJW?3%%M)Nis$EueFBqUpn0F;**@}T+SsF;b|3`Nju860nykuCG%!4T zb(vW?nXT;ZQ8O_$wI$444tm|Dfo*K1NR2&#p?3gOEUw zWG&g51~r%E>ic(?6;c@=7*n{(bHq&5LKtA4((Ch(jb_?!>`7R$Tzc0zY@h`YcgnmCoFHa+R8-c*obVl3X+riA7#J5e&0od)t( z2iu`woPg8rXHEDD0Qq-c3gJ8aI%_U%kxbzk8ODx$z`6oq3FXagQ}EH>TmThwVdu?+ z3F4;HRgQb_oA5(eR}%t394D$OmZL8k>+RpkKD#Qt8qez!y!G%+P3oeRz+7zhmBh4~ zuOGNM&E=lm7OoPX%L3ZG7?o6@c&W)a{|-_rgstv!vnBLP0Z)I64!TC?mHxeh;b&TpEykWGA{oMM_EBbzDn*x$bSH{zxY1@ literal 5222 zcmd^DcTf}FlSe>^G$UP_bfg49s)SwwNR=LXl}<2#NR3iO=~W0Fl^Q^Lk?_SJz4szS zdhZZA{J5L@@9zG*xw*NSoj3b--t6xC%)b4+*(hCYRdNyr5vHIRmv1_-@R4J9j;R|5bO`+maIxD`Jd1?lkO zt{z&oHaezeMkHD7%80Pg(+2fF0ubVVu#9#ZBa)|OC5^w|g>Qea3J>~T;Q!V(fXe>y zSjUl51F|{|$KU=+;#_-%+vX!2&jaJLR^=>p4U>ZT&Af=G_-&ef-3@pW57*XzP?^>57(NVWKR)&j z4aK)bfKQioC1BSFlk56pMa0V+3*y0vk7V!&Q@W=14x)^&)a9x$41jD;CSaYViWd{LMyAvASZMTvo+<+z z)}k`F5^pAqycx90b*D_A7cq?Pa{tt!Uwz3=Va|D%BBjH{OzV1K7OYKy^JYNpd#G=x zA5p54DysSATOg3)#iFg1lDM%KQlU4$RUZ+#1)*o-72aN-vPhe<73B*O_=}&DoqDoJ zJKZMGhbw*n=8tYHuvH)JQqQUN=2C13KL}?h&up2Pbi>Yrn%_S8*qW?4Fd5N^x>>~N z$YFI~z4F`kG+0GtwuKVU%sH@+vPc)>8Y4-YYB^>y=F;#?r7Qr>7sK2XBomaeX9kDh^)=VfH^@Zs*X7SJuA|sXFPF?ml4HLZ-NyLn#gAfna|6j zJIDLg*`3ujdjC*17S`61vr$V+3=P7fV`?2dEEg6yeiS2|&b1EC%l-VqcXoD?mA-xF z9^Tmw3>$jloZci=HeEdrf4umvwdQJxMngv0{Mg%PFD2Q1x{0SXSB>Ll=zed#o8`6Z zaq*I{qo!80oYmun)m7!f;lsnv=vitmpuq{wev4TG-QJ%V0a;*_wG3W@Ak#Bg!(P7Z zr7Hj4MMGl8v0J4=>W1<&*1vW0NY%`FzR^F~Qha1!DhW&715|)jd|_*}?+)ft1ppmF zeeF~0Ex$>Lrc4WO(qG-k3-vg@+b_dw)C@j>m^Nz~S?H$)a-h`5G%W~@`(A1c=8vv_ zULzg`D|-yBJ8wY8ZnCLE6&$Ni#^ppAz84f=mZ0=+UMzcrk&#VFXWuuo%IUT@g`OaB z6UyO=Z#KO%)i~#y{Q7I^x#wGqGXllNd%e~Nb{PBmFZmvlrb~ok#ICOYa0#YWA6B1s zM6*Nz7Y_vLU}kHZtJHLeZx%CX@9wM0`_;wrEKz&AdZxHsbY22n+K7Rz+rH{a9cl~a z8nhdzsIH!l{W{!>wTcElUxdGt{ygL-GAdF<`gzxn!izMUQGNha<_E4r`T9OdjDL6C z7TWU4%Mtm;XnS0@n}MszVvjdQ{%mAeB1$a~gmBHJ?w7ToO#|ExZbIp3NL zR5!%GY+;KiAgq%i>K z*3$<%o#YY%g*JXp4GdX2ySw+GjR%jm#ngWP+c4eDCy%kPE8FcalY?10y5)g%=xZCV z>gAz@sW0m-BKX%-NqXFuaZ7MA3>~VgDXY^~IPR*-%m4jto0V;mYf^{pkIXFQU^(9? zxOC#FXRu~XiKXUWWhav`h?-kai#r%cLrsn4{rTBuDm{tV+=98ddN?*qj`=d>N>1j{ z<P=>pc%8vUIpL4d5^ zOVE$MDPzuxU31$)ckZW*g9|9szK!r3>x*p;w}?*rliT<55tdHC;BfkN$*%dNy;X@_ zEjpe^7)OEIyHv?o;xios9}jU+A@RlM^l|xBt5Ei^Or~v=z$I^*DS(aNQqDEoAxZ5k zr-0&meKlQ?i2SVtuh(^f!*aWe>lnIKXM~OpD;Fdl%aE79cqt`7D>jl}gM>=P`s!M< zDy|0f=F$a+T?cl{=mX}jwWqni$}J<(SYJRlE&n2U%| ziJMtLtMxK^GYY@E){ydcAIn>TuQ&?lx&goG?cQ9(c!&$01#*7ofw-A`Hu zC5orbjf{4ayO)x6@IU_UfBB0&Y@$3)8L+XnH9ch_CQiapAE?zrD?DYBdssWx;p_YA zDfRS=`^ifQkn`Av3~xo|wH`z4%Wosc(c2pzZtg1QSJy{ZXXo;=xu}G%URPNlF)X}NMZHk$hoMEtbsi{Ui2Wl$6xf}=DlRp1~*!XwsL_TB$ zDqbQ!)i7G?8<^{v2R^}62Gdsf#I;tVuRR=%jOM3)z@m>2nB)e0H^&}mq@ljP#ZBCC zK)g?7y&0wW-u?mB!yyUrNmVe7E-jhD=M4m42;hs9q>u^_R^a5`)5ZyNtpT#UEd16= za@RfJBv7TH@$m(y34pJ(_lZH16d1Q-XKH4wbMqK&v%4D9ubpFznlusk?%x6_7*5tF z;%ZJA6e-HGu82_orpHy;bx#q8BPf!w@GjhEIj63xkE{~hkR4M|RV}Q&y1Abu)k(VzwM*{T59UOw8Ge z*a4z8(Xe(fsh~w;d0{EIA901bh`4+8Mf$IN+~-1jN*?Q9e<6be*)4?)qs5IoK84kD7wO&ty27)j3(ft0#9~Pw`^! z;4V!UM>-lTh_}%SE#yWKssM9y%b=5PL22D_Y^@y+gJdOQIaQ2T!s9}R!Op(DB~!lq zEfr-2@gVus2}WYLQ-_hE3Pal$%vzK!w#K}f__58pp-J+$mG*b0!S5xpYl9)HM8_L; zQqGIW*RQ$z6KQuM-%hQBxwa{MK(<;130y3@23->NlQ*Pis{obFuTt(GIjVXYg7hEZs}u2^-|to0@O2^53UF*V2gp%@4nbS~?p*+)}bU2V3_j6J^bFQeFq1HNbIA zdqY3|;Z6D#;AiQ8KtF!EzrsIBz^wyFXEw1g>$DycW{A0Z79qUu{aW?lgF@k` zQ1n2q{=E?YBx-p093Ij^1M`&lVntWeAA+0lF`F#2Jb3#qUX%OwXcd(pn@tjq-$_HL zP&7IDJUtNFUP8VlOHPq7U_EqKX%)AxFYh4X;1Ez_E9KB;8nzk{msT|T zChS2$Aa*~scjZTG*5$c;4Mc&qvpb(~KzTpZhq12ZBk@8&me6PP>KIPM^(4S1;nFV0TiMvI z`Xr|D4XjdxQoAk6B&`xmB5i~9XcDob9Uxy)>@D#L&t(= z29j&e^}FiBonKBSo?Xv-4h-^#otR?tO#b}2CilziY91vVzK*%Lg8%3zNRB|DNMKMn*=@*wfEPWj@c^%E{*bJn)GkunOc%v z(iMF7VFh2Pvb9m2GjRuu^Vv;5JZ|z7-Z)N|)Y`-L0Yau4T@f!`q*m}+r0?C$OP(=S z-8!tr8uEqVOHz0-aX9gL7DYO_?Wh<0X!*|4*4ZR zPT|*1@J2K67q9>{J#qnZ7p+T_s-Q5>H0+p`t43W7Tw^u{^zF8r*5TyL z6XD}0>{p$+RXGW?o2yZQY7Mw|r>1&1H~;u{yo;|sf_1>zJ|o59|F@z4>xBLvSdr3I yR@)OF$nFMq?}ZRs(la3KDo&064GBnaBfwK|#ZwG9st>$N2Txr^Te)1(I^SQFRR$8X36B0&PUu(^Tkh$s{R#a0xnsDNnE zszqB1Kdn*)MHFoj7ql+5Xf0ZoDk$Ja)mjx)tcsQx5!q$mK|n%;geA-S49N{NfG=7t zrJnEMkGbd0ojK+?^O<|kOd^F_orgdm5D4^cq<}X7vH)3tER#k76p{g2k^xD7+m|3q z!5eR)jMS9mq=#Hiok%3aKG1tDGMO@f$+WPtF|inpeUM9#OK*}(?=IQ;5@eH;9!{M# zS8upJc7Wb@iN)gLg53Da=UzAOA(tSR zumf@la_J3o>D?t;UxF;G>|kXZ*X7FY4H5%#3392+T>1}Wj>20w1_%TKfw~cqp-_MQ zn~-g6Xei9fZ)|A9j&AiVD~p|!gj*m`y95G3>t7`jiEw9cD(=S3KknRnb*N&o7#hC@SI>=GUw{0k@cf)yqJhvTBcsJ% zexbr(;C^{MeVklUR5aGc20LCuti*o@>YAD(4B(6K|IIJLySjezEIS9c2wT72Z|Ucs z;a0C{`}XfVJv}BmI=mv(umgLYot<#27nGTqwR7j*$xa`2_mMK0YWDWyzYPvjQR$ES zUnMS=dm!v5K96r^W`cbH+1`Nc@k8N!KL0C!zrk8V%1cYnpN=|yD124GTHJsCHg)6b z@1_>!0NK9-S@xevLZQ&cZaj8Km6Z5^OeT;0;7x9RZ&A+9PK!VL6t|F`o-W?(HC0qp zmX(!T+l<4Gu0u627>PG~M=TawQ&V%;Fg@&$YTC3ZUP~6Wwzj6Eq(+=Nl=TN6^TSe1qIGCT?~v25kiJ0CO%*LmY0=^#bOePR8>*& z`=!gdS=mfYjSuYY?HwoLaZ(9|!pr9`q(4m+3Is-`Ca&}5(&%)AP*r8+)!1M2p5-Vh zDVdM4ba8hhkw_sMf;cZ;JiL4F>Eq<(eycS!HSu9UuOXpOc>4GWUSogbq$%ywu7tSw z*qE~mmUv-DuW8VrLDtq|af`6#=H}@iyM=7sY-eXnl$)}1e0)|#L>|-98Zym!w)>pf z@o~4ZvvVFiyiKFgzT36$?%hN#muqb^*3ZvZM@O5WjPvY8i{>ZXx%-%%Y%t2e%WL7Z zX;UPW$;-3l53*S7 z{wn>aIDNFnZxxwLMhIQLd^PIK+1%VbD=W(tU-@)7wPYndvbKMbj|U+%aNs~ID@%mX z2On4~D=Pgsfkf7!AX-rk?PxO~j#^Fy|T z-o1NYBobNMjQi%B4U&NSbh7o0nyi9?!q5@JZ(h58?{-3CLjyvHsiAH*dNhed;_-Ng z!wyzIf9~S$rZ<0dSj?%2Cu}wzC)I^BQHgi{uywF^bejBx&5k^NqOH8!7`{KOvbnsbaF$wWEuUxoDB9UggyY*L58DU^BbB>#eiV8jk==Ee{H{NyL+}N12332fqliqdS z+|DzOe}hCKaY;!jkH?oWx3siOQeiA9DgA!m0b^sM4I6*f(&_Zgn?j~$B?L0s`r;GU%aSWALuuGwoBBRn4?F3K?w19{F0K= zupbUBTr_`wj)&XbyNR(EFCm0D9L};YR^;a9`TDMKp6YbsM8uxm`w>F-?j^2UyFpJ+ zcU{1mx;l=J_sZ((8hk>j#A5NIN69vwrrUBR6bgt%E`<{I!=dx%E-qR$-}mbkWo6~Q ztNc61Pp8vMOUfGBuIt^q_usme5U}2F?Yh;hq(^_-w(i7-{7)s@>)JmtlSK%9w(N^z zhr-WBo<4W_jJB?>^>|zRNsd%1_37i}dQRQffuRy0a=G>QZYNldZNJktH#ObA^M~hW zi)|h35kd@A)f3@|8ygx@lb>+v>Vme0Fx%ECYU=9crKJd=G1lX*{(5QP;6Y=ot?@BH zZz6xm95y!QEYWD&aGf`|<6NT`b@F87sZ*zM3tfzj9zJ{+Zgn9K54YegK?tFShKBR! zV>fRO{B)rwLTLPWn`zFo@85qgbt>`i8Z&0}?mat5B+`r-(`HO}k2!n6+`=p_?q*e0 zRaEqKb#*2}sHVE+bY!$_k}I>Mq(?JnPRD<(c3r!6BQrA#wMki7`S6hgav=!>g6%tk z4Gi=plfI`ILIoH#W;{?`2Y^YD=f%XyZ|;cz<7tYs1kh4=0zMw~h`Y?y!E zpg{v?&YC{Q!*%4y5eOj~mAYxumZqj=MMZkY_&ZugMy9E$v5U*c2%&M~tT=TX+|!4X zt%oJI(J|Q>ncluDON)!iWb&o6=i2r{HP5T-YHRTz!b(d@ z#A0#klP7u4h)il~Y9gG)B_+khgNJ_%X5vba!z4#M0`%4-$?}WQF$f{IPv-r5l76D2 zgNKJ3ZXwAU_7$=iZH6EFWQ)aOdj~toW1r!}hiPdINl(v|Kz71JdlHF+5TeuRHa6q3 zGP8;Q7bhc&LZR;3wU-!xP;qf-d3iZLuvFF6HH8I5UCt1hOs0v6QQIa`R8)V&?)H!< z6pFt7$Q#$=v$C=|_4T>A`9nJtOwG)s^#_a5X6Mhv67i&GjvP5cLRf^*f(25HW@Kb( zXleumNWn)YD`aM5wh5dxoQK z#@{}FF1Dtowy3CBBoZ|@OCf(Ni@&2eJ5SlZZI{ao_YXgGoc8flvrfg6zL;!%2C|Yr zZCxF0U7g9(oXmW-jh;Lke0=o znwr}fO2a*fMx#lba(+6U*4&nRd>&s(Ns-AUrm+YiJ9}HaOS`dHY$}y%X=#BS?Xs~M zw=pOHx4HCt~U>#>p>iJZodw{>-!NlcB047Rkikeqk@`c1!Gxn^r?t*<{) zBoZYjr*w|LqrAKp8X1kceEI6Hzg`IsKkDuCg`eLl+}T%?t#3fKqO9zf!#^)r;-xoi zsN_G5&^I)^mvBcY5^C$|rY0xPaC4hBJ0w?JLs8ntT zi5VLck6u$!Qilu~EU9l67njIpn#sv2ix*4Z1V~9qH8eCp2pO9g#>d}Uz4|K}jX-l_ zW23C{D>D|0Z83k^j83PwyLzk#i4V)mD{jPHkv#EFNlC>$UCYG8=*pGfU%qURykf+jqu3Y}z*LS&O zNMuCRrAxoz&aPEg$FZ`qw6(Pn3Wb4zn$j{9c3IrLc zsXM<7eqMiBNffOPg-4%^KnN8V7KVNkoS&PEI-UvCcT`?casU1U*K{q6d(qrYTj zW$)OrJ1s54xoxuS=r}4`6tWWZGL~iI{8{8@yNdWLg%Zcp{aoo`u*zl z!h)io4*i^z^a%HKtw|qFq|vCWSFcNbns)PMf{)Lt$H_zu*JZ}E*x1V_PDJGA7sN!L zpWxth?b;1Ih_Hf!!kU`ep2tb2(-;huhYwjfIe7^Qe;hm*j(fV5=O>F7e!4U#C$FNS zvZ8;ya@g>payz`A`X^?xBob-4-|ACGem=C1cnpdV8ajNqm-m-M(hnTC^nd@0jfvT_ zb-P$BHZ(E$bmYuMzF8LP2=PQ<9h>F^~ZHIz_ zg0Y#I+b7cE)P#>dyciv`KXliIkl=bw{qujyDqbTGAyipWq1<1Yc>3}3Wp!26OD-1& z28V@UF*ML z<;pcJEv?4JhT(?~XyW($yu21hMV|`|-NR(6`T6@gIekdv>QKD{2lsB@{@on+`9yy` z*M9uYT_N~jgmE~WoSeM%f&QqY7l(>ByY-J9Yvt)VH!SSXzI|b1#*Cgkc~T>f*Lke0 zrLC=X@W=1BhV1n4_>@d0Pn>AKVFU5L`NBo>Yieqb96q*v+b%^#`m!&VbSzsF)9oZS zgQ3#7hS2$3GUvC!L7RetXH0ifS7(3Dn(cD@*vT$GQdu)}XGq}s%`>JGFH@SB813A- z74Pz1i^Q$Y`-|gsbnv@v{=s{2>;D&@$E&TWQPLLO9=#m=B8!| zB|D1G<56ieNwuM)+KRS>QzQ~e>R*y?G)fXe<^+2ew)-2u6>B%*);r0i?yP^d_Jyh{ zqjNsUskODWrKLq#xjmgsO~!_7-R$l@yQZd=$yDo{9$fBADwV3F)c){FV)1deR<878 zKfd3uUq9@7Z8bMH^ZEP%0|xYTKwCVDiuBHxOifL#nwlD!EFZtWUm%zJQuca74^1o< z*Vex1@&oD4UcPJ~lNJ8Xr}2NTlkFcCrAK?cB`Njmr=zFWT^HW$S`-R}(x1}vfwId? zmK+^#zdg?wP~VceIbp6^ji^VoIHU67Sp|=UQdZg~+=`Ma2 zi@j*^0^I7UNF-8s2kcHg{66mcW0Y=tK%n(;e5DYW1VG`WD4 zDO(ofDDgAjuSJmc=L>p}mwn#eKi;m_bqOI?&Uu=S@DtUD@oGNit64;L5|c3!9BOP3 zWK+sd|IZ?4Z>QFpQoiwlF6q+BE;TA!BxJi%=06(%qHX|aky`8g`_q+Pmvm`km)e!> z!P)dBFpRI?U!UjG|FYP6b+@fop-)P=EB!FO=#nmN>{7e3McQ3pCTk{ZCTk{ZCfjf( zlW`K4lduw4lduve0!BlVp%Q14uo4Fv_9)0+1^_Wc30JxtRWtwq002ovPDHLkV1k_7 B&yxTE delta 4367 zcmV+q5%BJ|CzB_TB!5~-L_t(|oa~)>SQFRR$8X36B0&PUu(^Tkh$s{VrL7BAaRF(I ztx~mG_-U0YD57YKxS(~ZMXRWYRZ!fmTC0MJ71RT;T!LKckV|hb*?LpCfX}Ois{*cy_E!akLK!^FsE${Q9gs_q zOMfw!-d?ixCdk6d9u8J^U9Rl@LSjHJK`wQfOaFn)QF;T%0D(XtkOBddK>-<(0XBcx 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 JPDHLkV1jhM=P>{P diff --git a/testdata/snapshots/darwin/combobox-dark.png b/testdata/snapshots/darwin/combobox-dark.png index 8817795d71ee9116e36c310e17ebe9bf8079b478..f203a78fe4ad2d77b68a62034c1c5d672782e456 100644 GIT binary patch literal 3873 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 literal 3925 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 diff --git a/testdata/snapshots/darwin/combobox-light.png b/testdata/snapshots/darwin/combobox-light.png index fb9d148c812247dac2057b6c1a7e7da0e91676ee..38364d3aa1cf107e284b8bfae3323ead10127c78 100644 GIT binary patch delta 3153 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 delta 3157 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 diff --git a/testdata/snapshots/darwin/multi_select-dark.png b/testdata/snapshots/darwin/multi_select-dark.png index 01941b7dcb3cba6c6346123b1e8b909be2825bce..1e764a443c14debeafaf1d4dad5b35d10a25daec 100644 GIT binary patch delta 3579 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 B3M>Es delta 3564 zcmVt%jn!R@Ru=rT>%i4R^OjzcoW(WiVfd)XsBaD6llTi=5e*oFh z1X&jSVleV_aq@6+(Bo)h7{D4}O~D!`2%)$mzo;`=SW-7ZWJ6>>BeJ1EgEmwRO1Pke3re`4 zgbPZzpo9xbxS)g!O1Pke>oqLlf(C7bn}xoyz8)82F=>NZiX;gf7mCI8!pF2pojiY* zdjMI0EI<|@OF>p&kE;-O5Z!Xxpr*n!*!p_hz7$g{&!2V=whLgp0JaNYB)fpxF2Gn! z^(QPsi0GF0OIp>+^QYYd$O2>mvH)2MvLMi?6O$1S6n}j_WHmI{lWll2-gKQj(VER> z)8=Pu#+$BzKJU^!5J)Y=WHDdR_%e<62G7IG10iH?WzKiw)8<#~9#>Cq`c&tsPE&Mr zv}qeu7pPNpfep`U-I@rlp02XGpiL;+dpNPSuAUz6F9&;Do~6abOIK*a02CbXUcI2{ z_MJl70Dl_e;^=YW&+qGbiR9h#2ls0Z?mG0a>LEQoG-9%t>tfeuq-2yAmC-h2&!6HQ zmqihtdq#M3xV5roNPMn!~`lvVtin@_(*y;NIS zTXLtQ%}B$7v4BqLX&vlt#9TyB$^xWj(@YO zb6o6pRf_t3;?DC)DH$o35kkvjmf1Pm{q>`->9-HDdrnhqH$+En`FeYsP}E~UCdNkP zmDOi2q>030kyxCUUl8c?u0B^!^@gz*OQSE7ZSRmgX%o(#>EZ7^*VxDqAyi%S@N(7- z1&$l&b2mgsPPDen%E{~KkSv<-_kYgYZ_)F5wPwW`Mh(Bo<<48tbbPm)PxZLMt^|NJ4}oqy(Va!%&Wpydk|ESrxI3V#d>w0E}K zw`2dcw5*VbU^^#UeM9|C-+V$Mbohq^Ep4q&w{04yIgUPFf0Y-NrJYR+SR4?rIAAyk ztK9uCG&Z1n2v^{Oy2iJ?XDJ`CI@@=4O=S%|I1=vNExxyF&jFEGtaQL>ik(E#DHMsH zH)l-5(rC(L5kmVCj<1he8Gjt`UU0yBPuhgJ`2`tQuS+_mZ@RkZ>2kjQ_8$#`CJe(q z-MDu443DeXH|crE$k1RK-|6C|D;Lr-5JFFc!q5f&<4ugG@f~$JoE<;xt*)WA-kvNF zT9}(Da6CQpy3bs%hQ_A!%drs>kAzF-(h)+Y=BDnm+)_?l(BTyTo&jCr6KS z0fu4z(iIIt6cKQu^WTX}3?>*7l^5jn` zBuUb?F&Imu60)^*^?#p#v%}H;4F@~hH(g!)z2D(G+Hc#rn`dc35JX6TZ|{?vbvXw5 zTo&thudFRCsMVakt(C5Q z@oIinG}H;21bz>%BGvO-|FZdG5>Xh9`+iSTbMM`^Gdz31lU7xYbo#$+swO2|@o z@)1OFX+?2q#edHie~DhXbQ<5ua)KF~&6X<^%9RL0NF)}MB-wXQU4u<|C=$1~cTj78 zkyzXyXnfKpWHK?uV6QfSUR*ei@AzRa-BW8_`QIydZWgpXZd>_5goBGiZDlPry1G&8Sw%0Cc7k$(} zNQtn%%kYumtV}AqeIwt~%VYe6@!9Fww0S7qYien^nw^6XGBGxCpEfn;)^D;d8A3=R zkt*HeJK9GqSy*e$J%^rSHxOQR36_O_GPu3mBM)Y&_Q)I|w}LUF&kW*VPLE9&YS zoE_{gq<>u&x3^P&U?W2v9c@*t_q>6+a#dPUdF=-E;i;)P&eGhxzM=8qqlT$Y4yGnX z%`L6e*YLSdH#0RppPH_!$)yiKeKHdSq1;aDc>)pH*9dKlwO@X|6c`q;V%_pD)_?h+ zqGoCIQemse$kfowXLf2bwcOeA?Pna0&c46Lmwy$N2a%*)F0U%1E_1E^kI0LsQ(b3F z4_&n=HfBq0Wi5TkUNKor?;!8K2vfq#JIK4T^#0%r40^UTdJ_?nEcxx(-)EJbp1pb7 zrwoj7Bjee71uyAdgP`fWfI5Bnj`r@;re4a->U&+whG!MCD!jI?{$B@=DPE@}bksn$ z`hP(!LS#hPB0~fH20@eM1hYWjdE)l=x_SXgBA<7?HvTCx{c5(Bre<)!dyiY6Y}>h; zo{AcqTOKtCLKpb6S*+H!HlMj(JS&T@w(lw^y65}uob^#Ff4O{3pUVyM_bw^xUEei7 zZk=K~d6uVpenHVp54VN$eRm%?T2b}jFMqKgOQo{7@AtU6IImd|mU!x?-2B&N>wRoX zxj(4u2DduTh3 z<2ji(^RjMr$-4H$?G0bMeAc{~-Cf;h5|W=LEVbZO;7=EvmKT-13}Fd^U~90qet-9Q z?=M1-D}5pfoKXHQ)PHm;aQ9ZBq(jow&?I=&NY5U2_f*vd8=_Y+nOI>->09p8DHB44 z_sU+px&R8eX5**lLK=loc@yAY*Bw1JAaPZivkU*cP6=4XW$_F(G zNy${cNe~3t8)yIAeClYrQmsWIlchd&+ zh7FISLwz%5aDWeOL#^>bfb0uGmSJvcM!z1g{`}$UorjY98J{B^9c=?Hw@oONbV~m) zfRL7^CP5NiUEQeP0NvPZHh-JN>Xi1MsHr&)$BDi>485NoES5r{fbD`;vR&{=)Z%Y` zOrU#18B6?0cCyCN@f1`|GvlJ_iXrjbZ;|hsk5<>*ulZvAmrsZKkhnLNV(E3vWL1(# zI+gbkJT0kA*3Z&U@*dH>-zAeh`x=Fku&Pz03a|Z)(4eV*%gMZX_kUJlv!HpPX(>n@ z#Ciok69gKxNOF)9U%9I<;kl?yBx(~4uY1sjtzjej(?GAedh{}{2pq>W*v}qQCt(_F z0>{mvH)3tEI<|@3y=lK0%Tu9$l?S64@iIU2c*9yVlWsu zK|m})EWN5&8dH!JiaV^#P3RuzH5VlYgJErMDin7>EI};2s#qFR)dl%Q<@08`ayi-< z_ENA0jTzwtAryDy7nS!PfLMZ98mU+sQ_n7d1`QfCXwPpLga!kI!|&k$lTi=5li&`! m13UI1k-?T5J60h3e**vC*$fW_zr6C2` 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 Y6v(T%J69E?g8%>k07*qoM6N<$f)gwLQ2+n{ delta 3495 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 Vv_E88(Z>J)002ovPDHLkV1io8)^Gp- diff --git a/testdata/snapshots/darwin/select-dark.png b/testdata/snapshots/darwin/select-dark.png index 0141a4e802afa50ac9b33225a318efca8fb7adc5..5162a2e37fef80030ae2fe9a7bc82ecf15085cb4 100644 GIT binary patch delta 3763 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 delta 3722 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 diff --git a/testdata/snapshots/darwin/select-light.png b/testdata/snapshots/darwin/select-light.png index 9175808c3ebef7c34abcc3cdce532c32775b2ff2..3ef902fb948dae323322e6db78e9915deb035252 100644 GIT binary patch delta 3984 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 delta 3911 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 +c z?1xe`+9AuP7oDw40zJ>L9LhYG{9QYTt*rj#cdpYN#N3NQ#6D2C>nu1l-U0Pz+iRQT z!YX!Q?oh(~f|p#HZ9U=#(^$r?O^Ap3g)}uN6uE4ZH>(s-;AX(BR?+K$m?wUH{%<0} z{kECB1G?GuyOAa#IWaMBXnUNmBY$X(__f5E7otgo#W{HB>^U&_CIfY+o=wxdkHeU+ z4ts1RWH66_Cuca|B9pPcp`)s+RKdkW2V3lO;qIyLtJtjAA>F1G#EVq*%y$NNwIy-V zI_2*VJV`6h3>uHlUn|pyO}b9QyFy(NDO;R|<^qxI!2+m{HXE1U+0ln8xuXs~e0g&| zNZJ98j{L3gmZ%c?%@Y@ML2HrU`>3<&QVW58x)gjv00QJi?*;hfc$zuS>nSVGnaWt? z63NVWKBk|5N+JBN5R}Mc*RSp)!4n zGUjK@b^c{50AG*&!w?EFOMTAWXNSKR7wr zya^GYc$%4I+?98=^dr7rm@SbP250c-l)Ik0Ew;eP&4o+ia4#H>OG}>@zR-l>W+@jL z5@76PRjM6Vw#J~;%W(Bx@^y9Rs{8Fm$2a}~Ui_$~alHamdQ(^8s|jHrwpCr-wHGBG zmW$a1NZdxR6Ci`kajae_LD4r=y_(T!AYynpqL3M-x-wWfPV?vcAD|r&;?~mFdQnUn{!0 z$ev~FZn{H~!z9xOPRp)8i5`w<_ey5%4qZ4~_32P%_DEe>yM}n(8e1+bkl`S!qgomF zdp+^BzG+b0xIscsexDiLnepS;THfHf%fRaar4~fR&>5micOah*qZ(7{l0RP-8TGKm*O;_%oZ4KL|RauvmnDH>X9kPR@J z;-1m%4yQPMAuv-!wvjsyZ}>M^YwF#Ca36I}oAp(gloEGImbP@9JMwM_Xz>s0Gh o;aD^27=Y^k*?|F^ED`}^^w7?J?-Ug7ek}p&$~sDQ3f7VT2Vrt5Bme*a literal 7291 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 diff --git a/testdata/snapshots/darwin/switch-light.png b/testdata/snapshots/darwin/switch-light.png index fba34d78c24985cccf41afd822e4825494b5ca85..1f882560eb1b9cb64c8e98e489dbf0ce9cdf1767 100644 GIT binary patch literal 7220 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 M>iTMRD)#UH2Y(~e@c;k- literal 7120 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
    v XCbw18<;k4?i-f9@reYP)GW@>)Fr%fm diff --git a/testdata/snapshots/darwin/tree_select-dark.png b/testdata/snapshots/darwin/tree_select-dark.png index aba3b730a1dd4e6da108d863dba1ae70829c03b0..d3ebe83f81d1bf481efac81ac57268a45fee6b27 100644 GIT binary patch literal 4506 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 e_J1YV3Gx&T$mMMnz?*TAS-zx@{?5`uXE literal 4482 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 delta 3823 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 1 { panic("mujica: single ToggleGroup holds more than one value") @@ -67,7 +64,8 @@ func ToggleGroup[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], on := slices.Contains(*value, it.Value) pressed := on b := ui.ToggleBase(c, &pressed).Disabled(it.Disabled) - if b.Changed() && toggleGroupApply(value, it.Value, pressed, opts) { + // The base took input before Disabled applied: drop a disabled item's press. + if b.Changed() && !it.Disabled && !opts.Disabled && toggleGroupApply(value, it.Value, pressed, opts) { changed = true on = pressed } diff --git a/toggle_test.go b/toggle_test.go index 529a3b8..7186775 100644 --- a/toggle_test.go +++ b/toggle_test.go @@ -42,20 +42,6 @@ func TestToggleIconOnlyNamed(t *testing.T) { } } -func TestToggleDisabled(t *testing.T) { - on := false - tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - Toggle(c, &on, "Italic", ToggleOptions{Disabled: true}) - }, 300, 120) - _ = tt.Click("Italic") - tt.Key(0, ui.KeyTab) - tt.Key(0, ui.KeySpace) - if on { - t.Fatal("disabled toggle changed") - } -} - func TestToggleNeedsName(t *testing.T) { defer func() { if recover() == nil { diff --git a/tree_select.go b/tree_select.go index 1bc09ef..6194f48 100644 --- a/tree_select.go +++ b/tree_select.go @@ -59,9 +59,7 @@ type treeSelectRow struct { parent string } -// TreeSelect chooses one node, or several with Multiple, from a tree in a -// popup with search. Up and Down move, Right expands, Left collapses or -// goes to the parent, Space or Enter chooses; Escape closes. +// TreeSelect chooses one node, or several with Multiple, from a searchable tree in a popup. func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts TreeSelectOptions) ChoiceResult { if !opts.Multiple && len(*selected) > 1 { panic("mujica: single TreeSelect holds more than one ID") @@ -73,6 +71,9 @@ func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts tr.Label(opts.Label) } st := *ui.Local(tr, treeSelectKey{}, func() *treeSelectState { return &treeSelectState{expanded: map[string]bool{}} }) + if opts.Disabled { + st.open = false + } if tr.Clicked() || (!st.open && tr.Shortcut(0, ui.KeyDown)) { st.open, st.query = !st.open, "" } @@ -104,10 +105,14 @@ func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts selectNote(c, msg(c, "tree_select.no_results")) return } + shown := make(map[string]bool, len(rows)) + for _, r := range rows { + shown[r.node.ID] = true + } ui.Scroll(c).MaxHeight(300).Padding(4).Children(func() { ui.Column(c).FocusGroup(ui.Vertical).Children(func() { for i, r := range rows { - treeSelectRowView(c, st, selected, r, i == 0, opts) + treeSelectRowView(c, st, selected, r, i == 0, shown, opts) } }) }) @@ -127,7 +132,7 @@ func treeSelectWalk(nodes []TreeSelectNode, fn func(n *TreeSelectNode)) { } } -// treeSelectVisible lists the rows to show: expanded ones, or while searching the matches and their ancestors. +// treeSelectVisible lists the rows to show: expanded ones, or matches and their ancestors. func treeSelectVisible(nodes []TreeSelectNode, st *treeSelectState) []treeSelectRow { q := strings.ToLower(strings.TrimSpace(st.query)) var rows []treeSelectRow @@ -155,9 +160,9 @@ func treeSelectVisible(nodes []TreeSelectNode, st *treeSelectState) []treeSelect return rows } -// treeSelectCheck returns a node's check state under the rule. +// treeSelectCheck returns a node's check state: its own in single mode, else by the rule. func treeSelectCheck(n *TreeSelectNode, selected []string, opts TreeSelectOptions) internal.CheckState { - if opts.Rule == TreeSelectIndependent || len(n.Children) == 0 { + if !opts.Multiple || opts.Rule == TreeSelectIndependent || len(n.Children) == 0 { if slices.Contains(selected, n.ID) { return internal.Checked } @@ -201,14 +206,38 @@ func treeSelectToggle(n *TreeSelectNode, selected []string, on bool, opts TreeSe return selected } -func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r treeSelectRow, first bool, opts TreeSelectOptions) { +// treeSelectFirstChild returns the first child of n that shows and is enabled, or "". +func treeSelectFirstChild(n *TreeSelectNode, shown map[string]bool) string { + for _, ch := range n.Children { + if shown[ch.ID] && !ch.Disabled { + return ch.ID + } + } + return "" +} + +// treeSelectDescribe says what MyGo cannot set on custom rows (ui/element.go:282). +func treeSelectDescribe(c *ui.Context, b *ui.Element, branch, open bool, state internal.CheckState) { + var parts []string + if branch { + parts = append(parts, msg(c, map[bool]string{true: "tree_select.expanded", false: "tree_select.collapsed"}[open])) + } + if state == internal.Mixed { + parts = append(parts, msg(c, "tree_select.mixed")) + } + if len(parts) > 0 { + b.Description(strings.Join(parts, ", ")) + } +} + +func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r treeSelectRow, first bool, shown map[string]bool, opts TreeSelectOptions) { k := Tokens(c) d := Density(c) n := r.node branch := len(n.Children) > 0 open := branch && (st.expanded[n.ID] || strings.TrimSpace(st.query) != "") ui.Row(c).Key(n.ID).AlignItems(ui.Center).Gap(2).Padding(0, 0, 0, float32(r.depth)*18).Children(func() { - arrow := ui.Box(c).Size(18, 18).Shrink(0).Radius(theme.ControlRadius) + arrow := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0).Radius(theme.ControlRadius) if branch { arrow.Role(ui.RoleButton).Label(fmt.Sprintf(msg(c, map[bool]string{true: "tree_select.collapse", false: "tree_select.expand"}[open]), n.Label)) if arrow.Clicked() { @@ -218,6 +247,8 @@ func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r arrow.Background(k.SurfaceHover) } arrow.Draw(func(p *ui.Painter, rc ui.Rect) { + in := (rc.W - 16) / 2 + rc = ui.Rect{X: rc.X + in, Y: rc.Y + in, W: 16, H: 16} internal.Mark(p, rc, map[bool]string{true: "chevron-down", false: "chevron-right"}[open], k.TextMuted) }) } @@ -230,20 +261,28 @@ func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r *selected = treeSelectToggle(n, *selected, on, opts) } } else { - b = ui.ButtonBase(c).Disabled(n.Disabled).MinHeight(d.ControlHeight()-4).Padding(0, d.Unit()*2). - Justify(ui.Start).Radius(theme.ControlRadius).Grow(1) + chosen := state == internal.Checked + ui.Row(c).Grow(1).Disabled(n.Disabled).Children(func() { + b = ui.ButtonBase(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2). + Justify(ui.Start).Radius(theme.ControlRadius).Grow(1) + }) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Clicked() { *selected, st.open = []string{n.ID}, false } + b.Role(ui.RoleTreeItem).Selected(chosen).Background(ui.Transparent).TextColor(k.Text) switch { - case state == internal.Checked: + case chosen: b.Background(k.Selection).TextColor(k.AccentText) case b.Hovered(): b.Background(k.SurfaceHover) } - b.Role(ui.RoleTreeItem).Children(func() { ui.Text(c, n.Label).SingleLine() }) + b.Children(func() { + ui.Text(c, n.Label).SingleLine().Grow(1) + selectCheck(c, chosen) + }) } + treeSelectDescribe(c, b, branch, open, state) if st.focus == n.ID || first && st.focus == "\x00" { b.Focus() st.focus = "" @@ -253,7 +292,7 @@ func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r case right && branch && !open: st.expanded[n.ID] = true case right && branch: - st.focus = n.Children[0].ID + st.focus = treeSelectFirstChild(n, shown) case left && open && st.expanded[n.ID]: st.expanded[n.ID] = false case left && r.parent != "":