# 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