gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/widget/label_test.go (about) 1 package widget 2 3 import ( 4 "image" 5 "math" 6 "testing" 7 8 "gioui.org/text" 9 "golang.org/x/image/math/fixed" 10 ) 11 12 // TestGlyphIterator ensures that the glyph iterator computes correct bounding 13 // boxes and baselines for a variety of glyph sequences. 14 func TestGlyphIterator(t *testing.T) { 15 fontSize := 16 16 stdAscent := fixed.I(fontSize) 17 stdDescent := fixed.I(4) 18 stdLineHeight := stdAscent + stdDescent 19 type testcase struct { 20 name string 21 str string 22 maxWidth int 23 maxLines int 24 viewport image.Rectangle 25 expectedDims image.Rectangle 26 expectedBaseline int 27 stopAtGlyph int 28 } 29 for _, tc := range []testcase{ 30 { 31 name: "empty string", 32 str: "", 33 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 34 expectedDims: image.Rectangle{ 35 Max: image.Point{X: 0, Y: stdLineHeight.Round()}, 36 }, 37 expectedBaseline: fontSize, 38 stopAtGlyph: 0, 39 }, 40 { 41 name: "simple", 42 str: "MMM", 43 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 44 expectedDims: image.Rectangle{ 45 Max: image.Point{X: 40, Y: stdLineHeight.Round()}, 46 }, 47 expectedBaseline: fontSize, 48 stopAtGlyph: 2, 49 }, 50 { 51 name: "simple clipped horizontally", 52 str: "MMM", 53 viewport: image.Rectangle{Max: image.Pt(20, math.MaxInt)}, 54 // The dimensions should only include the first two glyphs. 55 expectedDims: image.Rectangle{ 56 Max: image.Point{X: 27, Y: stdLineHeight.Round()}, 57 }, 58 expectedBaseline: fontSize, 59 stopAtGlyph: 2, 60 }, 61 { 62 name: "simple clipped vertically", 63 str: "M\nM\nM\nM", 64 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, 2*stdLineHeight.Floor()-3)}, 65 // The dimensions should only include the first two lines. 66 expectedDims: image.Rectangle{ 67 Max: image.Point{X: 14, Y: 39}, 68 }, 69 expectedBaseline: fontSize, 70 stopAtGlyph: 4, 71 }, 72 { 73 name: "simple truncated", 74 str: "mmm", 75 maxLines: 1, 76 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 77 // This truncation should have no effect because the text is already one line. 78 expectedDims: image.Rectangle{ 79 Max: image.Point{X: 40, Y: stdLineHeight.Round()}, 80 }, 81 expectedBaseline: fontSize, 82 stopAtGlyph: 2, 83 }, 84 { 85 name: "whitespace", 86 str: " ", 87 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 88 expectedDims: image.Rectangle{ 89 Max: image.Point{X: 14, Y: stdLineHeight.Round()}, 90 }, 91 expectedBaseline: fontSize, 92 stopAtGlyph: 2, 93 }, 94 { 95 name: "multi-line with hard newline", 96 str: "你\n好", 97 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 98 expectedDims: image.Rectangle{ 99 Max: image.Point{X: 12, Y: 39}, 100 }, 101 expectedBaseline: fontSize, 102 stopAtGlyph: 3, 103 }, 104 { 105 name: "multi-line with soft newline", 106 str: "你好", // UAX#14 allows line breaking between these characters. 107 maxWidth: fontSize, 108 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 109 expectedDims: image.Rectangle{ 110 Max: image.Point{X: 12, Y: 39}, 111 }, 112 expectedBaseline: fontSize, 113 stopAtGlyph: 2, 114 }, 115 { 116 name: "trailing hard newline", 117 str: "m\n", 118 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 119 // We expect the dimensions to account for two vertical lines because of the 120 // newline at the end. 121 expectedDims: image.Rectangle{ 122 Max: image.Point{X: 14, Y: 39}, 123 }, 124 expectedBaseline: fontSize, 125 stopAtGlyph: 1, 126 }, 127 { 128 name: "truncated trailing hard newline", 129 str: "m\n", 130 maxLines: 1, 131 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 132 // We expect the dimensions to reflect only a single line despite the newline 133 // at the end. 134 expectedDims: image.Rectangle{ 135 Max: image.Point{X: 14, Y: 20}, 136 }, 137 expectedBaseline: fontSize, 138 stopAtGlyph: 1, 139 }, 140 } { 141 t.Run(tc.name, func(t *testing.T) { 142 maxWidth := 200 143 if tc.maxWidth != 0 { 144 maxWidth = tc.maxWidth 145 } 146 glyphs := getGlyphs(16, 0, maxWidth, text.Start, tc.str) 147 it := textIterator{viewport: tc.viewport, maxLines: tc.maxLines} 148 for i, g := range glyphs { 149 ok := it.processGlyph(g, true) 150 if !ok && i != tc.stopAtGlyph { 151 t.Errorf("expected iterator to stop at glyph %d, stopped at %d", tc.stopAtGlyph, i) 152 } 153 if !ok { 154 break 155 } 156 } 157 if it.bounds != tc.expectedDims { 158 t.Errorf("expected bounds %#+v, got %#+v", tc.expectedDims, it.bounds) 159 } 160 if it.baseline != tc.expectedBaseline { 161 t.Errorf("expected baseline %d, got %d", tc.expectedBaseline, it.baseline) 162 } 163 }) 164 } 165 } 166 167 // TestGlyphIteratorPadding ensures that the glyph iterator computes correct padding 168 // around glyphs with unusual bounding boxes. 169 func TestGlyphIteratorPadding(t *testing.T) { 170 type testcase struct { 171 name string 172 glyph text.Glyph 173 viewport image.Rectangle 174 expectedDims image.Rectangle 175 expectedPadding image.Rectangle 176 expectedBaseline int 177 } 178 for _, tc := range []testcase{ 179 { 180 name: "simple", 181 glyph: text.Glyph{ 182 X: 0, 183 Y: 50, 184 Advance: fixed.I(50), 185 Ascent: fixed.I(50), 186 Descent: fixed.I(50), 187 Bounds: fixed.Rectangle26_6{ 188 Min: fixed.Point26_6{ 189 X: fixed.I(-5), 190 Y: fixed.I(-56), 191 }, 192 Max: fixed.Point26_6{ 193 X: fixed.I(57), 194 Y: fixed.I(58), 195 }, 196 }, 197 }, 198 viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, 199 expectedDims: image.Rectangle{ 200 Max: image.Point{X: 50, Y: 100}, 201 }, 202 expectedBaseline: 50, 203 expectedPadding: image.Rectangle{ 204 Min: image.Point{ 205 X: -5, 206 Y: -6, 207 }, 208 Max: image.Point{ 209 X: 7, 210 Y: 8, 211 }, 212 }, 213 }, 214 } { 215 t.Run(tc.name, func(t *testing.T) { 216 it := textIterator{viewport: tc.viewport} 217 it.processGlyph(tc.glyph, true) 218 if it.bounds != tc.expectedDims { 219 t.Errorf("expected bounds %#+v, got %#+v", tc.expectedDims, it.bounds) 220 } 221 if it.baseline != tc.expectedBaseline { 222 t.Errorf("expected baseline %d, got %d", tc.expectedBaseline, it.baseline) 223 } 224 if it.padding != tc.expectedPadding { 225 t.Errorf("expected padding %d, got %d", tc.expectedPadding, it.padding) 226 } 227 }) 228 } 229 }