seedhammer-v1-companion/gui/text/text.go
mineracks 9675c05ff1 emulator: real gui — lift upstream v1.3.0 gui/ + wire Platform
The Phase 2 scaffolding boot screen is replaced with the actual
upstream v1.3.0 gui package running in browser. cmd/emulator now
calls gui.NewApp(plat, version) + drives a.Frame() in a goroutine.

Lifts:
  gui/                         (top-level UI + state machine)
  gui/assets                   (icons, fonts, 9-patch images)
  gui/layout                   (constraint-based UI layout)
  gui/op                       (drawing op primitives)
  gui/saver                    (screensaver + idle timeout)
  gui/text                     (text shaping)
  gui/widget                   (button/menu/keyboard widgets)
All 11 .go files lifted verbatim from seedhammer/seedhammer @ v1.3.0
(commit 2f071c1d...), import paths rewritten seedhammer.com → mineracks.

browserPlatform now implements gui.Platform (12 methods):
  Events(deadline)   — drains v1.Event chan, maps to gui.ButtonEvent
                       (gui.Button enum order matches platform/v1.Button,
                        so the conversion is a direct uint cast)
  Wakeup()           — no-op (no sleep state in browser)
  PlateSizes()       — backup.SquarePlate, backup.LargePlate
  Engraver()         — nullEngraver stub (browser can't punch metal)
  EngraverParams()   — copy of mjolnir.Params {StrokeWidth: 38,
                       Millimeter: 126}. Inlined because tarm/serial
                       (mjolnir's USB dep) doesn't compile to js/wasm.
  CameraFrame(size)  — emits FrameEvent{Error: stubbed} so QR-scan
                       screens fall through cleanly; real handoff
                       lands when SeedSigner sim wires up (Phase 2.5)
  Now()              — time.Now()
  DisplaySize()      — 240×240
  Dirty(r)           — records rect, resets chunk cursor
  NextChunk()        — one-shot: returns full sub-image once per
                       Dirty cycle, then flushes the whole frame
                       buffer to JS via emulatorPaint()
  ScanQR()           — returns nil decodes (stub)
  Debug()            — false

The Engraver interface is satisfied by nullEngraver — Engrave() returns
"engraver not connected" so the GUI's engrave flow fails cleanly
instead of looking like it's working.

WASM grows 2.7MB → 8.0MB (gui package + btcd + crypto deps + fonts
all linked in). Acceptable for the v1 emulator one-time cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:53:06 +10:00

119 lines
2.0 KiB
Go

package text
import (
"image"
"unicode"
"unicode/utf8"
"golang.org/x/image/math/fixed"
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
)
type Line struct {
Text string
Width int
Dot image.Point
}
type Style struct {
Face *bitmap.Face
Alignment Alignment
LineHeight float32
LetterSpacing int
}
type Alignment int
const (
AlignStart Alignment = iota
AlignEnd
AlignCenter
)
func (l Style) Layout(maxWidth int, txt string) ([]Line, image.Point) {
var lines []Line
prevC := rune(-1)
adv := fixed.I(0)
wordAdv := fixed.I(0)
wordIdx := 0
maxAdv := 0
prev := 0
idx := 0
m := l.Face.Metrics()
asc, desc := m.Ascent, m.Descent
lheight := m.Height.Ceil()
if l.LineHeight != 0 {
lheight = int(float32(lheight) * l.LineHeight)
}
doty := asc.Ceil()
endLine := func() {
prevC = -1
if a := adv.Ceil(); a > maxAdv {
maxAdv = a
}
lines = append(lines, Line{
Text: txt[prev:idx],
Width: adv.Ceil(),
Dot: image.Pt(0, doty),
})
wordIdx = 0
wordAdv = 0
doty += lheight
}
for idx < len(txt) {
c, n := utf8.DecodeRuneInString(txt[idx:])
a, ok := l.Face.GlyphAdvance(c)
if !ok {
prevC = -1
idx += n
continue
}
softnl := unicode.IsSpace(c)
if softnl {
wordIdx = idx
wordAdv = adv
}
if prevC >= 0 {
a += l.Face.Kern(prevC, c)
}
a += fixed.I(l.LetterSpacing)
if c == '\n' || (idx > prev && (adv+a).Ceil() > maxWidth) {
if wordIdx > 0 {
idx = wordIdx
adv = wordAdv
_, n = utf8.DecodeRuneInString(txt[idx:])
softnl = true
}
endLine()
prev = idx
idx += n
adv = a
if softnl {
// Skip space or newline.
prev += n
adv = 0
}
continue
}
idx += n
prevC = c
adv += a
}
idx = len(txt)
if prev < idx {
endLine()
}
for i, line := range lines {
switch l.Alignment {
case AlignCenter:
lines[i].Dot.X = (maxAdv - line.Width) / 2
case AlignEnd:
lines[i].Dot.X = maxAdv - line.Width
}
}
return lines, image.Point{
X: maxAdv,
Y: doty - lheight + desc.Ceil(),
}
}