scene/text.go
Functions
func Bounds
func (ts *TextShape) Bounds() Rect {
if ts.path == nil {
return EmptyRect()
}
return ts.path.Bounds()
}
func Config
Config returns the current configuration.
func (r *TextRenderer) Config() TextRendererConfig {
r.mu.RLock()
defer r.mu.RUnlock()
return r.config
}
func DefaultTextRendererConfig
DefaultTextRendererConfig returns the default configuration.
func DefaultTextRendererConfig() TextRendererConfig {
return TextRendererConfig{
FlipY: false,
SubpixelPositioning: true,
HintingEnabled: false,
}
}
func DrawGlyphs
DrawGlyphs draws pre-shaped glyphs to the scene as a TagText glyph run.
Like DrawText but skips shaping — use when glyphs are already shaped.
The original text string is required for GPU renderer's DrawString path.
func (s *Scene) DrawGlyphs(str string, glyphs []text.ShapedGlyph, face text.Face, x, y float32, brush Brush) error {
if len(glyphs) == 0 {
return nil
}
if face == nil {
return &text.FontError{Reason: "face is nil"}
}
return s.encodeTextRun(str, glyphs, face, x, y, brush)
}
func DrawText
DrawText shapes text and records it as a TagText glyph run in the scene.
Shaping (HarfBuzz) is performed once at recording time. Resolution to pixels
(atlas, outline, text op) is deferred to render/playback time via the renderer's
text resolution capability. See ADR-022.
Breaking change from v0.45: previously converted glyphs to vector paths
(one Fill per glyph, no hinting). Now stores compact glyph references
that renderers resolve at playback time with full hinting and atlas batching.
func (s *Scene) DrawText(str string, face text.Face, x, y float32, brush Brush) error {
if str == "" {
return nil
}
if face == nil {
return &text.FontError{Reason: "face is nil"}
}
shaped := text.Shape(str, face)
if len(shaped) == 0 {
return nil
}
return s.encodeTextRun(str, shaped, face, x, y, brush)
}
func Get
Get retrieves a renderer from the pool.
func (p *TextRendererPool) Get() *TextRenderer {
return p.pool.Get().(*TextRenderer)
}
func NewTextRenderer
NewTextRenderer creates a new text renderer with default configuration.
func NewTextRenderer() *TextRenderer {
return NewTextRendererWithConfig(DefaultTextRendererConfig())
}
func NewTextRendererPool
NewTextRendererPool creates a new pool of text renderers.
func NewTextRendererPool() *TextRendererPool {
return &TextRendererPool{
pool: sync.Pool{
New: func() any {
return NewTextRenderer()
},
},
}
}
func NewTextRendererWithConfig
NewTextRendererWithConfig creates a new text renderer with the given configuration.
func NewTextRendererWithConfig(config TextRendererConfig) *TextRenderer {
return &TextRenderer{
extractor: text.NewOutlineExtractor(),
pathPool: NewPathPool(),
config: config,
}
}
func NewTextShape
NewTextShape creates a TextShape from text.
The text is shaped and converted to a path at the specified position.
func NewTextShape(str string, face text.Face, x, y float32) (*TextShape, error) {
if str == "" {
return &TextShape{path: NewPath()}, nil
}
renderer := NewTextRenderer()
rendered, err := renderer.RenderText(str, face)
if err != nil {
return nil, err
}
// Combine all glyph paths with offset
composite := NewPath()
for _, rg := range rendered {
if rg.Path != nil && !rg.Path.IsEmpty() {
// Transform path to final position
transformed := rg.Path.Transform(TranslateAffine(x, y))
appendPath(composite, transformed)
}
}
return &TextShape{path: composite}, nil
}
func Put
Put returns a renderer to the pool.
func (p *TextRendererPool) Put(r *TextRenderer) {
if r != nil {
p.pool.Put(r)
}
}
func RenderGlyph
RenderGlyph converts a single shaped glyph to a renderable path.
The path is positioned at the glyph's location and scaled appropriately.
func (r *TextRenderer) RenderGlyph(glyph text.ShapedGlyph, face text.Face) (*RenderedGlyph, error) {
r.mu.RLock()
config := r.config
r.mu.RUnlock()
// Get the font source and size
source := face.Source()
if source == nil {
return nil, &text.FontError{Reason: "face has no font source"}
}
size := face.Size()
parsed := source.Parsed()
cache := r.ensureCache()
fontID := computeSceneTextFontID(source)
sizeKey := computeSizeKey(size)
cacheKey := text.OutlineCacheKey{
FontID: fontID,
GID: glyph.GID,
Size: sizeKey,
Hinting: text.HintingNone,
}
outline := cache.GetOrCreate(cacheKey, func() *text.GlyphOutline {
o, err := r.extractor.ExtractOutline(parsed, glyph.GID, size)
if err != nil || o == nil || o.IsEmpty() {
return nil
}
return o
})
// Create the rendered glyph
rendered := &RenderedGlyph{
Advance: float32(glyph.XAdvance),
X: float32(glyph.X),
Y: float32(glyph.Y),
GID: glyph.GID,
Type: text.GlyphTypeOutline,
Cluster: glyph.Cluster,
}
// Handle empty outlines (like space)
if outline == nil {
rendered.Path = nil
rendered.Bounds = EmptyRect()
return rendered, nil
}
// Convert outline to path with positioning
path := r.outlineToPath(outline, float32(glyph.X), float32(glyph.Y), config)
rendered.Path = path
rendered.Bounds = path.Bounds()
return rendered, nil
}
func RenderGlyphs
RenderGlyphs converts multiple shaped glyphs to renderable paths.
Returns a slice of rendered glyphs in the same order as input.
func (r *TextRenderer) RenderGlyphs(glyphs []text.ShapedGlyph, face text.Face) ([]*RenderedGlyph, error) {
if len(glyphs) == 0 {
return nil, nil
}
r.mu.RLock()
config := r.config
r.mu.RUnlock()
// Get the font source and size
source := face.Source()
if source == nil {
return nil, &text.FontError{Reason: "face has no font source"}
}
size := face.Size()
parsed := source.Parsed()
cache := r.ensureCache()
fontID := computeSceneTextFontID(source)
sizeKey := computeSizeKey(size)
// Render all glyphs
rendered := make([]*RenderedGlyph, len(glyphs))
for i, glyph := range glyphs {
rg := &RenderedGlyph{
Advance: float32(glyph.XAdvance),
X: float32(glyph.X),
Y: float32(glyph.Y),
GID: glyph.GID,
Type: text.GlyphTypeOutline,
Cluster: glyph.Cluster,
}
cacheKey := text.OutlineCacheKey{
FontID: fontID,
GID: glyph.GID,
Size: sizeKey,
Hinting: text.HintingNone,
}
outline := cache.GetOrCreate(cacheKey, func() *text.GlyphOutline {
o, err := r.extractor.ExtractOutline(parsed, glyph.GID, size)
if err != nil || o == nil || o.IsEmpty() {
return nil
}
return o
})
if outline == nil {
rg.Path = nil
rg.Bounds = EmptyRect()
rendered[i] = rg
continue
}
// Convert to path
path := r.outlineToPath(outline, float32(glyph.X), float32(glyph.Y), config)
rg.Path = path
rg.Bounds = path.Bounds()
rendered[i] = rg
}
return rendered, nil
}
func RenderRun
RenderRun converts a shaped run to renderable glyphs.
func (r *TextRenderer) RenderRun(run *text.ShapedRun) ([]*RenderedGlyph, error) {
if run == nil || len(run.Glyphs) == 0 {
return nil, nil
}
return r.RenderGlyphs(run.Glyphs, run.Face)
}
func RenderText
RenderText shapes and renders text in one operation.
Uses the global shaper for text shaping.
func (r *TextRenderer) RenderText(str string, face text.Face) ([]*RenderedGlyph, error) {
if str == "" {
return nil, nil
}
// Shape the text
shaped := text.Shape(str, face)
if len(shaped) == 0 {
return nil, nil
}
return r.RenderGlyphs(shaped, face)
}
func SetConfig
SetConfig updates the configuration.
func (r *TextRenderer) SetConfig(config TextRendererConfig) {
r.mu.Lock()
defer r.mu.Unlock()
r.config = config
}
func TextAdvance
TextAdvance returns the total advance width of rendered glyphs.
func TextAdvance(glyphs []*RenderedGlyph) float32 {
if len(glyphs) == 0 {
return 0
}
// The advance is the position of the last glyph plus its advance
last := glyphs[len(glyphs)-1]
return last.X + last.Advance
}
func TextBounds
TextBounds computes the bounding box for rendered text.
func TextBounds(glyphs []*RenderedGlyph) Rect {
if len(glyphs) == 0 {
return EmptyRect()
}
bounds := EmptyRect()
for _, rg := range glyphs {
if rg.Path != nil {
bounds = bounds.Union(rg.Bounds)
}
}
return bounds
}
func ToCompositePath
ToCompositePath combines multiple rendered glyphs into a single path.
This is useful for text that will be rendered as a single unit.
func (r *TextRenderer) ToCompositePath(glyphs []*RenderedGlyph) *Path {
if len(glyphs) == 0 {
return nil
}
composite := NewPath()
for _, rg := range glyphs {
if rg.Path != nil && !rg.Path.IsEmpty() {
appendPath(composite, rg.Path)
}
}
return composite
}
func ToPath
ToPath returns the path representation of the text.
func (ts *TextShape) ToPath() *Path {
return ts.path
}
Structs
type TextRenderer struct
TextRenderer converts shaped text to scene paths for GPU rendering.
It supports glyph outlines and handles positioning, transforms, and caching.
TextRenderer is safe for concurrent use.
type TextRenderer struct {
mu sync.RWMutex
// extractor extracts glyph outlines from fonts
extractor *text.OutlineExtractor
// pathPool reuses Path objects to reduce allocations
pathPool *PathPool
// config holds renderer configuration
config TextRendererConfig
// cache holds cached glyph outlines (lazy-initialized from global)
cache *text.GlyphCache
}
type TextRendererConfig struct
TextRendererConfig holds configuration for TextRenderer.
type TextRendererConfig struct {
// FlipY inverts the Y-axis for glyph outlines (default: false).
// Our OutlineExtractor preserves sfnt's Y-down convention
// (Y=0 at baseline, Y<0 above, Y>0 below), which matches
// screen coordinates directly. Set to true only if your
// coordinate system has Y increasing upward.
FlipY bool
// SubpixelPositioning enables fractional glyph positioning.
// When false, glyphs are snapped to integer pixel positions.
SubpixelPositioning bool
// HintingEnabled enables font hinting for sharper rendering at small sizes.
HintingEnabled bool
}
type RenderedGlyph struct
RenderedGlyph represents a glyph that has been converted to a path.
type RenderedGlyph struct {
// Path is the vector path representing the glyph outline.
// May be nil for non-outline glyphs (bitmap, empty).
Path *Path
// Bounds is the bounding box of the rendered glyph.
Bounds Rect
// Advance is the distance to the next glyph position.
Advance float32
// Position is the glyph position relative to the text origin.
X, Y float32
// GID is the glyph ID.
GID text.GlyphID
// Type indicates the glyph type.
Type text.GlyphType
// Cluster is the source character index.
Cluster int
}
type TextRendererPool struct
TextRendererPool manages a pool of TextRenderers for concurrent use.
type TextRendererPool struct {
pool sync.Pool
}
type TextShape struct
TextShape represents a shaped text string as a scene Shape.
This allows text to be used with fill/stroke operations.
type TextShape struct {
path *Path
}
Bounds returns the bounding rectangle of the text.