Files
prevara/glyphiter.go
2025-11-30 16:45:22 +00:00

144 lines
4.4 KiB
Go

package gel
import (
"image"
"github.com/p9c/p9/pkg/gel/gio/f32"
l "github.com/p9c/p9/pkg/gel/gio/layout"
"github.com/p9c/p9/pkg/gel/gio/op"
"github.com/p9c/p9/pkg/gel/gio/op/clip"
"github.com/p9c/p9/pkg/gel/gio/op/paint"
"github.com/p9c/p9/pkg/gel/gio/text"
"golang.org/x/image/math/fixed"
)
// GlyphIterator computes bounding boxes and paints text glyphs.
// It is the consolidated iterator used by Label, Text, and Editor widgets.
type GlyphIterator struct {
Viewport image.Rectangle
MaxLines int
Material op.CallOp // Optional material for text color
Truncated int
LinesSeen int
LineOff f32.Point
Padding image.Rectangle
Bounds image.Rectangle
Visible bool
First bool
Baseline int
}
// FixedToFloat converts a fixed.Int26_6 to float32.
func FixedToFloat(i fixed.Int26_6) float32 {
return float32(i) / 64.0
}
// ProcessGlyph checks whether the glyph is visible within the iterator's viewport
// and updates the iterator's text dimensions to include the glyph.
func (it *GlyphIterator) ProcessGlyph(g text.Glyph, ok bool) (visibleOrBefore bool) {
if it.MaxLines > 0 {
if g.Flags&text.FlagTruncator != 0 && g.Flags&text.FlagClusterBreak != 0 {
it.Truncated = int(g.Runes)
}
if g.Flags&text.FlagLineBreak != 0 {
it.LinesSeen++
}
if it.LinesSeen == it.MaxLines && g.Flags&text.FlagParagraphBreak != 0 {
return false
}
}
if d := g.Bounds.Min.X.Floor(); d < it.Padding.Min.X {
it.Padding.Min.X = d
}
if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.Padding.Max.X {
it.Padding.Max.X = d
}
if d := (g.Bounds.Min.Y + g.Ascent).Floor(); d < it.Padding.Min.Y {
it.Padding.Min.Y = d
}
if d := (g.Bounds.Max.Y - g.Descent).Ceil(); d > it.Padding.Max.Y {
it.Padding.Max.Y = d
}
logicalBounds := image.Rectangle{
Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()),
Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()),
}
if !it.First {
it.First = true
it.Baseline = int(g.Y)
it.Bounds = logicalBounds
}
above := logicalBounds.Max.Y < it.Viewport.Min.Y
below := logicalBounds.Min.Y > it.Viewport.Max.Y
left := logicalBounds.Max.X < it.Viewport.Min.X
right := logicalBounds.Min.X > it.Viewport.Max.X
it.Visible = !above && !below && !left && !right
if it.Visible {
it.Bounds.Min.X = min(it.Bounds.Min.X, logicalBounds.Min.X)
it.Bounds.Min.Y = min(it.Bounds.Min.Y, logicalBounds.Min.Y)
it.Bounds.Max.X = max(it.Bounds.Max.X, logicalBounds.Max.X)
it.Bounds.Max.Y = max(it.Bounds.Max.Y, logicalBounds.Max.Y)
}
return ok && !below
}
// PaintGlyph buffers and paints text glyphs. If Material is set, it applies the material.
func (it *GlyphIterator) PaintGlyph(gtx l.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) {
visibleOrBefore := it.ProcessGlyph(glyph, true)
if it.Visible {
if len(line) == 0 {
it.LineOff = f32.Point{X: FixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(l.FPt(it.Viewport.Min))
}
line = append(line, glyph)
}
if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore {
t := op.Affine(f32.AffineId().Offset(it.LineOff)).Push(gtx.Ops)
path := shaper.Shape(line)
outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
if it.Material != (op.CallOp{}) {
it.Material.Add(gtx.Ops)
}
paint.PaintOp{}.Add(gtx.Ops)
outline.Pop()
if call := shaper.Bitmaps(line); call != (op.CallOp{}) {
call.Add(gtx.Ops)
}
t.Pop()
line = line[:0]
}
return line, visibleOrBefore
}
// RenderText is a helper that renders text using the GlyphIterator.
// It handles the common pattern of iterating glyphs and computing dimensions.
func RenderText(gtx l.Context, shaper *text.Shaper, maxLines int, material op.CallOp) l.Dimensions {
cs := gtx.Constraints
m := op.Record(gtx.Ops)
viewport := image.Rectangle{Max: cs.Max}
it := GlyphIterator{
Viewport: viewport,
MaxLines: maxLines,
Material: material,
}
var glyphs [32]text.Glyph
line := glyphs[:0]
for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() {
var ok bool
if line, ok = it.PaintGlyph(gtx, shaper, g, line); !ok {
break
}
}
call := m.Stop()
viewport.Min = viewport.Min.Add(it.Padding.Min)
viewport.Max = viewport.Max.Add(it.Padding.Max)
clipStack := clip.Rect(viewport).Push(gtx.Ops)
call.Add(gtx.Ops)
dims := l.Dimensions{Size: it.Bounds.Size()}
dims.Size = cs.Constrain(dims.Size)
dims.Baseline = dims.Size.Y - it.Baseline
clipStack.Pop()
return dims
}