Pad/internal/ui/shaper.go
Greg Pomerantz fa43bf2402 Add ShapeText/DrawShapedText: single shaper call, zero-call drawing
- New shaper.go: ShapeText() returns ShapedText with measured width +
  stored PathSpec + BitmapsCall, DrawShapedText() draws with zero shaper calls
- drawStatusBar: ShapeText for filename + right icons, positions from widths
- drawBottomBar: ShapeText for cursor/bytepos/wrap, positions by alignment
- No separate measureText helper: ShapeText.Width provides measurements
- StatusBar uses IconLeft/IconRow string slices for icon configuration
- BottomBar uses BarAlignment enum for item positioning
2026-05-12 12:06:28 -04:00

112 lines
2.8 KiB
Go

package ui
import (
"image/color"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"golang.org/x/image/math/fixed"
)
// ShapedText holds layout results from a single ShapeText call.
// The PathSpec and BitmapsCall can be drawn without further shaper access.
type ShapedText struct {
Width int // total width in pixels
PathSpec clip.PathSpec // vector glyph path, ready to draw
BitmapsCall op.CallOp // bitmap glyph call (empty if none)
FirstX fixed.Int26_6 // first glyph X offset
FirstY int32 // first glyph Y offset
Text string
FontSize unit.Sp
}
// ShapeText layouts text, measures width, and returns a ShapedText
// with an immutable path ready for drawing. The shaper is called once.
func (r *Renderer) ShapeText(gtx layout.Context, str string, size unit.Sp) ShapedText {
r.shp.LayoutString(text.Parameters{
PxPerEm: fixed.I(gtx.Sp(size)),
MinWidth: 0,
MaxWidth: 1000,
MaxLines: 1,
}, str)
// Collect glyphs and measure width
var glyphs []text.Glyph
var cumWidth fixed.Int26_6
for {
g, ok := r.shp.NextGlyph()
if !ok {
break
}
cumWidth += g.Advance
glyphs = append(glyphs, g)
}
// Build lines and collect the last line's path spec
var lastSpec clip.PathSpec
var lastBitmaps op.CallOp
var firstX fixed.Int26_6
var firstY int32
var line []text.Glyph
for _, g := range glyphs {
if len(line) == 0 {
firstX = g.X
firstY = g.Y
}
line = append(line, g)
if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
lastSpec = r.shp.Shape(line)
lastBitmaps = r.shp.Bitmaps(line)
line = line[:0]
}
}
if len(line) > 0 {
lastSpec = r.shp.Shape(line)
lastBitmaps = r.shp.Bitmaps(line)
}
return ShapedText{
Width: int(cumWidth >> 6),
PathSpec: lastSpec,
BitmapsCall: lastBitmaps,
FirstX: firstX,
FirstY: firstY,
Text: str,
FontSize: size,
}
}
// DrawShapedText draws a pre-shaped text at the given position.
// x, y are in Dp. Uses the stored path spec — zero shaper calls.
func (r *Renderer) DrawShapedText(gtx layout.Context, st ShapedText, x, y Dp, col color.NRGBA) {
xPx := r.toPx(x)
yPx := r.toPx(y)
m := op.Record(gtx.Ops)
// Compute offset: position + first glyph offset
offX := float32(xPx) + float32(st.FirstX)/64.0
offY := float32(yPx) + float32(st.FirstY)
t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(offX, offY))).Push(gtx.Ops)
// Draw vector path
outline := clip.Outline{Path: st.PathSpec}.Op().Push(gtx.Ops)
paint.ColorOp{Color: col}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
outline.Pop()
// Draw bitmap glyphs
if st.BitmapsCall != (op.CallOp{}) {
st.BitmapsCall.Add(gtx.Ops)
}
t.Pop()
call := m.Stop()
call.Add(gtx.Ops)
}