Pad/doc/shaper_usage.md
2026-05-09 11:02:42 -04:00

310 lines
8.1 KiB
Markdown

# 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
```go
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:
```go
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:
```go
params := text.Parameters{
PxPerEm: fixed.I(gtx.Sp(size)), // size is unit.Sp
}
```
**WRONG**: Don't use arbitrary multipliers like `1024 * pixelsPerDp`:
```go
// 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:
```go
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)
```go
shp.LayoutString(params, "Hello, World!")
```
### 3.2 Layout (for readers)
```go
shp.Layout(params, strings.NewReader("Hello, World!"))
```
## 4. Iterating Through Glyphs
After layout, iterate through glyphs using `NextGlyph()`:
```go
for {
g, ok := shp.NextGlyph()
if !ok {
break
}
// Process glyph g
}
```
### 4.1 Glyph Fields
```go
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:
```go
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.Advance` is in `fixed.Int26_6` format (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:
```go
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
```go
// 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:
```go
// 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):
```go
// 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):
```go
// 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:
```go
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**:
1. Layout is done once with `lt.Layout(e.params, r)`
2. Glyphs are iterated with `NextGlyph()` and stored in `e.index`
3. 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)
```go
// 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:
1. Layout text with `MaxWidth` for word wrap
2. Iterate through glyphs to get per-character positions
3. 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))` for `PxPerEm`
- **Always** call `shp.Draw()` after `LayoutString()`/`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