diff --git a/text/gotext.go b/text/gotext.go index a358ea8d..6d729da8 100644 --- a/text/gotext.go +++ b/text/gotext.go @@ -74,6 +74,9 @@ type line struct { // descent is the height below the baseline, including // the line gap. descent fixed.Int26_6 + // lineHeight captures the gap that should exist between the baseline of this + // line and the previous (if any). + lineHeight fixed.Int26_6 // bounds is the visible bounds of the line. bounds fixed.Rectangle26_6 // direction is the dominant direction of the line. This direction will be @@ -471,13 +474,17 @@ func (s *shaperImpl) Layout(params Parameters, txt io.RuneReader) document { } func calculateYOffsets(lines []line) { - currentY := 0 - prevDesc := fixed.I(0) + if len(lines) < 1 { + return + } + // Ceil the first value to ensure that we don't baseline it too close to the top of the + // viewport and cut off the top pixel. + currentY := fixed.I(lines[0].ascent.Ceil()) for i := range lines { - ascent, descent := lines[i].ascent, lines[i].descent - currentY += (prevDesc + ascent).Ceil() - lines[i].yOffset = currentY - prevDesc = descent + if i > 0 { + currentY += lines[i].lineHeight + } + lines[i].yOffset = currentY.Round() } } @@ -499,8 +506,12 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document { } // Convert to Lines. textLines := make([]line, len(ls)) + maxHeight := fixed.Int26_6(0) for i := range ls { otLine := toLine(&s.orderer, ls[i], params.Locale.Direction) + if otLine.lineHeight > maxHeight { + maxHeight = otLine.lineHeight + } isFinalLine := i == len(ls)-1 if isFinalLine && hasNewline { // If there was a trailing newline update the rune counts to include @@ -548,6 +559,17 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document { } textLines[i] = otLine } + if params.LineHeight != 0 { + maxHeight = params.LineHeight + } + if params.LineHeightScale == 0 { + params.LineHeightScale = 1.2 + } + + maxHeight = floatToFixed(fixedToFloat(maxHeight) * params.LineHeightScale) + for i := range textLines { + textLines[i].lineHeight = maxHeight + } calculateYOffsets(textLines) return document{ lines: textLines, @@ -633,6 +655,10 @@ func fixedToFloat(i fixed.Int26_6) float32 { return float32(i) / 64.0 } +func floatToFixed(f float32) fixed.Int26_6 { + return fixed.Int26_6(f * 64) +} + // Bitmaps returns an op.CallOp that will display all bitmap glyphs within gs. // The positioning of the bitmaps uses the same logic as Shape(), so the returned // CallOp can be added at the same offset as the path data returned by Shape() @@ -781,8 +807,12 @@ func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line runs: make([]runLayout, len(o)), direction: dir, } + maxSize := fixed.Int26_6(0) for i := range o { run := o[i] + if run.Size > maxSize { + maxSize = run.Size + } line.runs[i] = runLayout{ Glyphs: toGioGlyphs(run.Glyphs, run.Size, orderer.indexFor(run.Face)), Runes: Range{ @@ -810,6 +840,7 @@ func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line line.descent = -run.LineBounds.Descent + run.LineBounds.Gap } } + line.lineHeight = maxSize computeVisualOrder(&line) // Account for glyphs hanging off of either side in the bounds. if len(line.visualOrder) > 0 { diff --git a/text/lru.go b/text/lru.go index 35dd4b4f..4eff262f 100644 --- a/text/lru.go +++ b/text/lru.go @@ -160,6 +160,8 @@ type layoutKey struct { font giofont.Font forceTruncate bool wrapPolicy WrapPolicy + lineHeight fixed.Int26_6 + lineHeightScale float32 } type pathKey struct { diff --git a/text/shaper.go b/text/shaper.go index b2297501..bcab39e3 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -62,6 +62,16 @@ type Parameters struct { // Locale provides primary direction and language information for the shaped text. Locale system.Locale + // LineHeightScale is a scaling factor applied to the LineHeight of a paragraph. If zero, a default + // value of 1.2 will be used. + LineHeightScale float32 + + // LineHeight is the distance between the baselines of two lines of text. If zero, the PxPerEm + // of the any given paragraph will set the LineHeight of that paragraph. This value will be + // scaled by LineHeightScale, so applications desiring a specific fixed value + // should set LineHeightScale to 1. + LineHeight fixed.Int26_6 + // forceTruncate controls whether the truncator string is inserted on the final line of // text with a MaxLines. It is unexported because this behavior only makes sense for the // shaper to control when it iterates paragraphs of text. @@ -334,16 +344,18 @@ func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte } // Alignment is not part of the cache key because changing it does not impact shaping. lk := layoutKey{ - ppem: params.PxPerEm, - maxWidth: params.MaxWidth, - minWidth: params.MinWidth, - maxLines: params.MaxLines, - truncator: params.Truncator, - locale: params.Locale, - font: params.Font, - forceTruncate: params.forceTruncate, - wrapPolicy: params.WrapPolicy, - str: asStr, + ppem: params.PxPerEm, + maxWidth: params.MaxWidth, + minWidth: params.MinWidth, + maxLines: params.MaxLines, + truncator: params.Truncator, + locale: params.Locale, + font: params.Font, + forceTruncate: params.forceTruncate, + wrapPolicy: params.WrapPolicy, + str: asStr, + lineHeight: params.LineHeight, + lineHeightScale: params.LineHeightScale, } if l, ok := l.layoutCache.Get(lk); ok { return l diff --git a/widget/index_test.go b/widget/index_test.go index 3d767652..ee86410a 100644 --- a/widget/index_test.go +++ b/widget/index_test.go @@ -169,10 +169,10 @@ func TestIndexPositionWhitespace(t *testing.T) { {x: fixed.Int26_6(832), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)}, {x: fixed.Int26_6(832), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}}, {x: fixed.Int26_6(832), y: 54, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 2, lineCol: screenPos{line: 2}}, - {x: fixed.Int26_6(6), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 3, lineCol: screenPos{line: 3}}, - {x: fixed.Int26_6(576), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 4, lineCol: screenPos{line: 3, col: 1}}, - {x: fixed.Int26_6(1146), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 5, lineCol: screenPos{line: 3, col: 2}}, - {x: fixed.Int26_6(1658), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 6, lineCol: screenPos{line: 3, col: 3}}, + {x: fixed.Int26_6(6), y: 74, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 3, lineCol: screenPos{line: 3}}, + {x: fixed.Int26_6(576), y: 74, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 4, lineCol: screenPos{line: 3, col: 1}}, + {x: fixed.Int26_6(1146), y: 74, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 5, lineCol: screenPos{line: 3, col: 2}}, + {x: fixed.Int26_6(1658), y: 74, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 6, lineCol: screenPos{line: 3, col: 3}}, }, }, { @@ -320,7 +320,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(0), - yOff: 56, + yOff: 41, glyphs: 15, width: fixed.Int26_6(7905), ascent: fixed.Int26_6(1407), @@ -328,7 +328,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(0), - yOff: 90, + yOff: 60, glyphs: 18, width: fixed.Int26_6(8813), ascent: fixed.Int26_6(1407), @@ -336,7 +336,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(0), - yOff: 117, + yOff: 80, glyphs: 4, width: fixed.Int26_6(2034), ascent: fixed.Int26_6(968), @@ -359,7 +359,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(0), - yOff: 56, + yOff: 41, glyphs: 19, width: fixed.Int26_6(8801), ascent: fixed.Int26_6(1407), @@ -367,7 +367,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(0), - yOff: 90, + yOff: 60, glyphs: 13, width: fixed.Int26_6(5852), ascent: fixed.Int26_6(1407), @@ -390,7 +390,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(2335), - yOff: 56, + yOff: 41, glyphs: 15, width: fixed.Int26_6(7905), ascent: fixed.Int26_6(1407), @@ -398,7 +398,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(1427), - yOff: 90, + yOff: 60, glyphs: 18, width: fixed.Int26_6(8813), ascent: fixed.Int26_6(1407), @@ -406,7 +406,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(8206), - yOff: 117, + yOff: 80, glyphs: 4, width: fixed.Int26_6(2034), ascent: fixed.Int26_6(968), @@ -429,7 +429,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(1439), - yOff: 56, + yOff: 41, glyphs: 19, width: fixed.Int26_6(8801), ascent: fixed.Int26_6(1407), @@ -437,7 +437,7 @@ func TestIndexPositionLines(t *testing.T) { }, { xOff: fixed.Int26_6(4388), - yOff: 90, + yOff: 60, glyphs: 13, width: fixed.Int26_6(5852), ascent: fixed.Int26_6(1407),