text,widget: minimize loss of positional precision in shaping

This commit combs through the logic of computing glyph sizes and positions,
attempting to remove all unnecessary rounding and truncation. This is in
an effort to help text display consistently when different-length strings
are displayed near one another.

The specific problem prompting this change was end-aligned text stacked in
rows with a common suffix. If the rows displayed different values, they
would shape such that those final glyphs were at different fractional x
coordinates, and then they would be aligned with rounding that could display
them at different x positions in spite of the fact that both suffixes are
the same glyphs.

By removing rounding from Alignment.Align, the largest problem is fixed, but
I'm also removing other unnecessary loss of precision that can circumstantially
contribute to this sort of visual issue.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-04-05 10:16:39 -04:00
committed by Elias Naur
parent c0d3f67b04
commit 73787b8478
4 changed files with 32 additions and 24 deletions
+11 -11
View File
@@ -168,10 +168,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(0), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 3, lineCol: screenPos{line: 3}},
{x: fixed.Int26_6(570), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 4, lineCol: screenPos{line: 3, col: 1}},
{x: fixed.Int26_6(1140), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 5, lineCol: screenPos{line: 3, col: 2}},
{x: fixed.Int26_6(1652), 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: 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}},
},
},
{
@@ -380,7 +380,7 @@ func TestIndexPositionLines(t *testing.T) {
glyphs: bidiLTRTextOpp,
expectedLines: []lineInfo{
{
xOff: fixed.Int26_6(3072),
xOff: fixed.Int26_6(3107),
yOff: 22,
glyphs: 15,
width: fixed.Int26_6(7133),
@@ -388,7 +388,7 @@ func TestIndexPositionLines(t *testing.T) {
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(2304),
xOff: fixed.Int26_6(2335),
yOff: 56,
glyphs: 15,
width: fixed.Int26_6(7905),
@@ -396,7 +396,7 @@ func TestIndexPositionLines(t *testing.T) {
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(1408),
xOff: fixed.Int26_6(1427),
yOff: 90,
glyphs: 18,
width: fixed.Int26_6(8813),
@@ -404,7 +404,7 @@ func TestIndexPositionLines(t *testing.T) {
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(8192),
xOff: fixed.Int26_6(8206),
yOff: 117,
glyphs: 4,
width: fixed.Int26_6(2034),
@@ -419,7 +419,7 @@ func TestIndexPositionLines(t *testing.T) {
glyphs: bidiRTLTextOpp,
expectedLines: []lineInfo{
{
xOff: fixed.Int26_6(384),
xOff: fixed.Int26_6(404),
yOff: 22,
glyphs: 20,
width: fixed.Int26_6(9836),
@@ -427,7 +427,7 @@ func TestIndexPositionLines(t *testing.T) {
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(1408),
xOff: fixed.Int26_6(1439),
yOff: 56,
glyphs: 19,
width: fixed.Int26_6(8801),
@@ -435,7 +435,7 @@ func TestIndexPositionLines(t *testing.T) {
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(4352),
xOff: fixed.Int26_6(4388),
yOff: 90,
glyphs: 13,
width: fixed.Int26_6(5852),
+8 -3
View File
@@ -5,6 +5,7 @@ package widget
import (
"image"
"gioui.org/f32"
"gioui.org/io/semantic"
"gioui.org/layout"
"gioui.org/op"
@@ -88,7 +89,7 @@ type textIterator struct {
// linesSeen tracks the quantity of line endings this iterator has seen.
linesSeen int
// lineOff tracks the origin for the glyphs in the current line.
lineOff image.Point
lineOff f32.Point
// padding is the space needed outside of the bounds of the text to ensure no
// part of a glyph is clipped.
padding image.Rectangle
@@ -146,6 +147,10 @@ func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visib
return g, ok && !below
}
func fixedToFloat(i fixed.Int26_6) float32 {
return float32(i) / 64.0
}
// paintGlyph buffers up and paints text glyphs. It should be invoked iteratively upon each glyph
// until it returns false. The line parameter should be a slice with
// a backing array of sufficient size to buffer multiple glyphs.
@@ -157,12 +162,12 @@ func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyp
_, visibleOrBefore := it.processGlyph(glyph, true)
if it.visible {
if len(line) == 0 {
it.lineOff = image.Point{X: glyph.X.Floor(), Y: int(glyph.Y)}.Sub(it.viewport.Min)
it.lineOff = f32.Point{X: fixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(layout.FPt(it.viewport.Min))
}
line = append(line, glyph)
}
if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore {
t := op.Offset(it.lineOff).Push(gtx.Ops)
t := op.Affine(f32.Affine2D{}.Offset(it.lineOff)).Push(gtx.Ops)
path := shaper.Shape(line)
outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
it.material.Add(gtx.Ops)