diff --git a/base_disabled_test.go b/base_disabled_test.go new file mode 100644 index 0000000..8356e7a --- /dev/null +++ b/base_disabled_test.go @@ -0,0 +1,56 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +// TestBasesBlockedByDisabledWrapper flips a focused MyGo base's wrapper to Disabled in the frame Space arrives. +func TestBasesBlockedByDisabledWrapper(t *testing.T) { + for name, base := range map[string]func(c *ui.Context, on *bool) *ui.Element{ + "CheckboxBase": func(c *ui.Context, on *bool) *ui.Element { return ui.CheckboxBase(c, on) }, + "RadioBase": func(c *ui.Context, on *bool) *ui.Element { return ui.RadioBase(c, on, true) }, + "SwitchBase": func(c *ui.Context, on *bool) *ui.Element { return ui.SwitchBase(c, on) }, + "ToggleBase": func(c *ui.Context, on *bool) *ui.Element { return ui.ToggleBase(c, on) }, + } { + for _, disable := range []bool{false, true} { + on, off := false, false + tt := ui.NewTester(func(c *ui.Context) { + ui.Row(c).Disabled(off).Children(func() { + base(c, &on).Label("Target").Size(40, 20) + }) + }, 200, 100) + tt.Key(0, ui.KeyTab) + if !tt.Focused("Target") { + t.Fatalf("%s: not focused", name) + } + off = disable + tt.Key(0, ui.KeySpace) + tt.Frame() + if on == disable { + t.Errorf("%s: disabled=%v, Space left the value %v", name, disable, on) + } + } + } +} + +// TestComboboxBaseIgnoresDisabledWrapper shows why Combobox hands its base a copy: a focused one still takes typing. +func TestComboboxBaseIgnoresDisabledWrapper(t *testing.T) { + q, off := "", false + tt := ui.NewTester(func(c *ui.Context) { + ui.Row(c).Disabled(off).Children(func() { + ui.ComboboxBase(c, &q).Input.Label("Target").Width(120) + }) + }, 200, 100) + tt.Key(0, ui.KeyTab) + if !tt.Focused("Target") { + t.Fatal("not focused") + } + off = true + tt.Type("a") + tt.Frame() + if q != "a" { + t.Fatalf("typing left %q; the wrapper now blocks it and Combobox can drop its copy", q) + } +} diff --git a/combobox.go b/combobox.go index 934e46c..01a9d8c 100644 --- a/combobox.go +++ b/combobox.go @@ -55,8 +55,14 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio refocus := ui.Local(f, comboboxKey{"refocus"}, func() bool { return false }) var p *ui.ComboboxParts f.Children(func() { + // A focused ComboboxBase takes typing under a disabled wrapper, so it edits a copy. + target := query + if opts.Disabled { + cp := *query + target = &cp + } ui.Row(c).Key(*gen).Grow(1).Children(func() { - p = ui.ComboboxBase(c, choiceTarget(query, opts.Disabled)) + p = ui.ComboboxBase(c, target) }) if *refocus { p.Input.Focus() diff --git a/multi_select.go b/multi_select.go index 188c76e..7bd4af6 100644 --- a/multi_select.go +++ b/multi_select.go @@ -180,9 +180,9 @@ func multiSelectPopup[T comparable](c *ui.Context, k theme.Tokens, values *[]T, func multiSelectCheck(c *ui.Context, k theme.Tokens, on *bool, st internal.CheckState, label string, disabled bool) *ui.Element { d := Density(c) var cb *ui.Element - // A disabled wrapper and a copy block the base, which toggles as it is built. + // A disabled wrapper blocks 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). + cb = ui.CheckboxBase(c, on).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2). Gap(d.Unit() * 2).AlignItems(ui.Center).Radius(theme.ControlRadius).Grow(1) }) if st == internal.Mixed { diff --git a/popup_menu.go b/popup_menu.go index 184d6bd..24650cc 100644 --- a/popup_menu.go +++ b/popup_menu.go @@ -56,14 +56,9 @@ func (m *PopupMenu) Item(label string, opts PopupMenuItemOptions) bool { // Check adds an item toggling *checked and reports whether it changed. func (m *PopupMenu) Check(label string, checked *bool, opts PopupMenuItemOptions) bool { - // The base toggles as it is built, so a disabled wrapper and a copy come first. - value := checked - if opts.Disabled { - cp := *checked - value = &cp - } + // The base toggles as it is built, so a disabled wrapper comes first. var base *ui.Element - ui.Column(m.c).Shrink(0).Disabled(opts.Disabled).Children(func() { base = ui.CheckboxBase(m.c, value) }) + ui.Column(m.c).Shrink(0).Disabled(opts.Disabled).Children(func() { base = ui.CheckboxBase(m.c, checked) }) it := m.row(base, opts.Disabled, false) m.content(it, label, *checked, false) if opts.Disabled || !it.Changed() { diff --git a/switch.go b/switch.go index e2f2d22..40531cd 100644 --- a/switch.go +++ b/switch.go @@ -30,9 +30,9 @@ func Switch(c *ui.Context, on *bool, label string, opts SwitchOptions) *ui.Eleme d := Density(c) blocked := opts.Disabled || opts.Loading var sw *ui.Element - // Disabled on a wrapper, and a copy, block the base before it takes input. + // Disabled on a wrapper blocks 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). + sw = ui.SwitchBase(c, on).Gap(d.Unit() * 2).AlignItems(ui.Center). MinHeight(d.ControlHeight()).FocusRing(false) }) if opts.Description != "" { @@ -82,15 +82,6 @@ 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/toggle.go b/toggle.go index 29fa4f0..4bd8846 100644 --- a/toggle.go +++ b/toggle.go @@ -25,7 +25,7 @@ func Toggle(c *ui.Context, on *bool, label string, opts ToggleOptions) *ui.Eleme } var b *ui.Element ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() { - b = ui.ToggleBase(c, choiceTarget(on, opts.Disabled)) + b = ui.ToggleBase(c, on) }) if b.Changed() { slog.Debug("mujica: toggle changed", "label", label+opts.Label, "on", *on)