Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/text_layout_tests.rs
StratoSDK / crates / strato-ui-renderer / src / windowing / winit / text_layout_tests.rs
1use super::*;
2use crate::fonts::{collect_glyph_indices, init_fonts, Properties};
3use crate::platform::FontDB as _;
4use crate::{
5 elements::DEFAULT_UI_LINE_HEIGHT_RATIO,
6 text_layout::{TextStyle, DEFAULT_TOP_BOTTOM_RATIO},
7};
8use anyhow::Result;
9 
10const FONT_SIZE: f32 = 16.;
11const FRAME_WIDTH: f32 = 80.;
12const FRAME_HEIGHT: f32 = f32::MAX;
13 
14#[test]
15fn test_fixed_width_tab_size_affects_tab_width() -> Result<()> {
16 let (font_db, roboto) = init_fonts();
17 
18 let tabbed = "\tX";
19 let spaced = " X";
20 
21 let line_style = LineStyle {
22 font_size: FONT_SIZE,
23 line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
24 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
25 fixed_width_tab_size: Some(8),
26 };
27 
28 let tabbed_line = font_db.text_layout_system().layout_line(
29 tabbed,
30 line_style,
31 &[(
32 0..tabbed.chars().count(),
33 StyleAndFont::new(roboto, Properties::default(), TextStyle::new()),
34 )],
35 f32::MAX,
36 crate::text_layout::ClipConfig::default(),
37 );
38 let spaced_line = font_db.text_layout_system().layout_line(
39 spaced,
40 line_style,
41 &[(
42 0..spaced.chars().count(),
43 StyleAndFont::new(roboto, Properties::default(), TextStyle::new()),
44 )],
45 f32::MAX,
46 crate::text_layout::ClipConfig::default(),
47 );
48 
49 let error = (tabbed_line.width - spaced_line.width).abs();
50 assert!(
51 error < 1.0,
52 "expected tab width ~= 8 spaces; got tabbed {}, spaced {} (error {})",
53 tabbed_line.width,
54 spaced_line.width,
55 error
56 );
57 
58 Ok(())
59}
60 
61#[test]
62fn test_layout_text_first_line_indent_small() -> Result<()> {
63 let (font_db, roboto) = init_fonts();
64 
65 let text = "Let's lay out s𐍈me Roboto text.";
66 // 0123456789012345678901234567890
67 let line_style = LineStyle {
68 font_size: FONT_SIZE,
69 line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
70 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
71 fixed_width_tab_size: None,
72 };
73 let style_runs = [(
74 0..text.encode_utf16().count(),
75 StyleAndFont::new(roboto, Properties::default(), TextStyle::new()),
76 )];
77 
78 // First, lay out the text with no head indent.
79 let no_indent_frame = font_db.text_layout_system().layout_text(
80 text,
81 line_style,
82 &style_runs,
83 FRAME_WIDTH,
84 FRAME_HEIGHT,
85 Default::default(),
86 None,
87 );
88 
89 // The text should contain multiple lines.
90 // The first line has about the same amount of content as the others,
91 // since there's no head indent.
92 assert_eq!(no_indent_frame.lines().len(), 4);
93 assert_eq!(
94 collect_glyph_indices(&no_indent_frame),
95 vec![
96 vec![0, 1, 2, 3, 4, 5, 6, 7, 8], // 9 is whitespace.
97 vec![10, 11, 12, 13, 14, 15, 16, 17], // 18 is whitespace.
98 vec![19, 20, 21, 22, 23, 24], // 25 is whitespace.
99 vec![26, 27, 28, 29, 30],
100 ]
101 );
102 assert!(first_line_bounded(&no_indent_frame, 0., FRAME_WIDTH));
103 assert!(all_lines_bounded(&no_indent_frame, FRAME_WIDTH));
104 
105 // Lay out the text with a 5px head indent.
106 let small_indent_frame = font_db.text_layout_system().layout_text(
107 text,
108 line_style,
109 &style_runs,
110 FRAME_WIDTH,
111 FRAME_HEIGHT,
112 Default::default(),
113 Some(5.),
114 );
115 
116 // The first line has about the same amount of content as the others,
117 // since the head indent is small.
118 assert_eq!(small_indent_frame.lines().len(), 4);
119 assert_eq!(
120 collect_glyph_indices(&small_indent_frame),
121 vec![
122 vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
123 vec![10, 11, 12, 13, 14, 15, 16, 17],
124 vec![19, 20, 21, 22, 23, 24],
125 vec![26, 27, 28, 29, 30],
126 ]
127 );
128 assert!(first_line_bounded(&small_indent_frame, 5., FRAME_WIDTH));
129 assert!(all_lines_bounded(&small_indent_frame, FRAME_WIDTH));
130 
131 // Lay out the text with a 40px head indent,
132 // which is half the width of the frame.
133 let half_indent_frame = font_db.text_layout_system().layout_text(
134 text,
135 line_style,
136 &style_runs,
137 FRAME_WIDTH,
138 FRAME_HEIGHT,
139 Default::default(),
140 Some(FRAME_WIDTH / 2.),
141 );
142 
143 // The text contains an additional line to accommodate the indent.
144 assert_eq!(half_indent_frame.lines().len(), 5);
145 assert_eq!(
146 collect_glyph_indices(&half_indent_frame),
147 vec![
148 vec![0, 1, 2, 3, 4], // Fewer glyphs fit on this line. 5 is whitespace.
149 vec![6, 7, 8, 9, 10, 11, 12], // 13 is whitespace.
150 vec![14, 15, 16, 17],
151 vec![19, 20, 21, 22, 23, 24],
152 vec![26, 27, 28, 29, 30],
153 ]
154 );
155 assert!(first_line_bounded(
156 &half_indent_frame,
157 FRAME_WIDTH / 2.,
158 FRAME_WIDTH,
159 ));
160 assert!(all_lines_bounded(&half_indent_frame, FRAME_WIDTH));
161 
162 Ok(())
163}
164 
165#[test]
166fn test_layout_text_first_line_indent_medium() -> Result<()> {
167 let (font_db, roboto) = init_fonts();
168 
169 let text = "Let's lay out s𐍈me Roboto text.";
170 // 0123456789012345678901234567890
171 let line_style = LineStyle {
172 font_size: FONT_SIZE,
173 line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
174 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
175 fixed_width_tab_size: None,
176 };
177 let style_runs = [(
178 0..text.encode_utf16().count(),
179 StyleAndFont::new(roboto, Properties::default(), TextStyle::new()),
180 )];
181 
182 // First, lay out the text with no head indent.
183 let no_indent_frame = font_db.text_layout_system().layout_text(
184 text,
185 line_style,
186 &style_runs,
187 FRAME_WIDTH,
188 FRAME_HEIGHT,
189 Default::default(),
190 Some(0.),
191 );
192 
193 // The text should contain multiple lines.
194 // The first line has about the same amount of content as the others,
195 // since there's no head indent.
196 assert_eq!(no_indent_frame.lines().len(), 4);
197 assert_eq!(
198 collect_glyph_indices(&no_indent_frame),
199 vec![
200 vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
201 vec![10, 11, 12, 13, 14, 15, 16, 17],
202 vec![19, 20, 21, 22, 23, 24],
203 vec![26, 27, 28, 29, 30],
204 ]
205 );
206 assert!(first_line_bounded(&no_indent_frame, 0., FRAME_WIDTH));
207 assert!(all_lines_bounded(&no_indent_frame, FRAME_WIDTH));
208 
209 // Lay out the text with a head indent that's 15px smaller than
210 // the width of the frame.
211 let overflow_indent_frame = font_db.text_layout_system().layout_text(
212 text,
213 line_style,
214 &style_runs,
215 FRAME_WIDTH,
216 FRAME_HEIGHT,
217 Default::default(),
218 Some(FRAME_WIDTH - 20.),
219 );
220 
221 // The first line should have some glyphs on it, but not the whole
222 // first word.
223 assert_eq!(overflow_indent_frame.lines().len(), 5);
224 assert_eq!(
225 collect_glyph_indices(&overflow_indent_frame),
226 vec![
227 vec![0, 1], // Only a few glyphs fit.
228 vec![2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
229 vec![14, 15, 16, 17],
230 vec![19, 20, 21, 22, 23, 24],
231 vec![26, 27, 28, 29, 30],
232 ]
233 );
234 assert!(first_line_bounded(
235 &overflow_indent_frame,
236 FRAME_WIDTH - 20.,
237 FRAME_WIDTH,
238 ));
239 assert!(all_lines_bounded(&overflow_indent_frame, FRAME_WIDTH));
240 
241 Ok(())
242}
243 
244#[test]
245fn test_layout_text_first_line_indent_large() -> Result<()> {
246 let (font_db, roboto) = init_fonts();
247 
248 let text = "Let's lay out s𐍈me Roboto text.";
249 // 0123456789012345678901234567890
250 let line_style = LineStyle {
251 font_size: FONT_SIZE,
252 line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
253 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
254 fixed_width_tab_size: None,
255 };
256 let style_runs = [(
257 0..text.encode_utf16().count(),
258 StyleAndFont::new(roboto, Properties::default(), TextStyle::new()),
259 )];
260 
261 // First, lay out the text with no head indent.
262 let no_indent_frame = font_db.text_layout_system().layout_text(
263 text,
264 line_style,
265 &style_runs,
266 FRAME_WIDTH,
267 FRAME_HEIGHT,
268 Default::default(),
269 Some(0.),
270 );
271 
272 // The text should contain multiple lines.
273 // The first line has about the same amount of content as the others,
274 // since there's no head indent.
275 assert_eq!(no_indent_frame.lines().len(), 4);
276 assert_eq!(
277 collect_glyph_indices(&no_indent_frame),
278 vec![
279 vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
280 vec![10, 11, 12, 13, 14, 15, 16, 17],
281 vec![19, 20, 21, 22, 23, 24],
282 vec![26, 27, 28, 29, 30],
283 ]
284 );
285 assert!(first_line_bounded(&no_indent_frame, 0., FRAME_WIDTH));
286 assert!(all_lines_bounded(&no_indent_frame, FRAME_WIDTH));
287 
288 // Lay out the text with a head indent that's 5px bigger than the width of the frame.
289 let overflow_indent_frame = font_db.text_layout_system().layout_text(
290 text,
291 line_style,
292 &style_runs,
293 FRAME_WIDTH,
294 FRAME_HEIGHT,
295 Default::default(),
296 Some(FRAME_WIDTH + 5.),
297 );
298 
299 // The first line is left entirely blank since no glyphs fit on it.
300 assert_eq!(
301 collect_glyph_indices(&overflow_indent_frame),
302 vec![
303 vec![], // No glyphs fit on this line.
304 vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
305 vec![10, 11, 12, 13, 14, 15, 16, 17],
306 vec![19, 20, 21, 22, 23, 24],
307 vec![26, 27, 28, 29, 30],
308 ]
309 );
310 assert!(first_line_bounded(
311 &overflow_indent_frame,
312 FRAME_WIDTH + 5.,
313 FRAME_WIDTH,
314 ));
315 assert!(all_lines_bounded(&overflow_indent_frame, FRAME_WIDTH));
316 
317 // Lay out the text with a 79px head indent,
318 // which spans almost the entire width of the frame.
319 let big_indent_frame = font_db.text_layout_system().layout_text(
320 text,
321 line_style,
322 &style_runs,
323 FRAME_WIDTH,
324 FRAME_HEIGHT,
325 Default::default(),
326 Some(FRAME_WIDTH - 0.1),
327 );
328 
329 // The first line is left entirely blank since no glyphs fit on it.
330 assert_eq!(big_indent_frame.lines().len(), 5);
331 assert_eq!(
332 collect_glyph_indices(&big_indent_frame),
333 vec![
334 vec![], // No glyphs fit on this line.
335 vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
336 vec![10, 11, 12, 13, 14, 15, 16, 17],
337 vec![19, 20, 21, 22, 23, 24],
338 vec![26, 27, 28, 29, 30],
339 ]
340 );
341 assert!(first_line_bounded(
342 &big_indent_frame,
343 FRAME_WIDTH - 0.1,
344 FRAME_WIDTH,
345 ));
346 assert!(all_lines_bounded(&big_indent_frame, FRAME_WIDTH));
347 
348 Ok(())
349}
350 
351// TODO(PLAT-779): check all line bounds once bidirectional wrapping is fixed in cosmic-text.
352// See https://github.com/pop-os/cosmic-text/issues/252.
353#[test]
354fn test_layout_text_first_line_indent_small_bidirectional() -> Result<()> {
355 let (font_db, roboto) = init_fonts();
356 
357 let text = "brekkie, إفطار, lunch (غداء) and dinner - عشاء";
358 // 0123456783210945678901265437890123456789015432
359 // RTL spans: |-----| |----| |----|
360 let line_style = LineStyle {
361 font_size: FONT_SIZE,
362 line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
363 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
364 fixed_width_tab_size: None,
365 };
366 let style_runs = [(
367 0..text.encode_utf16().count(),
368 StyleAndFont::new(roboto, Properties::default(), TextStyle::new()),
369 )];
370 
371 // First, lay out the text with no head indent.
372 let no_indent_frame = font_db.text_layout_system().layout_text(
373 text,
374 line_style,
375 &style_runs,
376 FRAME_WIDTH,
377 FRAME_HEIGHT,
378 Default::default(),
379 None,
380 );
381 
382 // The text should contain multiple lines.
383 // The first line has about the same amount of content as the others,
384 // since there's no head indent.
385 assert_eq!(no_indent_frame.lines().len(), 4);
386 assert!(first_line_bounded(&no_indent_frame, 0., FRAME_WIDTH));
387 // assert!(all_lines_bounded(&no_indent_frame, FRAME_WIDTH));
388 
389 // Lay out the text with a 5px head indent.
390 let small_indent_frame = font_db.text_layout_system().layout_text(
391 text,
392 line_style,
393 &style_runs,
394 FRAME_WIDTH,
395 FRAME_HEIGHT,
396 Default::default(),
397 Some(5.),
398 );
399 
400 // The first line has about the same amount of content as the others,
401 // since the head indent is small.
402 assert_eq!(small_indent_frame.lines().len(), 4);
403 assert!(first_line_bounded(&small_indent_frame, 5., FRAME_WIDTH));
404 // assert!(all_lines_bounded(&small_indent_frame, FRAME_WIDTH));
405 
406 // Lay out the text with a 40px head indent,
407 // which is half the width of the frame.
408 let half_indent_frame = font_db.text_layout_system().layout_text(
409 text,
410 line_style,
411 &style_runs,
412 FRAME_WIDTH,
413 FRAME_HEIGHT,
414 Default::default(),
415 Some(FRAME_WIDTH / 2.),
416 );
417 
418 // The text contains an additional line to accommodate the indent.
419 assert_eq!(half_indent_frame.lines().len(), 5);
420 assert!(first_line_bounded(
421 &half_indent_frame,
422 FRAME_WIDTH / 2.,
423 FRAME_WIDTH,
424 ));
425 // assert!(all_lines_bounded(&half_indent_frame, FRAME_WIDTH));
426 
427 Ok(())
428}
429 
430// TODO(PLAT-779): check all line bounds once bidirectional wrapping is fixed in cosmic-text.
431// See https://github.com/pop-os/cosmic-text/issues/252.
432#[test]
433fn test_layout_text_first_line_indent_medium_bidirectional() -> Result<()> {
434 let (font_db, roboto) = init_fonts();
435 
436 let text = "brekkie, إفطار, lunch (غداء) and dinner - عشاء";
437 // 0123456783210945678901265437890123456789015432
438 // RTL spans: |-----| |----| |----|
439 let line_style = LineStyle {
440 font_size: FONT_SIZE,
441 line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
442 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
443 fixed_width_tab_size: None,
444 };
445 let style_runs = [(
446 0..text.encode_utf16().count(),
447 StyleAndFont::new(roboto, Properties::default(), TextStyle::new()),
448 )];
449 
450 // First, lay out the text with no head indent.
451 let no_indent_frame = font_db.text_layout_system().layout_text(
452 text,
453 line_style,
454 &style_runs,
455 FRAME_WIDTH,
456 FRAME_HEIGHT,
457 Default::default(),
458 None,
459 );
460 
461 // The text should contain multiple lines.
462 // The first line has about the same amount of content as the others,
463 // since there's no head indent.
464 assert_eq!(no_indent_frame.lines().len(), 4);
465 assert!(first_line_bounded(&no_indent_frame, 0., FRAME_WIDTH));
466 // assert!(all_lines_bounded(&no_indent_frame, FRAME_WIDTH));
467 
468 // Lay out the text with a head indent that's 15px smaller than
469 // the width of the frame.
470 let overflow_indent_frame = font_db.text_layout_system().layout_text(
471 text,
472 line_style,
473 &style_runs,
474 FRAME_WIDTH,
475 FRAME_HEIGHT,
476 Default::default(),
477 Some(FRAME_WIDTH - 20.),
478 );
479 
480 // The first line should have some glyphs on it, but not the whole
481 // first word.
482 assert_eq!(overflow_indent_frame.lines().len(), 5);
483 assert!(first_line_bounded(
484 &overflow_indent_frame,
485 FRAME_WIDTH - 20.,
486 FRAME_WIDTH,
487 ));
488 // assert!(all_lines_bounded(&overflow_indent_frame, FRAME_WIDTH));
489 
490 Ok(())
491}
492 
493// TODO(PLAT-779): check all line bounds once bidirectional wrapping is fixed in cosmic-text.
494// See https://github.com/pop-os/cosmic-text/issues/252.
495#[test]
496fn test_layout_text_first_line_indent_large_bidirectional() -> Result<()> {
497 let (font_db, roboto) = init_fonts();
498 
499 let text = "brekkie, إفطار, lunch (غداء) and dinner - عشاء";
500 // 0123456783210945678901265437890123456789015432
501 // RTL spans: |-----| |----| |----|
502 let line_style = LineStyle {
503 font_size: FONT_SIZE,
504 line_height_ratio: DEFAULT_UI_LINE_HEIGHT_RATIO,
505 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
506 fixed_width_tab_size: None,
507 };
508 let style_runs = [(
509 0..text.encode_utf16().count(),
510 StyleAndFont::new(roboto, Properties::default(), TextStyle::new()),
511 )];
512 
513 // First, lay out the text with no head indent.
514 let no_indent_frame = font_db.text_layout_system().layout_text(
515 text,
516 line_style,
517 &style_runs,
518 FRAME_WIDTH,
519 FRAME_HEIGHT,
520 Default::default(),
521 Some(0.),
522 );
523 
524 // The text should contain multiple lines.
525 // The first line has about the same amount of content as the others,
526 // since there's no head indent.
527 assert_eq!(no_indent_frame.lines().len(), 4);
528 assert!(first_line_bounded(&no_indent_frame, 0., FRAME_WIDTH));
529 // assert!(all_lines_bounded(&no_indent_frame, FRAME_WIDTH));
530 
531 // Lay out the text with a head indent that's 5px bigger than the width of the frame.
532 let overflow_indent_frame = font_db.text_layout_system().layout_text(
533 text,
534 line_style,
535 &style_runs,
536 FRAME_WIDTH,
537 FRAME_HEIGHT,
538 Default::default(),
539 Some(FRAME_WIDTH + 5.),
540 );
541 
542 // The first line is left entirely blank since no glyphs fit on it.
543 assert_eq!(overflow_indent_frame.lines().len(), 5);
544 assert!(collect_glyph_indices(&overflow_indent_frame)
545 .first()
546 .unwrap()
547 .is_empty(),);
548 assert!(first_line_bounded(
549 &overflow_indent_frame,
550 FRAME_WIDTH + 5.,
551 FRAME_WIDTH,
552 ));
553 // assert!(all_lines_bounded(&overflow_indent_frame, FRAME_WIDTH));
554 
555 // Lay out the text with a 79px head indent,
556 // which spans almost the entire width of the frame.
557 let big_indent_frame = font_db.text_layout_system().layout_text(
558 text,
559 line_style,
560 &style_runs,
561 FRAME_WIDTH,
562 FRAME_HEIGHT,
563 Default::default(),
564 Some(FRAME_WIDTH - 0.1),
565 );
566 
567 // The first line is left entirely blank since no glyphs fit on it.
568 assert_eq!(big_indent_frame.lines().len(), 5);
569 assert!(collect_glyph_indices(&big_indent_frame)
570 .first()
571 .unwrap()
572 .is_empty(),);
573 assert!(first_line_bounded(
574 &big_indent_frame,
575 FRAME_WIDTH - 0.1,
576 FRAME_WIDTH,
577 ));
578 // assert!(all_lines_bounded(&big_indent_frame, FRAME_WIDTH));
579 
580 Ok(())
581}
582 
583/// Checks that the head indent and first line's width don't exceed the frame's width.
584fn first_line_bounded(frame: &TextFrame, first_line_indent: f32, frame_width: f32) -> bool {
585 let first_line_width = frame.lines().first().unwrap().width;
586 first_line_width + first_line_indent.min(frame_width) <= frame_width
587}
588 
589fn all_lines_bounded(frame: &TextFrame, frame_width: f32) -> bool {
590 frame.lines().iter().fold(true, |all_bounded, line| {
591 let current_bounded = line.width <= frame_width;
592 all_bounded && current_bounded
593 })
594}
595