1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicUsize, Ordering};
5
6use ui_layout::{
7 BoxModel, CustomLayouter, InlineBox, LayoutBox, LayoutContext, LineSpan, MeasureResult, Rect,
8};
9
10use crate::engine::bridge::text::GlyphCluster;
11use crate::engine::layouter::builder::DEFAULT_LINE_FACTOR;
12use crate::engine::layouter::types::{LineHeight, TextAlign, TextFlowStyle, WhiteSpace};
13
14thread_local! {
15 static TEXT_RESULTS: RefCell<HashMap<usize, Arc<TextLayoutResult>>> =
16 RefCell::new(HashMap::new());
17}
18
19static NEXT_TEXT_ID: AtomicUsize = AtomicUsize::new(1);
20
21#[derive(Debug, Clone)]
23pub struct TextLayoutResult {
24 pub spans: Vec<LineSpan>,
26 pub line_texts: Vec<String>,
28}
29
30#[derive(Debug)]
37pub struct TextFlowLayouter {
38 pub id: usize,
40 text: String,
41 clusters: Vec<GlyphCluster>,
42 flow_style: TextFlowStyle,
43}
44
45impl TextFlowLayouter {
46 pub fn new(text: String, flow_style: TextFlowStyle, mut clusters: Vec<GlyphCluster>) -> Self {
47 clusters.sort_by_key(|c| c.byte_offset);
48 let id = NEXT_TEXT_ID.fetch_add(1, Ordering::Relaxed);
49 Self {
50 id,
51 text,
52 flow_style,
53 clusters,
54 }
55 }
56
57 fn line_height(&self) -> f32 {
59 match self.flow_style.line_height {
60 LineHeight::Number(factor) => self.flow_style.font_size * factor,
61 LineHeight::Normal => self.flow_style.font_size * DEFAULT_LINE_FACTOR,
62 LineHeight::Px(px) => px,
63 }
64 .max(1.0)
65 }
66
67 pub fn get_result(id: usize) -> Option<Arc<TextLayoutResult>> {
69 TEXT_RESULTS.with(|cache| cache.borrow().get(&id).cloned())
70 }
71
72 fn remove_result(id: usize) {
73 TEXT_RESULTS.with(|cache| {
74 cache.borrow_mut().remove(&id);
75 });
76 }
77
78 fn compute_layout(
79 &self,
80 available_first_line_space: f32,
81 available_space: f32,
82 start_pos: (f32, f32),
83 ) -> TextLayoutResult {
84 let lh = self.line_height();
90 let text_len = self.text.len();
91 let clusters = &self.clusters;
92
93 let align = self.flow_style.text_align;
94 let white_space = self.flow_style.white_space;
95 let wrap_overflow = matches!(
96 white_space,
97 WhiteSpace::Normal
98 | WhiteSpace::PreWrap
99 | WhiteSpace::PreLine
100 | WhiteSpace::BreakSpaces
101 );
102 let forced_newline = matches!(
103 white_space,
104 WhiteSpace::Pre | WhiteSpace::PreWrap | WhiteSpace::PreLine | WhiteSpace::BreakSpaces
105 );
106 let split_unbreakable = matches!(white_space, WhiteSpace::Normal);
107
108 if clusters.is_empty() && !self.text.is_empty() {
110 if forced_newline && self.text.bytes().all(|b| b == b'\n') {
111 let line_count = self.text.bytes().filter(|b| *b == b'\n').count() + 1;
114 return TextLayoutResult {
115 spans: (0..line_count)
116 .map(|i| LineSpan {
117 x_range: start_pos.0..start_pos.0,
118 line_pos: (start_pos.0, start_pos.1 + i as f32 * lh),
119 line_index: i,
120 })
121 .collect(),
122 line_texts: vec![String::new(); line_count],
123 };
124 }
125 return TextLayoutResult {
126 spans: vec![LineSpan {
127 x_range: start_pos.0..start_pos.0,
128 line_pos: start_pos,
129 line_index: 0,
130 }],
131 line_texts: vec![self.text.clone()],
132 };
133 }
134 if clusters.is_empty() && !self.text.is_empty() {
135 return TextLayoutResult {
136 spans: Vec::new(),
137 line_texts: Vec::new(),
138 };
139 }
140
141 let first_line_width = if available_first_line_space > 0.0 {
147 available_first_line_space
148 } else {
149 0.0
150 };
151 let line_width = |line_index: usize| {
152 if line_index == 0 {
153 first_line_width
154 } else {
155 available_space
156 }
157 };
158
159 let aligned_x = |line_index: usize, x_pos: f32, line_w: f32| {
160 let available = line_width(line_index);
161
162 x_pos
163 + match align {
164 TextAlign::Left => 0.0,
165 TextAlign::Center => (available - line_w) / 2.0,
166 TextAlign::Right => available - line_w,
167 }
168 };
169
170 let mut spans: Vec<LineSpan> = Vec::new();
171 let mut line_texts: Vec<String> = Vec::new();
172
173 let mut line_start: usize = 0; let mut x_pos = start_pos.0; let mut y_pos = start_pos.1;
176 let mut line_index: usize = 0;
177 let mut accumulated: f32 = 0.0; let mut last_breakable_cluster: Option<usize> = None; let clusters_between = |from_byte: usize, to_byte: usize| -> f32 {
181 let from_idx = clusters.partition_point(|c| c.byte_offset < from_byte);
182 let to_idx = clusters.partition_point(|c| c.byte_offset < to_byte);
183 clusters[from_idx..to_idx].iter().map(|c| c.width).sum()
184 };
185
186 macro_rules! emit_line {
187 ($end_byte:expr) => {{
188 let end_byte = $end_byte;
189
190 let line_w = clusters_between(line_start, end_byte);
191 x_pos = aligned_x(line_index, x_pos, line_w);
192
193 let line_str = &self.text[line_start..end_byte];
194 let trimmed = line_str.trim_end_matches('\n');
195 let line_text = if trimmed.is_empty() {
196 String::new()
197 } else {
198 trimmed.to_string()
199 };
200
201 spans.push(LineSpan {
202 x_range: x_pos..(x_pos + line_w),
203 line_pos: (x_pos, y_pos),
204 line_index,
205 });
206 line_texts.push(line_text);
207 }};
208 }
209
210 let mut i = 0;
211 while i < clusters.len() {
212 let frag = &clusters[i];
213 let next_byte = clusters
214 .get(i + 1)
215 .map(|f| f.byte_offset)
216 .unwrap_or(text_len);
217
218 if forced_newline && let Some(rel) = self.text[line_start..next_byte].find('\n') {
219 let nl_byte = line_start + rel;
220 let nl_before_cluster = nl_byte < frag.byte_offset;
221
222 if nl_byte > line_start {
223 emit_line!(nl_byte);
224 } else {
225 x_pos = aligned_x(line_index, x_pos, 0.0);
227 spans.push(LineSpan {
228 x_range: x_pos..x_pos,
229 line_pos: (x_pos, y_pos),
230 line_index,
231 });
232 line_texts.push(String::new());
233 }
234
235 line_start = nl_byte + 1;
236 x_pos = 0.0;
237 y_pos += lh;
238 line_index += 1;
239 accumulated = 0.0;
240 last_breakable_cluster = None;
241
242 if !nl_before_cluster {
245 i += 1;
246 }
247 continue;
248 }
249
250 let current_line_width = line_width(line_index);
252 if wrap_overflow && accumulated > 0.0 && accumulated + frag.width > current_line_width {
253 if let Some(break_at) = last_breakable_cluster {
254 let break_byte = if break_at < clusters.len() {
256 clusters[break_at].byte_offset
257 } else {
258 text_len
259 };
260
261 if break_byte > line_start || spans.is_empty() {
262 emit_line!(break_byte);
263
264 line_start = break_byte;
265 x_pos = 0.0;
266 y_pos += lh;
267 line_index += 1;
268 last_breakable_cluster = None;
269
270 accumulated = if break_at < i {
273 clusters_between(break_byte, clusters[i].byte_offset)
274 } else {
275 0.0
276 };
277 }
278 } else if accumulated + frag.width <= available_space {
279 y_pos += lh;
285 line_index += 1;
286 x_pos = 0.0;
287 accumulated = clusters_between(line_start, clusters[i].byte_offset);
288 last_breakable_cluster = None;
289 } else if split_unbreakable {
290 let break_byte = clusters[i].byte_offset;
293 if break_byte > line_start || spans.is_empty() {
294 emit_line!(break_byte);
295
296 line_start = break_byte;
297 x_pos = 0.0;
298 y_pos += lh;
299 line_index += 1;
300 last_breakable_cluster = None;
301 accumulated = 0.0;
302 }
303 }
304 }
305
306 accumulated += frag.width;
308
309 if frag.break_allowed {
310 last_breakable_cluster = Some(i + 1);
311 }
312
313 i += 1;
314 }
315
316 if forced_newline {
323 let rest = &self.text[line_start..text_len];
324 if rest.is_empty() {
325 x_pos = aligned_x(line_index, 0.0, 0.0);
329 spans.push(LineSpan {
330 x_range: x_pos..x_pos,
331 line_pos: (x_pos, y_pos),
332 line_index,
333 });
334 line_texts.push(String::new());
335 } else {
336 for seg in rest.split('\n') {
337 let seg_end = line_start + seg.len();
338 let line_w = clusters_between(line_start, seg_end);
339 x_pos = aligned_x(line_index, x_pos, line_w);
345 spans.push(LineSpan {
346 x_range: x_pos..(x_pos + line_w),
347 line_pos: (x_pos, y_pos),
348 line_index,
349 });
350 line_texts.push(seg.to_string());
351 line_index += 1;
352 y_pos += lh;
353 line_start = seg_end + 1; x_pos = 0.0;
355 }
356 }
357 } else if line_start < text_len {
358 let line_w = clusters_between(line_start, text_len);
359 let trimmed = &self.text[line_start..text_len];
360 x_pos = aligned_x(line_index, x_pos, line_w);
361
362 spans.push(LineSpan {
363 x_range: x_pos..(x_pos + line_w),
364 line_pos: (x_pos, y_pos),
365 line_index,
366 });
367 line_texts.push(trimmed.trim_end_matches('\n').to_string());
368 }
369
370 if spans.is_empty() {
371 spans.push(LineSpan {
373 x_range: start_pos.0..start_pos.0,
374 line_pos: start_pos,
375 line_index: 0,
376 });
377 line_texts.push(String::new());
378 }
379
380 TextLayoutResult { spans, line_texts }
381 }
382}
383
384impl Drop for TextFlowLayouter {
385 fn drop(&mut self) {
386 Self::remove_result(self.id);
387 }
388}
389
390impl CustomLayouter for TextFlowLayouter {
391 fn layout(&mut self, ctx: &LayoutContext) -> LayoutBox {
392 let result = self.compute_layout(
393 ctx.available_inline_size,
394 ctx.containing_block_width.unwrap_or(f32::MAX),
395 ctx.start_pos,
396 );
397 let spans = result.spans.clone();
398
399 TEXT_RESULTS.with(|cache| {
400 cache.borrow_mut().insert(self.id, Arc::new(result));
401 });
402
403 let (start_x, start_y) = ctx.start_pos;
404 let lh = self.line_height();
405 let total_width = spans
406 .iter()
407 .map(|s| s.line_pos.0 + s.width())
408 .filter(|x| !x.is_nan())
409 .max_by(f32::total_cmp)
410 .map(|max_x| (max_x - start_x).max(0.0))
411 .unwrap_or(0.0);
412 let total_height = spans
413 .iter()
414 .map(|s| s.line_index)
415 .max()
416 .map(|line| (line as f32 + 1.0) * lh)
417 .unwrap_or(0.0);
418 let rect = Rect {
419 x: start_x,
420 y: start_y,
421 width: total_width,
422 height: total_height,
423 };
424 let box_model = BoxModel {
425 sticky_edges: None,
426 border_box: rect,
427 padding_box: rect,
428 content_box: rect,
429 children_box: rect,
430 };
431
432 LayoutBox::InlineBox(InlineBox {
433 box_model,
434 line_spans: spans,
435 })
436 }
437
438 fn measure(&self, _ctx: &LayoutContext) -> MeasureResult {
439 let total_width: f32 = self.clusters.iter().map(|c| c.width).sum();
440 let total_height = self.line_height();
441 MeasureResult {
442 width: total_width,
443 height: total_height,
444 }
445 }
446
447 fn write_debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 write!(f, "TextFlowLayouter [{}]", self.text.escape_debug())
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 use crate::engine::layouter::types::TextFlowStyle;
457
458 fn cluster(byte_offset: usize, width: f32, break_allowed: bool) -> GlyphCluster {
459 GlyphCluster {
460 byte_offset,
461 width,
462 break_allowed,
463 }
464 }
465
466 fn layout(text: &str, clusters: Vec<GlyphCluster>, line_width: f32) -> TextLayoutResult {
467 TextFlowLayouter::new(text.to_string(), TextFlowStyle::default(), clusters).compute_layout(
468 line_width,
469 line_width,
470 (0.0, 0.0),
471 )
472 }
473
474 fn layout_with(
475 text: &str,
476 clusters: Vec<GlyphCluster>,
477 line_width: f32,
478 white_space: WhiteSpace,
479 ) -> TextLayoutResult {
480 let mut flow = TextFlowStyle::default();
481 flow.white_space = white_space;
482 TextFlowLayouter::new(text.to_string(), flow, clusters).compute_layout(
483 line_width,
484 line_width,
485 (0.0, 0.0),
486 )
487 }
488
489 #[test]
490 fn wraps_at_word_boundaries() {
491 let result = layout(
492 "aaa bbb ccc",
493 vec![
494 cluster(0, 30.0, false),
495 cluster(3, 5.0, true),
496 cluster(4, 30.0, false),
497 cluster(7, 5.0, true),
498 cluster(8, 30.0, false),
499 ],
500 70.0,
501 );
502 assert_eq!(result.line_texts.len(), 2);
503 assert_eq!(result.line_texts[0].trim_end(), "aaa bbb");
504 assert_eq!(result.line_texts[1], "ccc");
505 assert_eq!(result.spans[0].width(), 70.0);
506 assert_eq!(result.spans[1].width(), 30.0);
507 assert_eq!(result.spans[0].line_index, 0);
508 assert_eq!(result.spans[1].line_index, 1);
509 }
510
511 #[test]
512 fn aligns_each_line_within_the_available_inline_space() {
513 let clusters = vec![
514 cluster(0, 20.0, false),
515 cluster(2, 10.0, true),
516 cluster(3, 20.0, false),
517 ];
518 let mut centered = TextFlowStyle::default();
519 centered.text_align = TextAlign::Center;
520 let result = TextFlowLayouter::new("aa bb".to_string(), centered, clusters.clone())
521 .compute_layout(40.0, 40.0, (0.0, 0.0));
522 assert_eq!(result.line_texts, vec!["aa ", "bb"]);
523 assert_eq!(result.spans[0].line_pos.0, 5.0);
524 assert_eq!(result.spans[1].line_pos.0, 10.0);
525 assert_eq!(result.spans[0].x_range, 5.0..35.0);
526 assert_eq!(result.spans[1].x_range, 10.0..30.0);
527
528 let mut right = TextFlowStyle::default();
529 right.text_align = TextAlign::Right;
530 let result = TextFlowLayouter::new("aa".to_string(), right, clusters[..1].to_vec())
531 .compute_layout(40.0, 40.0, (0.0, 0.0));
532 assert_eq!(result.spans[0].line_pos.0, 20.0);
533 assert_eq!(result.spans[0].x_range, 20.0..40.0);
534 }
535
536 #[test]
537 fn wraps_cjk_per_character() {
538 let clusters: Vec<GlyphCluster> = (0..10).map(|i| cluster(i, 10.0, true)).collect();
539 let result = layout("aaaaaaaaaa", clusters, 40.0);
540 assert_eq!(result.line_texts, vec!["aaaa", "aaaa", "aa"]);
541 assert_eq!(result.spans.len(), 3);
542 for span in &result.spans {
543 assert!(span.width() <= 40.0);
544 }
545 }
546
547 #[test]
548 fn splits_unbreakable_run() {
549 let clusters: Vec<GlyphCluster> = (0..6).map(|i| cluster(i, 20.0, false)).collect();
550 let result = layout("abcdef", clusters, 80.0);
551 assert_eq!(result.line_texts, vec!["abcd", "ef"]);
552 assert_eq!(result.spans[0].width(), 80.0);
553 assert_eq!(result.spans[1].width(), 40.0);
554 }
555
556 #[test]
557 fn no_overhang_for_word_wider_than_line() {
558 let result = layout(
559 "hello supercalifragilistic",
560 vec![
561 cluster(0, 30.0, false),
562 cluster(5, 5.0, true),
563 cluster(6, 93.0, false),
564 ],
565 80.0,
566 );
567 assert_eq!(result.line_texts.len(), 2);
568 assert_eq!(result.line_texts[0].trim_end(), "hello");
569 assert_eq!(result.spans[0].width(), 35.0);
570 assert_eq!(result.spans[1].width(), 93.0);
571 }
572
573 #[test]
574 fn single_cluster_wider_than_line_stays_alone() {
575 let result = layout("x", vec![cluster(0, 93.0, false)], 80.0);
576 assert_eq!(result.line_texts, vec!["x"]);
577 assert_eq!(result.spans.len(), 1);
578 assert_eq!(result.spans[0].width(), 93.0);
579 }
580
581 #[test]
582 fn first_line_uses_narrower_space() {
583 let clusters = vec![
584 cluster(0, 20.0, false),
585 cluster(2, 4.0, true),
586 cluster(3, 20.0, false),
587 cluster(5, 4.0, true),
588 cluster(6, 20.0, false),
589 cluster(8, 4.0, true),
590 cluster(9, 20.0, false),
591 ];
592 let result = TextFlowLayouter::new(
593 "aa bb cc dd".to_string(),
594 TextFlowStyle::default(),
595 clusters,
596 )
597 .compute_layout(32.0, 64.0, (0.0, 0.0));
598 let texts: Vec<String> = result
599 .line_texts
600 .iter()
601 .map(|s| s.trim_end().to_string())
602 .collect();
603 assert_eq!(texts, vec!["aa", "bb cc", "dd"]);
604 assert_eq!(result.spans[0].width(), 24.0);
605 assert_eq!(result.spans[1].width(), 48.0);
606 assert_eq!(result.spans[2].width(), 20.0);
607 }
608
609 #[test]
610 fn first_word_moves_to_next_line_when_first_line_is_narrow() {
611 let clusters = vec![
612 cluster(0, 9.0, false),
613 cluster(1, 9.0, false),
614 cluster(2, 9.0, false),
615 cluster(3, 9.0, false),
616 cluster(4, 9.0, false),
617 cluster(5, 4.0, true),
618 cluster(6, 10.0, false),
619 cluster(7, 10.0, false),
620 cluster(8, 10.0, false),
621 cluster(9, 10.0, false),
622 cluster(10, 10.0, false),
623 ];
624 let result = TextFlowLayouter::new(
625 "Hello world".to_string(),
626 TextFlowStyle::default(),
627 clusters,
628 )
629 .compute_layout(30.0, 100.0, (0.0, 0.0));
630 assert_eq!(result.line_texts, vec!["Hello world"]);
631 assert_eq!(result.spans.len(), 1);
632 assert_eq!(result.spans[0].line_index, 1);
633 assert_eq!(result.spans[0].width(), 99.0);
634 }
635
636 #[test]
637 fn first_word_splits_when_too_wide_for_every_line() {
638 let clusters = vec![
639 cluster(0, 9.0, false),
640 cluster(1, 9.0, false),
641 cluster(2, 9.0, false),
642 cluster(3, 9.0, false),
643 cluster(4, 9.0, false),
644 ];
645 let result = TextFlowLayouter::new("Hello".to_string(), TextFlowStyle::default(), clusters)
646 .compute_layout(30.0, 40.0, (0.0, 0.0));
647 assert_eq!(result.line_texts, vec!["Hell", "o"]);
648 }
649
650 #[test]
651 fn nowrap_never_wraps_on_overflow() {
652 let clusters = vec![
653 cluster(0, 30.0, false),
654 cluster(3, 5.0, true),
655 cluster(4, 30.0, false),
656 cluster(7, 5.0, true),
657 cluster(8, 30.0, false),
658 ];
659 let result = layout_with("aaa bbb ccc", clusters, 50.0, WhiteSpace::Nowrap);
660 assert_eq!(result.line_texts, vec!["aaa bbb ccc"]);
661 assert_eq!(result.spans.len(), 1);
662 assert_eq!(result.spans[0].width(), 100.0);
663 }
664
665 #[test]
666 fn pre_breaks_only_on_newline() {
667 let no_wrap_clusters = vec![
668 cluster(0, 40.0, false),
669 cluster(3, 4.0, true),
670 cluster(4, 40.0, false),
671 cluster(7, 4.0, true),
672 cluster(8, 40.0, false),
673 ];
674 let no_wrap = layout_with("aaa bbb ccc", no_wrap_clusters, 60.0, WhiteSpace::Pre);
675 assert_eq!(no_wrap.line_texts, vec!["aaa bbb ccc"]);
676 assert_eq!(no_wrap.spans.len(), 1);
677 assert_eq!(no_wrap.spans[0].width(), 128.0);
678
679 let newline_clusters = vec![
680 cluster(0, 40.0, false),
681 cluster(4, 4.0, true),
682 cluster(5, 40.0, false),
683 ];
684 let forced = layout_with("aaaa\nbbbb", newline_clusters, 50.0, WhiteSpace::Pre);
685 assert_eq!(forced.line_texts, vec!["aaaa", "bbbb"]);
686 assert_eq!(forced.spans.len(), 2);
687 assert_eq!(forced.spans[0].line_index, 0);
688 assert_eq!(forced.spans[1].line_index, 1);
689 }
690
691 #[test]
692 fn pre_wrap_breaks_on_newline_and_wraps() {
693 let clusters = vec![
694 cluster(0, 20.0, false),
695 cluster(2, 4.0, true),
696 cluster(3, 20.0, false),
697 cluster(5, 4.0, true),
698 cluster(6, 20.0, false),
699 ];
700 let result = layout_with("aa bb cc", clusters, 34.0, WhiteSpace::PreWrap);
701 let texts: Vec<String> = result
702 .line_texts
703 .iter()
704 .map(|s| s.trim_end().to_string())
705 .collect();
706 assert_eq!(texts, vec!["aa", "bb", "cc"]);
707 }
708
709 #[test]
710 fn pre_line_breaks_on_newline_and_wraps() {
711 let clusters = vec![
712 cluster(0, 20.0, false),
713 cluster(2, 4.0, true),
714 cluster(3, 20.0, false),
715 cluster(5, 4.0, true),
716 cluster(6, 20.0, false),
717 ];
718 let result = layout_with("aa bb cc", clusters, 34.0, WhiteSpace::PreLine);
719 let texts: Vec<String> = result
720 .line_texts
721 .iter()
722 .map(|s| s.trim_end().to_string())
723 .collect();
724 assert_eq!(texts, vec!["aa", "bb", "cc"]);
725 assert_eq!(result.spans.len(), 3);
726 }
727
728 #[test]
729 fn break_spaces_wraps_inside_whitespace_run() {
730 let clusters = vec![
731 cluster(0, 20.0, false),
732 cluster(2, 5.0, true),
733 cluster(3, 5.0, true),
734 cluster(4, 20.0, false),
735 cluster(5, 20.0, false),
736 ];
737 let result = layout_with("aa bb", clusters, 25.0, WhiteSpace::BreakSpaces);
738 assert_eq!(result.line_texts, vec!["aa ", " ", "bb"]);
739 assert_eq!(result.spans.len(), 3);
740 }
741
742 #[test]
743 fn leading_newline_emits_empty_first_line() {
744 let clusters = vec![
745 cluster(1, 10.0, false),
746 cluster(2, 10.0, false),
747 cluster(3, 10.0, false),
748 ];
749 let result = layout_with("\nabc", clusters, 100.0, WhiteSpace::PreWrap);
750 assert_eq!(result.line_texts, vec!["", "abc"]);
751 assert_eq!(result.spans.len(), 2);
752 assert_eq!(result.spans[0].line_index, 0);
753 assert_eq!(result.spans[0].width(), 0.0);
754 assert_eq!(result.spans[1].line_index, 1);
755 assert_eq!(result.spans[1].width(), 30.0);
756 }
757
758 #[test]
759 fn trailing_newline_emits_empty_last_line() {
760 let clusters = vec![
761 cluster(0, 10.0, false),
762 cluster(1, 10.0, false),
763 cluster(2, 10.0, false),
764 ];
765 let result = layout_with("abc\n", clusters, 100.0, WhiteSpace::PreWrap);
766 assert_eq!(result.line_texts, vec!["abc", ""]);
768 assert_eq!(result.spans.len(), 2);
769 assert_eq!(result.spans[1].width(), 0.0);
770 }
771
772 #[test]
773 fn leading_and_trailing_newline_emit_empty_lines() {
774 let clusters = vec![
775 cluster(1, 10.0, false),
776 cluster(2, 10.0, false),
777 cluster(3, 10.0, false),
778 ];
779 let result = layout_with("\nabc\n", clusters, 100.0, WhiteSpace::PreWrap);
780 assert_eq!(result.line_texts, vec!["", "abc", ""]);
781 assert_eq!(result.spans.len(), 3);
782 }
783
784 #[test]
785 fn multiple_trailing_newlines_emit_empty_lines() {
786 let clusters = vec![
787 cluster(0, 10.0, false),
788 cluster(1, 10.0, false),
789 cluster(2, 10.0, false),
790 ];
791 let result = layout_with("abc\n\n", clusters, 100.0, WhiteSpace::PreWrap);
792 assert_eq!(result.line_texts, vec!["abc", "", ""]);
793 assert_eq!(result.spans.len(), 3);
794 }
795
796 #[test]
797 fn all_newlines_emit_n_plus_one_lines() {
798 let result = layout_with("\n\n", vec![], 100.0, WhiteSpace::PreWrap);
801 assert_eq!(result.line_texts, vec!["", "", ""]);
802 assert_eq!(result.spans.len(), 3);
803 }
804
805 #[test]
806 fn trailing_newline_ignored_for_normal_whitespace() {
807 let clusters = vec![
808 cluster(0, 10.0, false),
809 cluster(1, 10.0, false),
810 cluster(2, 10.0, false),
811 ];
812 let result = layout_with("abc\n", clusters, 100.0, WhiteSpace::Normal);
815 assert_eq!(result.line_texts, vec!["abc"]);
816 assert_eq!(result.spans.len(), 1);
817 }
818
819 #[test]
820 fn line_after_newline_starts_at_left_edge() {
821 let clusters = vec![
824 cluster(0, 10.0, false),
825 cluster(1, 10.0, false),
826 cluster(2, 10.0, false),
827 cluster(4, 10.0, false),
828 cluster(5, 10.0, false),
829 cluster(6, 10.0, false),
830 ];
831 let mut flow = TextFlowStyle::default();
832 flow.white_space = WhiteSpace::PreWrap;
833 let result = TextFlowLayouter::new("abc\ndef".to_string(), flow, clusters).compute_layout(
834 100.0,
835 100.0,
836 (0.0, 0.0),
837 );
838 assert_eq!(result.spans.len(), 2);
839 assert_eq!(result.spans[0].line_pos.0, 0.0, "first line x");
840 assert_eq!(result.spans[1].line_pos.0, 0.0, "line after newline x");
841 }
842
843 #[test]
844 fn line_after_newline_resets_to_absolute_zero() {
845 let clusters = vec![
848 cluster(0, 10.0, false),
849 cluster(1, 10.0, false),
850 cluster(2, 10.0, false),
851 cluster(4, 10.0, false),
852 cluster(5, 10.0, false),
853 cluster(6, 10.0, false),
854 ];
855 let mut flow = TextFlowStyle::default();
856 flow.white_space = WhiteSpace::PreWrap;
857 let result = TextFlowLayouter::new("abc\ndef".to_string(), flow, clusters).compute_layout(
858 100.0,
859 100.0,
860 (50.0, 0.0),
861 );
862 assert_eq!(result.spans.len(), 2);
863 assert_eq!(result.spans[0].line_pos.0, 50.0, "first line x");
864 assert_eq!(result.spans[1].line_pos.0, 0.0, "line after newline x");
865 }
866
867 #[test]
868 fn wrapped_line_starts_at_absolute_zero() {
869 let clusters = vec![
872 cluster(0, 30.0, false),
873 cluster(3, 5.0, true),
874 cluster(4, 30.0, false),
875 ];
876 let result =
877 TextFlowLayouter::new("aaa bbb".to_string(), TextFlowStyle::default(), clusters)
878 .compute_layout(40.0, 40.0, (50.0, 0.0));
879 assert_eq!(result.spans.len(), 2);
880 assert_eq!(result.spans[0].line_pos.0, 50.0, "first line x");
881 assert_eq!(result.spans[1].line_pos.0, 0.0, "wrapped line x");
882 }
883
884 #[test]
885 fn consecutive_newlines_emit_empty_lines() {
886 let clusters = vec![
887 cluster(0, 10.0, false),
888 cluster(1, 10.0, false),
889 cluster(4, 10.0, false),
890 cluster(5, 10.0, false),
891 ];
892 let result = layout_with("ab\n\ncd", clusters, 100.0, WhiteSpace::PreWrap);
893 assert_eq!(result.line_texts, vec!["ab", "", "cd"]);
894 assert_eq!(result.spans.len(), 3);
895 assert_eq!(result.spans[0].width(), 20.0);
896 assert_eq!(result.spans[1].width(), 0.0);
897 assert_eq!(result.spans[2].width(), 20.0);
898 }
899
900 #[test]
901 fn newline_between_paragraphs_keeps_span_width() {
902 let clusters = vec![
903 cluster(0, 15.0, false),
904 cluster(1, 15.0, false),
905 cluster(3, 15.0, false),
906 ];
907 let result = layout_with("ab\nc", clusters, 100.0, WhiteSpace::Pre);
908 assert_eq!(result.line_texts, vec!["ab", "c"]);
909 assert_eq!(result.spans[0].width(), 30.0);
910 assert_eq!(result.spans[1].width(), 15.0);
911 }
912
913 #[test]
914 fn pre_wrap_does_not_split_unbreakable_word() {
915 let clusters: Vec<GlyphCluster> = (0..6).map(|i| cluster(i, 20.0, false)).collect();
916 let result = layout_with("abcdef", clusters, 80.0, WhiteSpace::PreWrap);
917 assert_eq!(result.line_texts, vec!["abcdef"]);
918 assert_eq!(result.spans.len(), 1);
919 assert_eq!(result.spans[0].width(), 120.0);
920 }
921
922 #[test]
923 fn pre_line_does_not_split_unbreakable_word() {
924 let clusters: Vec<GlyphCluster> = (0..6).map(|i| cluster(i, 20.0, false)).collect();
925 let result = layout_with("abcdef", clusters, 80.0, WhiteSpace::PreLine);
926 assert_eq!(result.line_texts, vec!["abcdef"]);
927 assert_eq!(result.spans.len(), 1);
928 }
929
930 #[test]
931 fn break_spaces_does_not_split_unbreakable_word() {
932 let clusters: Vec<GlyphCluster> = (0..6).map(|i| cluster(i, 20.0, false)).collect();
933 let result = layout_with("abcdef", clusters, 80.0, WhiteSpace::BreakSpaces);
934 assert_eq!(result.line_texts, vec!["abcdef"]);
935 assert_eq!(result.spans.len(), 1);
936 }
937
938 #[test]
939 fn pre_wrap_wraps_at_whitespace_but_not_inside_word() {
940 let clusters = vec![
941 cluster(0, 20.0, false),
942 cluster(1, 20.0, false),
943 cluster(2, 20.0, false),
944 cluster(3, 20.0, false),
945 cluster(4, 4.0, true),
946 cluster(5, 15.0, false),
947 cluster(6, 15.0, false),
948 ];
949 let result = layout_with("abcd ef", clusters, 70.0, WhiteSpace::PreWrap);
950 let texts: Vec<String> = result
951 .line_texts
952 .iter()
953 .map(|s| s.trim_end().to_string())
954 .collect();
955 assert_eq!(texts, vec!["abcd", "ef"]);
956 }
957
958 #[test]
959 fn empty_text_emits_one_empty_line() {
960 let result = layout("", vec![], 100.0);
961 assert_eq!(result.line_texts, vec![""]);
962 assert_eq!(result.spans.len(), 1);
963 }
964
965 #[test]
966 fn trailing_newlines_match_split_semantics() {
967 let cases: &[(&str, &[&str])] = &[
970 ("foo", &["foo"]),
971 ("foo\n", &["foo", ""]),
972 ("foo\n\n", &["foo", "", ""]),
973 ("\n", &["", ""]),
974 ("\n\n", &["", "", ""]),
975 ];
976 for (text, expected) in cases {
977 let clusters: Vec<GlyphCluster> = text
978 .chars()
979 .enumerate()
980 .filter(|(_, c)| !c.is_whitespace())
981 .map(|(i, _)| cluster(i, 10.0, false))
982 .collect();
983 let result = layout_with(text, clusters, 100.0, WhiteSpace::Pre);
984 assert_eq!(&result.line_texts[..], *expected, "text = {:?}", text);
985 }
986 }
987
988 #[test]
989 fn trailing_newlines_do_not_accumulate_x_pos() {
990 let clusters = vec![cluster(0, 10.0, false), cluster(1, 10.0, false)];
994 let mut flow = TextFlowStyle::default();
995 flow.white_space = WhiteSpace::Pre;
996 flow.text_align = TextAlign::Center;
997 let result = TextFlowLayouter::new("ab\n\n".to_string(), flow, clusters).compute_layout(
998 100.0,
999 100.0,
1000 (0.0, 0.0),
1001 );
1002
1003 assert_eq!(result.line_texts, vec!["ab", "", ""]);
1004 assert_eq!(result.spans[0].line_pos.0, 40.0);
1006 assert_eq!(result.spans[1].line_pos.0, 50.0);
1009 assert_eq!(result.spans[2].line_pos.0, 50.0);
1010 }
1011}