mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
widget: update glyph iteration
This commit updates the textIterator and glyphIndex types to consume new flag information provided on glyphs. These changes allow widget.Label and widget.Editor to correctly compute text bounding boxes and to generate valid cursor positions at the end of text. Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"gioui.org/text"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// TestGlyphIterator ensures that the glyph iterator computes correct bounding
|
||||
// boxes and baselines for a variety of glyph sequences.
|
||||
func TestGlyphIterator(t *testing.T) {
|
||||
fontSize := 16
|
||||
stdAscent := fixed.I(fontSize)
|
||||
stdDescent := fixed.I(4)
|
||||
stdLineHeight := stdAscent + stdDescent
|
||||
type testcase struct {
|
||||
name string
|
||||
str string
|
||||
maxWidth int
|
||||
maxLines int
|
||||
viewport image.Rectangle
|
||||
expectedDims image.Rectangle
|
||||
expectedBaseline int
|
||||
stopAtGlyph int
|
||||
}
|
||||
for _, tc := range []testcase{
|
||||
{
|
||||
name: "empty string",
|
||||
str: "",
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 0, Y: stdLineHeight.Round()},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 0,
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
str: "MMM",
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 40, Y: stdLineHeight.Round()},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 2,
|
||||
},
|
||||
{
|
||||
name: "simple clipped horizontally",
|
||||
str: "MMM",
|
||||
viewport: image.Rectangle{Max: image.Pt(20, math.MaxInt)},
|
||||
// The dimensions should only include the first two glyphs.
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 27, Y: stdLineHeight.Round()},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 2,
|
||||
},
|
||||
{
|
||||
name: "simple clipped vertically",
|
||||
str: "M\nM\nM\nM",
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, 2*stdLineHeight.Floor()-3)},
|
||||
// The dimensions should only include the first two lines.
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 14, Y: 39},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 4,
|
||||
},
|
||||
{
|
||||
name: "simple truncated",
|
||||
str: "mmm",
|
||||
maxLines: 1,
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||
// This truncation should have no effect because the text is already one line.
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 40, Y: stdLineHeight.Round()},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 2,
|
||||
},
|
||||
{
|
||||
name: "whitespace",
|
||||
str: " ",
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 14, Y: stdLineHeight.Round()},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 2,
|
||||
},
|
||||
{
|
||||
name: "multi-line with hard newline",
|
||||
str: "你\n好",
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 12, Y: 39},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 3,
|
||||
},
|
||||
{
|
||||
name: "multi-line with soft newline",
|
||||
str: "你好", // UAX#14 allows line breaking between these characters.
|
||||
maxWidth: fontSize,
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 12, Y: 39},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 2,
|
||||
},
|
||||
{
|
||||
name: "trailing hard newline",
|
||||
str: "m\n",
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||
// We expect the dimensions to account for two vertical lines because of the
|
||||
// newline at the end.
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 14, Y: 39},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 1,
|
||||
},
|
||||
{
|
||||
name: "truncated trailing hard newline",
|
||||
str: "m\n",
|
||||
maxLines: 1,
|
||||
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||
// We expect the dimensions to reflect only a single line despite the newline
|
||||
// at the end.
|
||||
expectedDims: image.Rectangle{
|
||||
Max: image.Point{X: 14, Y: 20},
|
||||
},
|
||||
expectedBaseline: fontSize,
|
||||
stopAtGlyph: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
maxWidth := 200
|
||||
if tc.maxWidth != 0 {
|
||||
maxWidth = tc.maxWidth
|
||||
}
|
||||
glyphs := getGlyphs(16, 0, maxWidth, text.Start, tc.str)
|
||||
it := textIterator{viewport: tc.viewport, maxLines: tc.maxLines}
|
||||
for i, g := range glyphs {
|
||||
gOut, ok := it.processGlyph(g, true)
|
||||
if gOut != g {
|
||||
t.Errorf("textIterator modified glyphs[%d], original:\n%#+v, modified:\n%#+v", i, g, gOut)
|
||||
}
|
||||
if !ok && i != tc.stopAtGlyph {
|
||||
t.Errorf("expected iterator to stop at glyph %d, stopped at %d", tc.stopAtGlyph, i)
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if it.bounds != tc.expectedDims {
|
||||
t.Errorf("expected bounds %#+v, got %#+v", tc.expectedDims, it.bounds)
|
||||
}
|
||||
if it.baseline != tc.expectedBaseline {
|
||||
t.Errorf("expected baseline %d, got %d", tc.expectedBaseline, it.baseline)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user