8.1 KiB
Text Shaper Usage Guide
This document describes how to use the Gio text shaper correctly for text layout, measurement, and rendering. It serves as a reference to avoid common mistakes and ensure consistency across the codebase.
1. Creating a Shaper
import (
"gioui.org/text"
"gioui.org/x/gofont"
)
shp := text.NewShaper(text.WithCollection(gofont.Collection()))
Important: Always provide a font collection. Without it, the shaper may not load fonts correctly and will return zero-width glyphs.
2. Layout Parameters
The text.Parameters struct controls how text is shaped:
type Parameters struct {
PxPerEm fixed.Int26_6 // Font size in device pixels
Font font.Font // Specific font (zero value uses default)
MaxWidth int // Maximum width for word wrap (0 = no limit)
MinWidth int // Minimum width for word wrap
TextAlign text.Align // Text alignment (Left, Center, Right)
LineSpacing unit.Sp // Additional spacing between lines
WrapPolicy text.WrapPolicy // How to wrap text
}
2.1 PxPerEm Calculation
CORRECT: Use gtx.Sp(size) to convert SP to device pixels:
params := text.Parameters{
PxPerEm: fixed.I(gtx.Sp(size)), // size is unit.Sp
}
WRONG: Don't use arbitrary multipliers like 1024 * pixelsPerDp:
// WRONG - this produces incorrect widths
params := text.Parameters{
PxPerEm: fixed.Int26_6(1024 * pixelsPerDp),
}
Why: gtx.Sp(size) correctly converts SP to device pixels accounting for DPI scaling. The fixed.I() function converts the result to fixed.Int26_6 format.
2.2 Word Wrap
Set MaxWidth to enable word wrap:
params := text.Parameters{
PxPerEm: fixed.I(gtx.Sp(size)),
MaxWidth: int(widthInPixels),
WrapPolicy: text.WrapTrailingSpace,
}
The shaper automatically breaks text into lines at word boundaries when MaxWidth is set.
3. Layout Functions
3.1 LayoutString (for single strings)
shp.LayoutString(params, "Hello, World!")
3.2 Layout (for readers)
shp.Layout(params, strings.NewReader("Hello, World!"))
4. Iterating Through Glyphs
After layout, iterate through glyphs using NextGlyph():
for {
g, ok := shp.NextGlyph()
if !ok {
break
}
// Process glyph g
}
4.1 Glyph Fields
type Glyph struct {
ID uint32 // Glyph ID
X fixed.Int26_6 // Dot position in document coordinates
Y int32 // Baseline position
Ascent int32 // Line ascent
Descent int32 // Line descent
Advance fixed.Int26_6 // Logical width (horizontal advance)
Runes uint16 // Number of runes this glyph represents
Offset fixed.Point26_6 // Glyph offset from (X, Y)
Bounds image.Rectangle // Glyph bounds in device pixels
}
4.2 Accumulating Widths
To get per-character cumulative widths:
func charWidths(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp) []int {
shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str)
var widths []int
var cumWidth fixed.Int26_6 = 0
for {
g, ok := shp.NextGlyph()
if !ok {
break
}
cumWidth += g.Advance
widths = append(widths, int(cumWidth>>6)) // Convert from Int26_6 to int
}
return widths
}
Key points:
g.Advanceis infixed.Int26_6format (16.6 fixed-point)- Convert to int by shifting right by 6 bits:
int(cumWidth>>6) - The cumulative width is in device pixels
4.3 Getting Per-Character Widths
For hit testing (mapping UI coordinates → byte offset), we need per-character cumulative widths:
func charWidths(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp) []int {
shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str)
var widths []int
var cumWidth fixed.Int26_6 = 0
for {
g, ok := shp.NextGlyph()
if !ok {
break
}
cumWidth += g.Advance
widths = append(widths, int(cumWidth>>6))
}
return widths
}
5. Rendering Glyphs Directly
Instead of using material.Label() (which shapes text again), draw glyphs directly:
5.1 Drawing a Single Glyph
// After layout, iterate through glyphs
for {
g, ok := shp.NextGlyph()
if !ok {
break
}
// Draw glyph at position (g.X, g.Y)
// Use shp.DrawGlyph() or manual drawing
}
5.2 Using Shaper's Drawing Functions
The shaper provides drawing functions that use the already-computed glyph data:
// Draw all laid-out glyphs
shp.Draw(gtx, ops, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
Important: Call shp.Draw() AFTER calling LayoutString() or Layout(). The drawing functions use the glyph data computed during the layout phase.
5.3 Avoiding Double-Shaping
WRONG (shapes text twice):
// First pass: measure
widths := charWidths(gtx, shp, text, size)
// Second pass: render (material.Label calls shaper again)
material.Label(th, size, text).Layout(gtx)
CORRECT (shapes text once):
// Single pass: layout and draw
shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, text)
shp.Draw(gtx, ops, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
6. Comparison with Gio's textView
Gio's textView (widget/text.go) demonstrates the correct approach:
func (e *textView) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp) {
textSize := fixed.I(gtx.Sp(size))
// Set parameters
e.params.PxPerEm = textSize
e.params.MaxWidth = gtx.Constraints.Max.X
// Layout once
lt.Layout(e.params, r)
// Iterate through glyphs and store results
for {
g, ok := lt.NextGlyph()
if !it.processGlyph(g, ok) {
break
}
e.index.Glyph(g) // Store glyph info for rendering
}
}
Key insights:
- Layout is done once with
lt.Layout(e.params, r) - Glyphs are iterated with
NextGlyph()and stored ine.index - Rendering uses the stored glyph data, not a new shaper call
7. Common Pitfalls
7.1 Wrong PxPerEm Calculation
Mistake: Using arbitrary multipliers like 1024 * pixelsPerDp
Fix: Use fixed.I(gtx.Sp(size)) to get the font size in device pixels
7.2 Double-Shaping
Mistake: Calling charWidths() to measure, then material.Label() to render
Fix: Use shp.Draw() to render the already-laid-out glyphs
7.3 Forgetting to Reset Shaper
Mistake: Not clearing the shaper between layouts
Fix: LayoutString() and Layout() automatically reset the shaper state
7.4 Not Converting Int26_6 to int
Mistake: Using int(g.Advance) directly without shifting
Fix: Use int(g.Advance >> 6) to convert from fixed.Int26_6 to int
8. Usage in Pad
8.1 Filename Truncation (StatusBar)
// Measure widths
widths := charWidths(gtx, shp, filename, fontSize)
// Truncate if needed
if widths[len(widths)-1] > availableWidth {
// Find truncation point
}
// Render using shaper.Draw() instead of material.Label()
8.2 TextField (Editor Content)
For the TextField element, we'll use the same approach:
- Layout text with
MaxWidthfor word wrap - Iterate through glyphs to get per-character positions
- Use glyph positions for:
- Hit testing (UI coordinates → byte offset)
- Cursor positioning
- Selection rendering
- IME bridge sync
8.3 BottomBar
The BottomBar uses material.Label() for simple text that doesn't need precise positioning. This is acceptable because:
- The text is short and fixed
- We don't need per-character widths for hit testing
- The performance impact is negligible
However, for consistency, we could also use shp.Draw() for BottomBar text.
9. Summary
- Always use
fixed.I(gtx.Sp(size))forPxPerEm - Always call
shp.Draw()afterLayoutString()/Layout()to render - Avoid using
material.Label()when you need precise glyph positions - Reuse glyph data for measurement, hit testing, and rendering
- Follow Gio's textView as a reference implementation