Files
MujicaUI/pin_input.go
T

166 lines
4.4 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"strings"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"mujicaui/theme"
)
// PinInputOptions configure a PinInput. Length is 6 when zero.
type PinInputOptions struct {
Length int
// Label names the input; empty uses the built-in "Verification code".
Label string
Disabled bool
// Invalid draws the cells in the danger color.
Invalid bool
}
// PinInputParts are the parts of a PinInput.
type PinInputParts struct {
Element *ui.Element
changed, completed bool
}
// Changed reports whether the user changed the code this frame.
func (p PinInputParts) Changed() bool { return p.changed }
// Completed reports whether the user filled the last cell this frame.
func (p PinInputParts) Completed() bool { return p.completed }
// pinInputEvent is input taken between frames, applied in the next one.
type pinInputEvent struct {
text string
backspace bool
paste bool
}
type pinInputState struct {
queue []pinInputEvent
compose string
}
// PinInput edits a fixed-length code of digits, one per cell, keeping leading zeros.
// Typing fills the next cell, Backspace clears the last, and paste fills them all.
func PinInput(c *ui.Context, value *string, opts PinInputOptions) PinInputParts {
n := opts.Length
if n == 0 {
n = 6
}
if n < 0 || len(*value) > n || strings.Trim(*value, "0123456789") != "" {
panic(fmt.Sprintf("mujica: PinInput value %q does not fit %d digits", *value, n))
}
k := Tokens(c)
d := Density(c)
label := opts.Label
if label == "" {
label = msg(c, "pin_input.label")
}
parts := PinInputParts{}
e := ui.Row(c).Gap(d.Unit() * 2).Shrink(0).Role(ui.RoleTextField).Label(label).Disabled(opts.Disabled)
parts.Element = e
st := ui.Local(e, "pin", func() pinInputState { return pinInputState{} })
put := func(s string) {
var b strings.Builder
for _, r := range s {
if r >= '0' && r <= '9' {
b.WriteRune(r)
}
}
next := b.String()
if len(next) > n {
next = next[:n]
}
if next == *value {
return
}
was := len(*value)
*value = next
parts.changed = true
if len(next) == n && was < n {
parts.completed = true
slog.Debug("mujica: pin input completed", "length", n)
}
}
for _, ev := range st.queue {
switch {
case ev.paste:
put(c.ReadClipboard())
case ev.backspace && *value != "":
put((*value)[:len(*value)-1])
case ev.text != "":
put(*value + ev.text)
}
}
st.queue = st.queue[:0]
if opts.Disabled {
st.compose = ""
} else {
e.Focusable().FocusRing(false).HandleInput(func(ev ui.InputEvent) bool {
switch {
case ev.Kind == ui.InputText:
st.queue = append(st.queue, pinInputEvent{text: ev.Text})
st.compose = ""
case ev.Kind == ui.InputCompose:
st.compose = ev.Text
case ev.Kind == ui.InputCommand && ev.Text == "paste",
ev.Kind == ui.InputKeyDown && ev.Mods == ui.Cmd && ev.Key == ui.KeyV:
st.queue = append(st.queue, pinInputEvent{paste: true})
case ev.Kind == ui.InputKeyDown && ev.Mods == 0 && ev.Key == ui.KeyBackspace:
st.queue = append(st.queue, pinInputEvent{backspace: true})
default:
return false
}
return true
})
}
size := d.ControlHeight() + d.Unit()*2
active := min(len(*value), n-1)
focused := e.Focused()
compose := st.compose
e.Description(fmt.Sprintf(msg(c, "pin_input.filled"), len(*value), n))
e.TextCaret(ui.Rect{X: float32(active) * (size + d.Unit()*2), Y: 0, W: size, H: size})
e.Children(func() {
for i := range n {
cell := ui.Box(c).Size(size, size).Center().Radius(theme.ControlRadius).Background(k.Surface).Role(ui.RoleNone)
border := k.ControlBorder
switch {
case opts.Invalid:
border = k.Danger
case focused && i == active:
border = k.Focus
}
cell.Border(theme.BorderWidth, border)
ch := ""
if i < len(*value) {
ch = (*value)[i : i+1]
}
showCompose := focused && i == active && compose != ""
cell.Children(func() {
switch {
case showCompose:
ui.Text(c, compose).FontSize(theme.H3Size).Underline().SingleLine()
case ch != "":
ui.Text(c, ch).FontSize(theme.H3Size).FontFeatures("tnum").SingleLine()
}
})
caret := focused && i == active && len(*value) < n && !showCompose
cell.DrawOver(func(p *ui.Painter, r ui.Rect) {
if focused && i == active {
internal.FocusRing(p, r, theme.ControlRadius, k.Focus)
}
if caret {
h := r.H * 0.4
p.Fill(ui.Rect{X: r.X + r.W/2 - 0.5, Y: r.Y + (r.H-h)/2, W: 1, H: h}, k.Text, 0)
}
})
}
})
return parts
}