From 5ee3ed2f16bae27038bf6038b3eadc3e45987d2b 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 14:18:02 -0400 Subject: [PATCH] WIP essentials 6 --- bulk_action_bar.go | 2 +- choice_chips.go | 4 +- currency_input.go | 146 ++++++++++++++++++ currency_input_test.go | 120 ++++++++++++++ messages_essentials.go | 3 + scrub_input.go | 75 +++++++++ scrub_input_test.go | 82 ++++++++++ .../snapshots/darwin/currency_input-dark.png | Bin 0 -> 5372 bytes .../snapshots/darwin/currency_input-light.png | Bin 0 -> 5100 bytes .../snapshots/darwin/scrub_input-dark.png | Bin 0 -> 4370 bytes .../snapshots/darwin/scrub_input-light.png | Bin 0 -> 4192 bytes 11 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 currency_input.go create mode 100644 currency_input_test.go create mode 100644 scrub_input.go create mode 100644 scrub_input_test.go create mode 100644 testdata/snapshots/darwin/currency_input-dark.png create mode 100644 testdata/snapshots/darwin/currency_input-light.png create mode 100644 testdata/snapshots/darwin/scrub_input-dark.png create mode 100644 testdata/snapshots/darwin/scrub_input-light.png diff --git a/bulk_action_bar.go b/bulk_action_bar.go index ad4e0d7..0ca0e62 100644 --- a/bulk_action_bar.go +++ b/bulk_action_bar.go @@ -26,7 +26,7 @@ func BulkActionBar(c *ui.Context, selected int, opts BulkActionBarOptions, actio k := Tokens(c) u := Density(c).Unit() count := fmt.Sprintf(msg(c, "bulk_action_bar.selected"), selected) - bar := ui.Row(c).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4).MinHeight(Density(c).ControlHeight()+4*u). + bar := ui.Row(c).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4).MinHeight(Density(c).ControlHeight() + 4*u). Role(ui.RoleToolbar).Label(count).Disabled(opts.Disabled) overlayStyle(c, bar, overlayEnter(c, bar)) said := ui.Local(bar, "bulk_action_bar.said", func() int { return 0 }) diff --git a/choice_chips.go b/choice_chips.go index c979143..ac94e76 100644 --- a/choice_chips.go +++ b/choice_chips.go @@ -30,7 +30,7 @@ func ChoiceChips[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], } k := Tokens(c) d := Density(c) - g := ui.Row(c).Wrap().Gap(d.Unit()*2).Disabled(opts.Disabled).Role(ui.RoleGroup).FocusGroup(ui.Horizontal) + g := ui.Row(c).Wrap().Gap(d.Unit() * 2).Disabled(opts.Disabled).Role(ui.RoleGroup).FocusGroup(ui.Horizontal) if opts.Label != "" { g.Label(opts.Label) } @@ -54,7 +54,7 @@ func ChoiceChips[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], } func choiceChipLook[T comparable](c *ui.Context, k theme.Tokens, d theme.Density, b *ui.Element, on bool, it ToggleItem[T]) { - b.MinHeight(max(theme.MinHitSize, d.ControlHeight()-4)).MinWidth(theme.MinHitSize).Padding(0, d.Unit()*3).Gap(d.Unit()*1.5). + b.MinHeight(max(theme.MinHitSize, d.ControlHeight()-4)).MinWidth(theme.MinHitSize).Padding(0, d.Unit()*3).Gap(d.Unit() * 1.5). Radius(theme.PopupRadius) internal.Ring(b, k.Focus, theme.PopupRadius) bg, fg, line := k.Surface, k.Text, k.ControlBorder diff --git a/currency_input.go b/currency_input.go new file mode 100644 index 0000000..9b8f1cf --- /dev/null +++ b/currency_input.go @@ -0,0 +1,146 @@ +package mujica + +import ( + "fmt" + "log/slog" + "math" + "strconv" + "strings" + + "github.com/egoist/mygo/ui" +) + +// CurrencyInputOptions configure a CurrencyInput. +type CurrencyInputOptions struct { + // Symbol goes before the amount, as "$" or "¥"; empty shows none. + Symbol string + // Decimals is the minor unit digits; zero is 2. NoFraction makes it 0, as for JPY. + Decimals int + NoFraction bool + Label string + Disabled bool + // Error marks the value invalid and shows the message below. + Error string +} + +// currencyInputDigits resolves the decimals of opts. +func currencyInputDigits(opts CurrencyInputOptions) int { + switch { + case opts.Decimals < 0 || opts.Decimals > 6: + panic(fmt.Sprintf("mujica: CurrencyInput Decimals %d outside 0..6", opts.Decimals)) + case opts.NoFraction && opts.Decimals != 0: + panic("mujica: CurrencyInput NoFraction with Decimals") + case opts.NoFraction: + return 0 + case opts.Decimals == 0: + return 2 + } + return opts.Decimals +} + +// CurrencyFormat writes minor units as an amount with thousands separators. +func CurrencyFormat(minor int64, decimals int, group bool) string { + neg := minor < 0 + u := uint64(minor) + if neg { + u = -u + } + p := uint64(math.Pow10(decimals)) + whole := strconv.FormatUint(u/p, 10) + if group { + var b strings.Builder + for i, r := range whole { + if i > 0 && (len(whole)-i)%3 == 0 { + b.WriteByte(',') + } + b.WriteRune(r) + } + whole = b.String() + } + s := whole + if decimals > 0 { + s += "." + fmt.Sprintf("%0*d", decimals, u%p) + } + if neg { + s = "-" + s + } + return s +} + +// currencyInputParse reads an amount into minor units; reason is a message key when it fails. +func currencyInputParse(text string, decimals int) (minor int64, reason string) { + t := strings.NewReplacer(",", "", " ", "", " ", "").Replace(strings.TrimSpace(text)) + neg := strings.HasPrefix(t, "-") + t = strings.TrimPrefix(t, "-") + whole, frac, dot := strings.Cut(t, ".") + if whole == "" && frac == "" || strings.Trim(whole+frac, "0123456789") != "" || dot && strings.Contains(frac, ".") { + return 0, "currency_input.invalid" + } + if len(frac) > decimals { + return 0, "currency_input.precision" + } + frac += strings.Repeat("0", decimals-len(frac)) + v, err := strconv.ParseInt(whole+frac, 10, 64) + if err != nil { + return 0, "currency_input.invalid" + } + if neg { + v = -v + } + return v, "" +} + +// CurrencyInput edits *value, an amount in minor units (cents); it groups thousands while not focused. +func CurrencyInput(c *ui.Context, value *int64, opts CurrencyInputOptions) InputResult { + if value == nil { + panic("mujica: CurrencyInput needs a value") + } + dec := currencyInputDigits(opts) + k := Tokens(c) + var r InputResult + look := &textInputLook{disabled: opts.Disabled, err: opts.Error} + r.Element, r.Input = textInputFrame(c, look, func(frame *ui.Element) *ui.Element { + if opts.Symbol != "" { + ui.Text(c, opts.Symbol).TextColor(k.TextMuted).SingleLine() + } + text := ui.Local(frame, "text", func() string { return CurrencyFormat(*value, dec, false) }) + bad := ui.Local(frame, "bad", func() string { return "" }) + seen := ui.Local(frame, "seen", func() int64 { return *value }) + was := ui.Local(frame, "was", func() bool { return false }) + if *seen != *value { + *bad = "" + } + // Grouped while resting; plain digits once focused, for editing. + focused := frame.FocusWithin() + switch { + case *bad != "": + case !focused: + *text = CurrencyFormat(*value, dec, true) + case !*was || *seen != *value: + *text = CurrencyFormat(*value, dec, false) + } + *was = focused + in := textInputEditor(c, frame, text, look.disabled, ui.TextInputBase).Grow(1).MinWidth(Density(c).Unit() * 16).Label(opts.Label) + if in.Changed() && !look.disabled { + v, why := currencyInputParse(*text, dec) + *bad = "" + switch { + case why == "currency_input.precision": + *bad = fmt.Sprintf(msg(c, why), dec) + case why != "": + *bad = msg(c, why) + case v != *value: + *value, r.changed = v, true + } + } + *seen = *value + if look.err == "" { + look.err = *bad + } + return in + }, nil) + if r.changed { + slog.Debug("mujica: currency_input change", "minor", *value) + } + return r +} diff --git a/currency_input_test.go b/currency_input_test.go new file mode 100644 index 0000000..cef2209 --- /dev/null +++ b/currency_input_test.go @@ -0,0 +1,120 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestCurrencyFormatAndParse(t *testing.T) { + for _, tc := range []struct { + minor int64 + dec int + group bool + want string + }{ + {123450, 2, true, "1,234.50"}, {123450, 2, false, "1234.50"}, {-5, 2, true, "-0.05"}, + {1234567, 0, true, "1,234,567"}, {0, 3, true, "0.000"}, {100000000, 2, true, "1,000,000.00"}, + } { + if got := CurrencyFormat(tc.minor, tc.dec, tc.group); got != tc.want { + t.Errorf("CurrencyFormat(%d, %d) = %q, want %q", tc.minor, tc.dec, got, tc.want) + } + } + for text, want := range map[string]int64{"1,234.5": 123450, "0.07": 7, "-12": -1200, " 3 ": 300, "1.": 100} { + if v, why := currencyInputParse(text, 2); v != want || why != "" { + t.Errorf("parse %q = %d %q, want %d", text, v, why, want) + } + } + for text, want := range map[string]string{"1.234": "currency_input.precision", "abc": "currency_input.invalid", "": "currency_input.invalid", "1.2.3": "currency_input.invalid", "99999999999999999999": "currency_input.invalid"} { + if _, why := currencyInputParse(text, 2); why != want { + t.Errorf("parse %q: %q, want %q", text, why, want) + } + } +} + +// fieldText copies the focused field's text through the clipboard. +func fieldText(tt *ui.Tester) string { + tt.Command("selectAll") + tt.Command("copy") + return tt.Clipboard() +} + +func TestCurrencyInputTypingGroupingAndErrors(t *testing.T) { + v := int64(123450) + dis := false + n := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if CurrencyInput(c, &v, CurrencyInputOptions{Symbol: "$", Label: "Price", Disabled: dis}).Changed() { + n++ + } + Button(c, "Elsewhere", ButtonOptions{}) + }, 500, 200) + if !tt.HasText("$") { + t.Fatalf("no symbol: %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + if got := fieldText(tt); got != "1234.50" { + t.Fatalf("focused text %q, want ungrouped", got) + } + tt.Command("selectAll") + tt.Type("2500.255") + if v != 123450 || !tt.HasText("Use at most 2 decimal places") { + t.Fatalf("too precise: %d %q", v, tt.Texts()) + } + tt.Command("selectAll") + tt.Type("2500.25") + if v != 250025 || n != 1 || tt.HasText("decimal places") { + t.Fatalf("typed: %d (%d)", v, n) + } + tt.Key(0, ui.KeyTab) + tt.Key(ui.Shift, ui.KeyTab) + if got := fieldText(tt); got != "2500.25" { + t.Fatalf("refocused text %q", got) + } + dis = true + tt.Frame() + _ = tt.Click("Price") + tt.Type("9") + if v != 250025 { + t.Fatalf("disabled edit: %d", v) + } +} + +func TestCurrencyInputNoFractionAndRejects(t *testing.T) { + v := int64(1500) + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + CurrencyInput(c, &v, CurrencyInputOptions{Symbol: "¥", NoFraction: true, Label: "Amount"}) + }, 300, 100) + tt.Key(0, ui.KeyTab) + if got := fieldText(tt); got != "1500" { + t.Fatalf("yen %q", got) + } + tt.Command("selectAll") + tt.Type("12.5") + if v != 1500 || !tt.HasText("Use at most 0 decimal places") { + t.Fatalf("fraction accepted: %d", v) + } + for _, o := range []CurrencyInputOptions{{Decimals: -1}, {Decimals: 7}, {Decimals: 2, NoFraction: true}} { + func() { + defer func() { + if recover() == nil { + t.Errorf("%+v: no panic", o) + } + }() + ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); CurrencyInput(c, &v, o) }, 200, 100) + }() + } +} + +func TestCurrencyInputSnapshot(t *testing.T) { + a, b, d := int64(1234550), int64(98000), int64(0) + snapshot(t, "currency_input", 520, 80, func(c *ui.Context) { + ui.Row(c).Padding(16).Gap(12).Children(func() { + CurrencyInput(c, &a, CurrencyInputOptions{Symbol: "$"}).Element.Width(150) + CurrencyInput(c, &b, CurrencyInputOptions{Symbol: "¥", NoFraction: true}).Element.Width(150) + CurrencyInput(c, &d, CurrencyInputOptions{Symbol: "€", Disabled: true}).Element.Width(150) + }) + }) +} diff --git a/messages_essentials.go b/messages_essentials.go index f4a4f68..fcb62b3 100644 --- a/messages_essentials.go +++ b/messages_essentials.go @@ -18,6 +18,9 @@ func init() { "knob.label": {"Knob", "旋钮"}, + "currency_input.invalid": {"Enter an amount, such as 1,234.50", "请输入金额,例如 1,234.50"}, + "currency_input.precision": {"Use at most %d decimal places", "最多保留 %d 位小数"}, + "qr_code.label": {"QR code", "二维码"}, "qr_code.too_long": {"Text too long for a QR code", "文字过长,无法生成二维码"}, }) diff --git a/scrub_input.go b/scrub_input.go new file mode 100644 index 0000000..387bc67 --- /dev/null +++ b/scrub_input.go @@ -0,0 +1,75 @@ +package mujica + +import ( + "log/slog" + "math" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// ScrubInputOptions configure a ScrubInput; the range and step rules are NumberInput's. +type ScrubInputOptions struct { + Min, Max float64 + Step float64 + Precision int + Disabled bool +} + +// scrubInputPixels is how far a drag moves for one step. +const scrubInputPixels float32 = 4 + +// ScrubInput edits *value by dragging its label sideways, one Step per 4 DIPs, or by typing in its field. +func ScrubInput(c *ui.Context, label string, value *float64, opts ScrubInputOptions) InputResult { + if label == "" || value == nil { + panic("mujica: ScrubInput needs a label and a value") + } + nopts := NumberInputOptions{Min: opts.Min, Max: opts.Max, Step: opts.Step, Precision: opts.Precision, Label: label, Disabled: opts.Disabled} + s := numberInputResolve(nopts) + k := Tokens(c) + var r InputResult + row := ui.Row(c).AlignItems(ui.Center).Gap(Density(c).Unit() * 2).Disabled(opts.Disabled) + row.Children(func() { + h := ui.Box(c).Shrink(0).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Center().Cursor(ui.CursorResizeEW).Role(ui.RoleNone) + drag := ui.Local(h, "scrub_input.drag", func() float64 { return math.NaN() }) + dx, _, held := h.Dragged() + scrubbed := false + switch { + case !held || h.IsDisabled(): + *drag = math.NaN() + case math.IsNaN(*drag): + *drag = 0 + fallthrough + default: + start := *value + *drag += float64(dx) + if n := int(*drag / float64(scrubInputPixels)); n != 0 { + *drag -= float64(n) * float64(scrubInputPixels) + // One grid step at a time, so a long drag stops at the bound. + v := start + for range max(n, -n) { + v = s.stepBy(v, max(-1, min(1, n))) + } + if v != *value { + *value, scrubbed = v, true + } + } + } + fg := k.TextMuted + if held || h.Hovered() { + fg = k.AccentText + } + h.Children(func() { + ui.Text(c, label).TextColor(fg).Underline().DecorationColor(k.Border).SingleLine() + }) + r = NumberInput(c, value, nopts) + r.Element.Width(Density(c).Unit() * 36) + r.changed = r.changed || scrubbed + if scrubbed { + slog.Debug("mujica: scrub_input scrubbed", "label", label, "value", *value) + } + }) + r.Element = row + return r +} diff --git a/scrub_input_test.go b/scrub_input_test.go new file mode 100644 index 0000000..2e9502b --- /dev/null +++ b/scrub_input_test.go @@ -0,0 +1,82 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func scrubTester(v *float64, opts *ScrubInputOptions, n *int) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Row(c).Padding(20).Children(func() { + if ScrubInput(c, "Opacity", v, *opts).Changed() { + *n++ + } + }) + }, 500, 200) +} + +func TestScrubInputDragAndRange(t *testing.T) { + v := 0.5 + opts := ScrubInputOptions{Min: 0, Max: 1, Step: 0.1} + n := 0 + tt := scrubTester(&v, &opts, &n) + r, _ := tt.Find("Opacity") + x, y := r.X+r.W/2, r.Y+r.H/2 + tt.Press(x, y) + tt.Move(x+6, y) // one step, 2 DIPs carried + tt.Move(x+10, y) + tt.Release(x+10, y) + if v != 0.7 { + t.Fatalf("10 DIPs right: %v", v) + } + tt.Press(x, y) + tt.Move(x+400, y) + tt.Release(x+400, y) + if v != 1 { + t.Fatalf("past the max: %v", v) + } + tt.Press(x, y) + tt.Move(x-8, y) + tt.Release(x-8, y) + if v != 0.8 || n != 4 { + t.Fatalf("8 DIPs left: %v (%d)", v, n) + } +} + +func TestScrubInputTypingAndDisabled(t *testing.T) { + v := 2.0 + opts := ScrubInputOptions{Step: 1} + n := 0 + tt := scrubTester(&v, &opts, &n) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyUp) + if v != 3 { + t.Fatalf("Up in the field: %v", v) + } + tt.Command("selectAll") + tt.Type("12") + if v != 12 { + t.Fatalf("typed: %v", v) + } + opts.Disabled = true + tt.Frame() + r, _ := tt.Find("Opacity") + tt.Press(r.X+r.W/2, r.Y+r.H/2) + tt.Move(r.X+r.W/2+40, r.Y+r.H/2) + tt.Release(r.X+r.W/2+40, r.Y+r.H/2) + if v != 12 { + t.Fatalf("disabled scrub: %v", v) + } +} + +func TestScrubInputSnapshot(t *testing.T) { + a, b := 0.6, 24.0 + snapshot(t, "scrub_input", 520, 80, func(c *ui.Context) { + ui.Row(c).Padding(16).Gap(16).Children(func() { + ScrubInput(c, "Opacity", &a, ScrubInputOptions{Max: 1, Step: 0.1}) + ScrubInput(c, "Size", &b, ScrubInputOptions{Disabled: true}) + }) + }) +} diff --git a/testdata/snapshots/darwin/currency_input-dark.png b/testdata/snapshots/darwin/currency_input-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..07aaac8ff586ba209b4211d5d53da9a534768c60 GIT binary patch literal 5372 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 XeJ$fkKWZ79Y63t?T8b6&7Qz1k;>_5o literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/currency_input-light.png b/testdata/snapshots/darwin/currency_input-light.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a5df3afa0bd1e983cbfa697405c734a8ee768d GIT binary patch literal 5100 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 literal 0 HcmV?d00001