WIP essentials 9

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 14:24:18 -04:00
1 parent 7f5b67fc2c
commit d9e210ffa4
6 files changed
+234 -2

No files matched your search

+4 -2
View File
@@ -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)
}
+5
View File
@@ -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", "文字过长,无法生成二维码"},
})
+153
View File
@@ -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
}
+72
View File
@@ -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}) })
})
})
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB