diff --git a/doc/shaper_usage.md b/doc/shaper_usage.md index db17a11..a471d05 100644 --- a/doc/shaper_usage.md +++ b/doc/shaper_usage.md @@ -476,12 +476,110 @@ Look for: - **Text off-screen**: Wrong offset calculation - **Text clipped**: Clip region doesn't include text bounds +## 9. Multi-line Text Spacing + +When laying out multiple lines of text at different positions, you need to understand how Gio calculates baseline spacing. + +### 9.1 Gio's Line Height Calculation + +In `gio/text/gotext.go` (`calculateYOffsets` and `LayoutRunes`): + +```go +// First line baseline starts at the ascent height +currentY := lines[0].ascent.Ceil() + +// Subsequent baselines are spaced by lineHeight +for i := range lines { + if i > 0 { + currentY += lines[i].lineHeight.Round() + } + lines[i].yOffset = currentY +} + +// lineHeight = max(ascent + descent) * LineHeightScale +// Default LineHeightScale = 1.2 +if params.LineHeight != 0 { + maxHeight = params.LineHeight +} +if params.LineHeightScale == 0 { + params.LineHeightScale = 1.2 +} +maxHeight = floatToFixed(fixedToFloat(maxHeight) * params.LineHeightScale) +``` + +**Key points**: +- **First line baseline**: `ascent.Ceil()` (not 0) +- **Baseline-to-baseline spacing**: `lineHeight * LineHeightScale` +- **Default LineHeightScale**: 1.2 (adds 20% extra padding) +- **LineHeightScale 1.0**: tightest spacing, no extra padding + +### 9.2 Positioning Multiple Lines + +When drawing multiple lines at different positions (e.g., filename + icons in StatusBar), use the baseline spacing as your guide: + +```go +// Line 1: Filename at origin +filenameLineY := reg.Y + +// Line 2: Icons — baseline spacing ≈ font size with LineHeightScale=1.0 +// For 16SP font: baseline-to-baseline ≈ 20DP +iconsLineY := filenameLineY + unit.Dp(20) +``` + +**Why 20DP?** For a 16SP font on a typical display: +- `PxPerEm = 16 * 1.25 = 20` device pixels (2x display) +- `ascent + descent ≈ PxPerEm = 20` +- With `LineHeightScale = 1.0`: baseline spacing = 20DP + +### 9.3 Controlling Spacing + +You have two options to control line spacing: + +**Option A: Set `LineHeightScale` in LayoutString** +```go +shp.LayoutString(text.Parameters{ + PxPerEm: fixed.I(gtx.Sp(size)), + MinWidth: 0, + MaxWidth: availableWidth, + MaxLines: 1, + LineHeightScale: 1.0, // Tight spacing, no extra padding +}, str) +``` + +**Option B: Set `LineHeight` to a specific value** +```go +shp.LayoutString(text.Parameters{ + PxPerEm: fixed.I(gtx.Sp(size)), + MinWidth: 0, + MaxWidth: availableWidth, + MaxLines: 1, + LineHeight: fixed.I(gtx.Sp(18)), // Fixed 18SP baseline spacing +}, str) +``` + +### 9.4 Common Mistakes + +**Mistake**: Adding arbitrary spacing between lines without considering baseline positioning. + +**Fix**: Remember that the shaper's Y value is the **baseline**, not the top of the text. The visual top of the text is at `baseline - ascent`. So the visual gap between two lines is: + +``` +visualGap = (line2Baseline - line1Baseline) - (ascent1 + ascent2) + = lineHeight - (ascent1 + ascent2) +``` + +With `LineHeightScale = 1.0`: `visualGap ≈ 0` (lines touch) +With `LineHeightScale = 1.2`: `visualGap ≈ 0.2 * lineHeight` (20% padding) + ## 10. Summary - **Always** use `fixed.I(gtx.Sp(size))` for `PxPerEm` - **Always** set `MinWidth`, `MaxWidth`, `MaxLines` in `LayoutString` parameters - **Always** offset by `(x + first.X, y + first.Y)` in `drawLine` - **Always** wrap glyph drawing in `op.Record`/`m.Stop()` for clipping +- **Baseline spacing** = `lineHeight * LineHeightScale` (default 1.2) +- **For tight multi-line layouts**, use `LineHeightScale: 1.0` or set explicit `LineHeight` +- **Visual gap** between lines = `baselineSpacing - (ascent1 + ascent2)` - **Avoid** using `material.Label()` when you need precise glyph positions - **Reuse** glyph data for measurement, hit testing, and rendering - **Follow** Gio's `paintGlyph` as the reference implementation diff --git a/internal/ui/render.go b/internal/ui/render.go index 631ffb6..daa35c0 100644 --- a/internal/ui/render.go +++ b/internal/ui/render.go @@ -90,7 +90,6 @@ func (r *Renderer) drawStatusBar(gtx layout.Context, sb StatusBar) { // Line 1: Filename (truncated with ellipsis if needed) filenameLineY := reg.Y - filenameLineH := unit.Dp(24) // Truncate filename only if it doesn't fit displayFilename := sb.Filename @@ -117,8 +116,9 @@ func (r *Renderer) drawStatusBar(gtx layout.Context, sb StatusBar) { // Layout filename, measure widths, and draw in a single pass r.drawTruncatedText(gtx, th.Shaper, displayFilename, r.theme.FontSize, reg.X+unit.Dp(8), filenameLineY, availableWidth, spaceForText, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) - // Line 2: Icons - iconsLineY := filenameLineY + filenameLineH + // Line 2: Icons — positioned 20DP below filename baseline. + // This matches Gio's default line spacing (ascent+descent with LineHeightScale=1.0). + iconsLineY := filenameLineY + unit.Dp(20) // Left side: Cut, Copy, Paste icons leftX := reg.X + unit.Dp(8) diff --git a/pad b/pad index b8e33c7..4eaff5e 100755 Binary files a/pad and b/pad differ