From 0fdaea990f00108d369b013a37602c8c39fc7cd0 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Sat, 9 May 2026 11:02:42 -0400 Subject: [PATCH] Revert to material.Label for rendering - shp.Draw() not available in Gio v0.9.0 Co-authored-by: Qwen-Coder --- doc/shaper_usage.md | 309 ++++++++++++++++++++++++++++++++++++++++++++ pad | Bin 10490034 -> 10490034 bytes 2 files changed, 309 insertions(+) create mode 100644 doc/shaper_usage.md diff --git a/doc/shaper_usage.md b/doc/shaper_usage.md new file mode 100644 index 0000000..08975c6 --- /dev/null +++ b/doc/shaper_usage.md @@ -0,0 +1,309 @@ +# 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 diff --git a/pad b/pad index 2a72bc1b5d55ebf6496b0b66831cc521352be14f..004c7ae077cf22f474706a57dc28a3f71351722b 100755 GIT binary patch delta 1108 zcmb`?{ZGsR0LO76Mdi6X6waZpNZ-3}*Y_?Yy134DU3HZ^QQcjoI;E8AMCB=uJy0rz z-4CsK*tGm`%+n_GFvHCJz-G)3v6;>M;9S(5G88e=XmtWP(kX60GTX%4j^#jrD{a^Hx&Wkfy} zJ{X7b@Wll9!5}R15GG>^reYeVV+Lj-7$KO2*_ea52t^p?VLlc>hK2aMV-Xf( z3BnNp1tPs8@~9B`bWxO)LbOKGYE=%=snrT9??+wKTOY;0Ns#Q4AQagJNm-;79L@rj zQ<8)NyBJ&K=q|UK!u-T|QHoaz-5pj_OiQvp3QMsJ(O8bZU4RlQh=_$6agd;a7V%ht zl~{!YtVSZ%U@elMLo(K3J@iO{0UKaMsyA70nlkx6KW*VD(qKk9GLQ)ivXG4&Y=jjy zY(g$JV+-<-j{?}?fD?r%LNT_Y1f?j03)@hR3T#Ius;~pqsKHLuq7LIX2#0Y5M{x|t(TX;-qXQ>!5}oM6DRiR;y*Q0N^y7@T!+N%$KEL#- zvZka>_gZy%;IX)S^mF_xJmLDyXSW|^JggZUt~lrU14T2rumAu6 delta 1108 zcmb`??@!DD0LSqa3Au_?^7BMWq^`T}?i;1jadKVC>AFze_wL&-r}Pynl^@~q+eHeg z9cvzBe#Ca>#{QPLY*^DB^6bh z3YO<2*40+m7faTaRhHcBVwb3@Ds)sP<_OvOvQ@BDN=;IORlh;D=&Rd?qTNH$e(=W# zj6?uNArPZ624fL~aZn%_Aqd5IOh6bWA{-Hj#3V#vGNxcEreQi}K#7_7yJHq+BL;I2 zi#W{n4MoRCL{CW4=#$k6f>zWyv`&YhOVRp1WyinukNY>NlC_eg(>TjD<)X?iCJBOF z=Tz%7DnZnC)!58a1CvrU`qX4~m&ayKbQ_HEn1}gDzykd3Nl+mfYG{xGEp*UBKq?ku z5z>&3#mGP=vakf%FkmT`!HDI^!3vm=>oXY5A;JIoshg+B!%F0%0IOiZY81kXBG^!j zHCT&vC_yR8V220?oRCnC3RI#B)mRS~HlPNz*oZpF*o1m)#ujWv0~*nUZD>XdTCp8F zuoJt`hTYhMy>O!)`>-Dea1e)Z7)Njv9(3Rsj-wMN(1mWC#3`Ic56<{JwqAQ<*%xcw zuh@_6U++y+PM$R!9R5mkE6=|#?OktVL)Hqz>oGxZo?Y+nDe3og*XO&uKkF~~Z(s7Q z=Y6v!KvAGZ=>8JTYq>>ioWw5v3*A6XIXLO_uSI=5xLjwO{w3X aJ4591fzA&f6y}Fvoj2PX&kxr2dH(=A