WIP essentials 9
This commit is contained in:
6 files changed
+234
-2
No files matched your search
+4
-2
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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", "文字过长,无法生成二维码"},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 |
Reference in new issue
Block a user