From d9e210ffa454bc6a552dbce72ebe9427f3dbd725 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:24:18 -0400 Subject: [PATCH] WIP essentials 9 --- mention_input.go | 6 +- messages_essentials.go | 5 + signature_pad.go | 153 ++++++++++++++++++ signature_pad_test.go | 72 +++++++++ .../snapshots/darwin/signature_pad-dark.png | Bin 0 -> 8129 bytes .../snapshots/darwin/signature_pad-light.png | Bin 0 -> 8001 bytes 6 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 signature_pad.go create mode 100644 signature_pad_test.go create mode 100644 testdata/snapshots/darwin/signature_pad-dark.png create mode 100644 testdata/snapshots/darwin/signature_pad-light.png diff --git a/mention_input.go b/mention_input.go index 88125c2..e286fb0 100644 --- a/mention_input.go +++ b/mention_input.go @@ -57,7 +57,7 @@ func MentionInput(c *ui.Context, value *string, mentions *[]Mention, opts Mentio var r InputResult look := &textInputLook{disabled: opts.Disabled, err: opts.Error} r.Element, r.Input = textInputFrame(c, look, func(frame *ui.Element) *ui.Element { - in := ui.Box(c).Grow(1).MinWidth(Density(c).Unit()*20).Focusable().Clip().Role(ui.RoleTextField).Label(opts.Label).Cursor(ui.CursorText) + in := ui.Box(c).Grow(1).MinWidth(Density(c).Unit() * 20).Focusable().Clip().Role(ui.RoleTextField).Label(opts.Label).Cursor(ui.CursorText) s := *ui.Local(in, "mention_input", func() *mentionInputState { return &mentionInputState{at: -1} }) if s.doc.text == nil || s.synced != *value || !slices.Equal(s.syncedMentions, *mentions) { // Set from outside: start over at the end. @@ -314,7 +314,9 @@ func mentionInputPopup(c *ui.Context, k theme.Tokens, s *mentionInputState, fram Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(m.Name).Cursor(ui.CursorPointer) if i == s.pick { it.Selected(true).Background(k.Selection).TextColor(k.AccentText) - it.Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y + 6, W: 2, H: r.H - 12}, k.AccentText, 0) }) + it.Draw(func(p *ui.Painter, r ui.Rect) { + p.Fill(ui.Rect{X: r.X, Y: r.Y + 6, W: 2, H: r.H - 12}, k.AccentText, 0) + }) } else if it.Hovered() { it.Background(k.SurfaceHover) } diff --git a/messages_essentials.go b/messages_essentials.go index fcb62b3..ad95e04 100644 --- a/messages_essentials.go +++ b/messages_essentials.go @@ -21,6 +21,11 @@ func init() { "currency_input.invalid": {"Enter an amount, such as 1,234.50", "请输入金额,例如 1,234.50"}, "currency_input.precision": {"Use at most %d decimal places", "最多保留 %d 位小数"}, + "signature_pad.label": {"Signature", "签名"}, + "signature_pad.hint": {"Draw with the pointer; Delete clears", "用鼠标书写;Delete 键清除"}, + "signature_pad.empty": {"Sign here", "请在此签名"}, + "signature_pad.clear": {"Clear", "清除"}, + "qr_code.label": {"QR code", "二维码"}, "qr_code.too_long": {"Text too long for a QR code", "文字过长,无法生成二维码"}, }) diff --git a/signature_pad.go b/signature_pad.go new file mode 100644 index 0000000..53352b8 --- /dev/null +++ b/signature_pad.go @@ -0,0 +1,153 @@ +package mujica + +import ( + "cmp" + "log/slog" + "math" + "strconv" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// SignaturePoint is a point of a stroke, in DIPs from the pad's top left. +type SignaturePoint struct{ X, Y float32 } + +// SignaturePadOptions configure a SignaturePad. +type SignaturePadOptions struct { + // Height is the drawing area's height; zero is 160. The pad fills its parent's width. + Height float32 + // Label names the pad; empty uses "Signature". + Label string + Disabled bool +} + +// SignaturePath writes strokes as SVG path data: "M x y L x y …" per stroke. +func SignaturePath(strokes [][]SignaturePoint) string { + var b strings.Builder + num := func(v float32) string { return strconv.FormatFloat(float64(v), 'f', -1, 32) } + for _, st := range strokes { + for i, p := range st { + if b.Len() > 0 { + b.WriteByte(' ') + } + if i == 0 { + b.WriteString("M ") + } else { + b.WriteString("L ") + } + b.WriteString(num(p.X) + " " + num(p.Y)) + } + } + return b.String() +} + +// signaturePadDrawing is the stroke in progress. +type signaturePadDrawing struct { + active, ended bool +} + +// SignaturePad records pointer strokes into *strokes; Changed reports a finished stroke or a clear. +func SignaturePad(c *ui.Context, strokes *[][]SignaturePoint, opts SignaturePadOptions) ChoiceResult { + if strokes == nil { + panic("mujica: SignaturePad needs strokes") + } + if !(opts.Height >= 0) || math.IsInf(float64(opts.Height), 1) { + panic("mujica: SignaturePad Height must be finite and >= 0") + } + k := Tokens(c) + u := Density(c).Unit() + name := cmp.Or(opts.Label, msg(c, "signature_pad.label")) + col := ui.Column(c).FillWidth().Gap(u * 2).Disabled(opts.Disabled) + disabled := col.IsDisabled() + changed := false + col.Children(func() { + pad := ui.Box(c).FillWidth().Height(cmp.Or(opts.Height, 160)).Focusable().Radius(theme.CardRadius).Clip(). + Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Cursor(ui.CursorCrosshair). + Role(ui.RoleImage).Label(name).Description(msg(c, "signature_pad.hint")) + internal.Ring(pad, k.Focus, theme.CardRadius) + d := ui.Local(pad, "signature_pad", func() *signaturePadDrawing { return &signaturePadDrawing{} }) + s := *d + pad.HandleInput(func(ev ui.InputEvent) bool { + if pad.IsDisabled() { + return false + } + pt := SignaturePoint{ev.X, ev.Y} + switch { + case ev.Kind == ui.InputPointerDown && ev.Button == 0: + *strokes = append(*strokes, []SignaturePoint{pt}) + s.active = true + pad.Focus() + case ev.Kind == ui.InputPointerMove && s.active && len(*strokes) > 0: + last := &(*strokes)[len(*strokes)-1] + if q := (*last)[len(*last)-1]; math.Hypot(float64(pt.X-q.X), float64(pt.Y-q.Y)) >= 1 { + *last = append(*last, pt) + } + case ev.Kind == ui.InputPointerUp && s.active: + s.active, s.ended = false, true + default: + return false + } + return true + }) + if s.ended { + s.ended, changed = false, true + slog.Debug("mujica: signature_pad stroke", "strokes", len(*strokes)) + } + clear := func() { + if len(*strokes) > 0 { + *strokes, changed = nil, true + slog.Debug("mujica: signature_pad clear") + } + } + if !disabled && (pad.Shortcut(0, ui.KeyDelete) || pad.Shortcut(0, ui.KeyBackspace)) { + clear() + } + ink := signaturePadClone(*strokes) + empty := len(ink) == 0 + size := FontSize(c, theme.CaptionSize) + pad.Draw(func(p *ui.Painter, r ui.Rect) { + line := r.Y + r.H - 28 + internal.Rule(p, r.X+16, r.X+r.W-16, line, k.Border) + p.Text(r.X+16, line+4, "×", size, k.TextMuted) + if empty { + w, _ := p.MeasureText(0, ui.Span{Text: msg(c, "signature_pad.empty"), Size: size}) + p.Text(r.X+(r.W-w)/2, r.Y+r.H/2-size, msg(c, "signature_pad.empty"), size, k.TextMuted) + } + for _, st := range ink { + var path ui.Path + for i, q := range st { + x, y := internal.Snap(p, r.X+q.X), internal.Snap(p, r.Y+q.Y) + if i == 0 { + path.MoveTo(x, y) + } else { + path.LineTo(x, y) + } + } + if len(st) == 1 { + path.Circle(internal.Snap(p, r.X+st[0].X), internal.Snap(p, r.Y+st[0].Y), 1) + } + p.StrokePath(&path, 2, k.Text) + } + }) + ui.Row(c).Children(func() { + if Button(c, msg(c, "signature_pad.clear"), ButtonOptions{Variant: Ghost, Icon: icons.Must("trash-2"), Disabled: empty}).Clicked() && !disabled { + clear() + } + }) + }) + return ChoiceResult{col, changed} +} + +// signaturePadClone copies strokes so painting never sees later edits. +func signaturePadClone(s [][]SignaturePoint) [][]SignaturePoint { + out := make([][]SignaturePoint, len(s)) + for i, st := range s { + out[i] = append([]SignaturePoint(nil), st...) + } + return out +} diff --git a/signature_pad_test.go b/signature_pad_test.go new file mode 100644 index 0000000..5aaf1c8 --- /dev/null +++ b/signature_pad_test.go @@ -0,0 +1,72 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestSignaturePath(t *testing.T) { + got := SignaturePath([][]SignaturePoint{{{1, 2}, {3.5, 4}}, {{10, 10}}}) + if want := "M 1 2 L 3.5 4 M 10 10"; got != want { + t.Fatalf("path %q, want %q", got, want) + } + if SignaturePath(nil) != "" { + t.Fatal("empty strokes made a path") + } +} + +func TestSignaturePadStrokesAndClear(t *testing.T) { + var strokes [][]SignaturePoint + dis := false + n := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Padding(20).Width(400).Children(func() { + if SignaturePad(c, &strokes, SignaturePadOptions{Disabled: dis}).Changed() { + n++ + } + }) + }, 500, 300) + tt.Press(50, 60) + tt.Move(80, 70) + tt.Move(80.5, 70) // under a DIP: not kept + tt.Move(120, 90) + tt.Release(120, 90) + if len(strokes) != 1 || len(strokes[0]) != 3 || strokes[0][0] != (SignaturePoint{30, 40}) || n != 1 { + t.Fatalf("stroke %+v, changes %d", strokes, n) + } + tt.Press(200, 60) + tt.Release(200, 60) + if len(strokes) != 2 || n != 2 { + t.Fatalf("dot: %+v", strokes) + } + tt.Key(0, ui.KeyDelete) // drawing focused the pad + if len(strokes) != 0 || n != 3 { + t.Fatalf("Delete kept %d strokes", len(strokes)) + } + tt.Press(50, 60) + tt.Release(50, 60) + if err := tt.Click("Clear"); err != nil || len(strokes) != 0 { + t.Fatalf("Clear button: %v %d", err, len(strokes)) + } + dis = true + tt.Frame() + tt.Press(50, 60) + tt.Move(90, 60) + tt.Release(90, 60) + if len(strokes) != 0 { + t.Fatal("disabled pad drew") + } +} + +func TestSignaturePadSnapshot(t *testing.T) { + strokes := [][]SignaturePoint{{{30, 90}, {50, 60}, {60, 95}, {80, 55}, {95, 100}, {120, 70}, {150, 85}}, {{170, 60}, {175, 100}}} + var empty [][]SignaturePoint + snapshot(t, "signature_pad", 560, 260, func(c *ui.Context) { + ui.Row(c).Padding(16).Gap(16).Children(func() { + ui.Column(c).Width(250).Children(func() { SignaturePad(c, &strokes, SignaturePadOptions{Height: 180}) }) + ui.Column(c).Width(250).Children(func() { SignaturePad(c, &empty, SignaturePadOptions{Height: 180}) }) + }) + }) +} diff --git a/testdata/snapshots/darwin/signature_pad-dark.png b/testdata/snapshots/darwin/signature_pad-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..aec256994e61a2a3910a0f0a94dc8c4d794970bd GIT binary patch literal 8129 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 PPo$!xu2?4jJn&xt6^sZC literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/signature_pad-light.png b/testdata/snapshots/darwin/signature_pad-light.png new file mode 100644 index 0000000000000000000000000000000000000000..ac20d182e9f308acc9f3e200ccb9b203db4a635d GIT binary patch literal 8001 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