333 lines
9.1 KiB
Markdown
333 lines
9.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
|
|
|
|
Gio's textView draws glyphs using `shaper.Shape()` (for vector glyphs) and `shaper.Bitmaps()` (for bitmap glyphs like emoji). This is the correct approach to avoid double-shaping.
|
|
|
|
### 5.1 Drawing Text Using Gio's textView Approach
|
|
|
|
```go
|
|
func drawText(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp, x, y unit.Dp, col color.NRGBA) {
|
|
// Layout text
|
|
shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str)
|
|
|
|
// Draw glyphs using the same approach as Gio's textView
|
|
var glyphs [32]text.Glyph
|
|
line := glyphs[:0]
|
|
for g, ok := shp.NextGlyph(); ok; g, ok = shp.NextGlyph() {
|
|
line = append(line, g)
|
|
if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
|
|
drawLine(gtx, shp, line, x, y, col)
|
|
line = line[:0]
|
|
}
|
|
}
|
|
if len(line) > 0 {
|
|
drawLine(gtx, shp, line, x, y, col)
|
|
}
|
|
}
|
|
|
|
func drawLine(gtx layout.Context, shp *text.Shaper, line []text.Glyph, x, y unit.Dp, col color.NRGBA) {
|
|
// Apply offset transform
|
|
off := f32.Point{X: float32(x), Y: float32(y)}
|
|
t := op.Affine(f32.Affine2D{}.Offset(off)).Push(gtx.Ops)
|
|
|
|
// Draw vector glyphs
|
|
path := shp.Shape(line)
|
|
outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
|
|
paint.ColorOp{Color: col}.Add(gtx.Ops)
|
|
paint.PaintOp{}.Add(gtx.Ops)
|
|
outline.Pop()
|
|
|
|
// Draw bitmap glyphs (emoji, etc.)
|
|
if call := shp.Bitmaps(line); call != (op.CallOp{}) {
|
|
call.Add(gtx.Ops)
|
|
}
|
|
|
|
t.Pop()
|
|
}
|
|
```
|
|
|
|
### 5.2 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 for rendering):
|
|
|
|
```go
|
|
// Single pass: layout and draw using shaper.Shape()
|
|
drawText(gtx, shp, text, size, x, y, col)
|
|
```
|
|
|
|
**NOTE**: For truncation, we may need to layout twice:
|
|
1. First layout to measure widths and find truncation point
|
|
2. Second layout to draw the truncated text
|
|
|
|
This is acceptable because truncation is only needed when text doesn't fit, and the string is short (filename in StatusBar).
|
|
|
|
## 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
|