# 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