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, FloatAvoider, InlineBox, LayoutBox, LayoutContext, LineSpan,
8 MeasureResult, Rect,
9};
10
11use crate::engine::bridge::text::GlyphCluster;
12use crate::engine::layouter::builder::DEFAULT_LINE_FACTOR;
13use crate::engine::layouter::types::{LineHeight, TextAlign, TextFlowStyle, WhiteSpace};
14
15thread_local! {
16 static TEXT_RESULTS: RefCell<HashMap<usize, Arc<TextLayoutResult>>> =
17 RefCell::new(HashMap::new());
18}
19
20static NEXT_TEXT_ID: AtomicUsize = AtomicUsize::new(1);
21
22fn is_usable_width(width: f32) -> bool {
27 width.is_finite() && width < f32::MAX / 2.0
28}
29
30#[derive(Debug, Clone)]
32pub struct TextLayoutResult {
33 pub spans: Vec<LineSpan>,
35 pub line_texts: Vec<String>,
37}
38
39#[derive(Debug)]
46pub struct TextFlowLayouter {
47 pub id: usize,
49 text: String,
50 clusters: Vec<GlyphCluster>,
51 flow_style: TextFlowStyle,
52}
53
54impl TextFlowLayouter {
55 pub fn new(text: String, flow_style: TextFlowStyle, mut clusters: Vec<GlyphCluster>) -> Self {
56 clusters.sort_by_key(|c| c.byte_offset);
57 let id = NEXT_TEXT_ID.fetch_add(1, Ordering::Relaxed);
58 Self {
59 id,
60 text,
61 flow_style,
62 clusters,
63 }
64 }
65
66 fn line_height(&self) -> f32 {
68 match self.flow_style.line_height {
69 LineHeight::Number(factor) => self.flow_style.font_size * factor,
70 LineHeight::Normal => self.flow_style.font_size * DEFAULT_LINE_FACTOR,
71 LineHeight::Px(px) => px,
72 }
73 .max(1.0)
74 }
75
76 pub fn get_result(id: usize) -> Option<Arc<TextLayoutResult>> {
78 TEXT_RESULTS.with(|cache| cache.borrow().get(&id).cloned())
79 }
80
81 fn remove_result(id: usize) {
82 TEXT_RESULTS.with(|cache| {
83 cache.borrow_mut().remove(&id);
84 });
85 }
86
87 #[cfg(test)]
88 fn compute_layout(
89 &self,
90 available_first_line_space: f32,
91 available_space: f32,
92 start_pos: (f32, f32),
93 ) -> TextLayoutResult {
94 self.compute_layout_inner(available_first_line_space, available_space, start_pos, None)
95 }
96
97 fn compute_layout_inner(
106 &self,
107 available_first_line_space: f32,
108 available_space: f32,
109 start_pos: (f32, f32),
110 float_space: Option<&FloatAvoider>,
111 ) -> TextLayoutResult {
112 let lh = self.line_height();
120 let text_len = self.text.len();
121 let clusters = &self.clusters;
122
123 let align = self.flow_style.text_align;
124 let white_space = self.flow_style.white_space;
125 let wrap_overflow = matches!(
126 white_space,
127 WhiteSpace::Normal
128 | WhiteSpace::PreWrap
129 | WhiteSpace::PreLine
130 | WhiteSpace::BreakSpaces
131 );
132 let forced_newline = matches!(
133 white_space,
134 WhiteSpace::Pre | WhiteSpace::PreWrap | WhiteSpace::PreLine | WhiteSpace::BreakSpaces
135 );
136 let split_unbreakable = matches!(white_space, WhiteSpace::Normal);
137
138 if clusters.is_empty() && !self.text.is_empty() {
140 if forced_newline && self.text.bytes().all(|b| b == b'\n') {
141 let line_count = self.text.bytes().filter(|b| *b == b'\n').count() + 1;
144 return TextLayoutResult {
145 spans: (0..line_count)
146 .map(|i| LineSpan {
147 x_range: start_pos.0..start_pos.0,
148 line_pos: (start_pos.0, start_pos.1 + i as f32 * lh),
149 line_index: i,
150 })
151 .collect(),
152 line_texts: vec![String::new(); line_count],
153 };
154 }
155 return TextLayoutResult {
156 spans: vec![LineSpan {
157 x_range: start_pos.0..start_pos.0,
158 line_pos: start_pos,
159 line_index: 0,
160 }],
161 line_texts: vec![self.text.clone()],
162 };
163 }
164 if clusters.is_empty() && !self.text.is_empty() {
165 return TextLayoutResult {
166 spans: Vec::new(),
167 line_texts: Vec::new(),
168 };
169 }
170
171 let first_line_width = if available_first_line_space > 0.0 {
177 available_first_line_space
178 } else {
179 0.0
180 };
181 let line_metrics = |line_index: usize| -> (f32, f32) {
187 if line_index == 0 {
188 (start_pos.0, first_line_width)
189 } else if let Some(avoider) = float_space {
190 let (start, end) = avoider.avail_at(line_index as f32 * lh);
191 let start = start.max(0.0);
192 (start, (end.min(available_space) - start).max(0.0))
193 } else {
194 (0.0, available_space)
195 }
196 };
197 let line_width = |line_index: usize| line_metrics(line_index).1;
198
199 let aligned_x = |line_index: usize, x_pos: f32, line_w: f32| {
200 let available = line_width(line_index);
201 if !is_usable_width(available) {
206 return x_pos;
207 }
208
209 x_pos
210 + match align {
211 TextAlign::Left => 0.0,
212 TextAlign::Center => (available - line_w) / 2.0,
213 TextAlign::Right => available - line_w,
214 }
215 };
216
217 let mut spans: Vec<LineSpan> = Vec::new();
218 let mut line_texts: Vec<String> = Vec::new();
219
220 let mut line_start: usize = 0; let mut x_pos = start_pos.0; let mut y_pos = start_pos.1;
223 let mut line_index: usize = 0;
224 let mut accumulated: f32 = 0.0; let mut last_breakable_cluster: Option<usize> = None; let clusters_between = |from_byte: usize, to_byte: usize| -> f32 {
228 let from_idx = clusters.partition_point(|c| c.byte_offset < from_byte);
229 let to_idx = clusters.partition_point(|c| c.byte_offset < to_byte);
230 clusters[from_idx..to_idx].iter().map(|c| c.width).sum()
231 };
232
233 macro_rules! emit_line {
234 ($end_byte:expr) => {{
235 let end_byte = $end_byte;
236
237 let line_w = clusters_between(line_start, end_byte);
238 x_pos = aligned_x(line_index, x_pos, line_w);
239
240 let line_str = &self.text[line_start..end_byte];
241 let trimmed = line_str.trim_end_matches('\n');
242 let line_text = if trimmed.is_empty() {
243 String::new()
244 } else {
245 trimmed.to_string()
246 };
247
248 spans.push(LineSpan {
249 x_range: x_pos..(x_pos + line_w),
250 line_pos: (x_pos, y_pos),
251 line_index,
252 });
253 line_texts.push(line_text);
254 }};
255 }
256
257 let mut i = 0;
258 while i < clusters.len() {
259 let frag = &clusters[i];
260 let next_byte = clusters
261 .get(i + 1)
262 .map(|f| f.byte_offset)
263 .unwrap_or(text_len);
264
265 if forced_newline && let Some(rel) = self.text[line_start..next_byte].find('\n') {
266 let nl_byte = line_start + rel;
267 let nl_before_cluster = nl_byte < frag.byte_offset;
268
269 if nl_byte > line_start {
270 emit_line!(nl_byte);
271 } else {
272 x_pos = aligned_x(line_index, x_pos, 0.0);
274 spans.push(LineSpan {
275 x_range: x_pos..x_pos,
276 line_pos: (x_pos, y_pos),
277 line_index,
278 });
279 line_texts.push(String::new());
280 }
281
282 line_start = nl_byte + 1;
283 y_pos += lh;
284 line_index += 1;
285 x_pos = line_metrics(line_index).0;
286 accumulated = 0.0;
287 last_breakable_cluster = None;
288
289 if !nl_before_cluster {
292 i += 1;
293 }
294 continue;
295 }
296
297 let current_line_width = line_width(line_index);
299 if wrap_overflow && accumulated > 0.0 && accumulated + frag.width > current_line_width {
300 if let Some(break_at) = last_breakable_cluster {
301 let break_byte = if break_at < clusters.len() {
303 clusters[break_at].byte_offset
304 } else {
305 text_len
306 };
307
308 if break_byte > line_start || spans.is_empty() {
309 emit_line!(break_byte);
310
311 line_start = break_byte;
312 y_pos += lh;
313 line_index += 1;
314 x_pos = line_metrics(line_index).0;
315 last_breakable_cluster = None;
316
317 accumulated = if break_at < i {
320 clusters_between(break_byte, clusters[i].byte_offset)
321 } else {
322 0.0
323 };
324 }
325 } else if accumulated + frag.width <= line_width(line_index + 1) {
326 y_pos += lh;
332 line_index += 1;
333 x_pos = line_metrics(line_index).0;
334 accumulated = clusters_between(line_start, clusters[i].byte_offset);
335 last_breakable_cluster = None;
336 } else if split_unbreakable {
337 let break_byte = clusters[i].byte_offset;
340 if break_byte > line_start || spans.is_empty() {
341 emit_line!(break_byte);
342
343 line_start = break_byte;
344 y_pos += lh;
345 line_index += 1;
346 x_pos = line_metrics(line_index).0;
347 last_breakable_cluster = None;
348 accumulated = 0.0;
349 }
350 }
351 }
352
353 accumulated += frag.width;
355
356 if frag.break_allowed {
357 last_breakable_cluster = Some(i + 1);
358 }
359
360 i += 1;
361 }
362
363 if forced_newline {
370 let rest = &self.text[line_start..text_len];
371 if rest.is_empty() {
372 x_pos = aligned_x(line_index, line_metrics(line_index).0, 0.0);
376 spans.push(LineSpan {
377 x_range: x_pos..x_pos,
378 line_pos: (x_pos, y_pos),
379 line_index,
380 });
381 line_texts.push(String::new());
382 } else {
383 for seg in rest.split('\n') {
384 let seg_end = line_start + seg.len();
385 let line_w = clusters_between(line_start, seg_end);
386 x_pos = aligned_x(line_index, x_pos, line_w);
392 spans.push(LineSpan {
393 x_range: x_pos..(x_pos + line_w),
394 line_pos: (x_pos, y_pos),
395 line_index,
396 });
397 line_texts.push(seg.to_string());
398 line_index += 1;
399 y_pos += lh;
400 line_start = seg_end + 1; x_pos = line_metrics(line_index).0;
402 }
403 }
404 } else if line_start < text_len {
405 let line_w = clusters_between(line_start, text_len);
406 let trimmed = &self.text[line_start..text_len];
407 x_pos = aligned_x(line_index, x_pos, line_w);
408
409 spans.push(LineSpan {
410 x_range: x_pos..(x_pos + line_w),
411 line_pos: (x_pos, y_pos),
412 line_index,
413 });
414 line_texts.push(trimmed.trim_end_matches('\n').to_string());
415 }
416
417 if spans.is_empty() {
418 spans.push(LineSpan {
420 x_range: start_pos.0..start_pos.0,
421 line_pos: start_pos,
422 line_index: 0,
423 });
424 line_texts.push(String::new());
425 }
426
427 TextLayoutResult { spans, line_texts }
428 }
429}
430
431impl Drop for TextFlowLayouter {
432 fn drop(&mut self) {
433 Self::remove_result(self.id);
434 }
435}
436
437impl CustomLayouter for TextFlowLayouter {
438 fn layout(&mut self, ctx: &LayoutContext) -> LayoutBox {
439 let available_inline = ctx.available_inline_size;
451 let available_space = ctx
452 .containing_block_width
453 .filter(|w| is_usable_width(*w))
454 .unwrap_or(f32::MAX);
455 let result = self.compute_layout_inner(
456 available_inline,
457 available_space,
458 ctx.start_pos,
459 ctx.float_space.as_ref(),
460 );
461 let spans = result.spans.clone();
462
463 TEXT_RESULTS.with(|cache| {
464 cache.borrow_mut().insert(self.id, Arc::new(result));
465 });
466
467 let (start_x, start_y) = ctx.start_pos;
468 let lh = self.line_height();
469 let total_width = spans
474 .iter()
475 .map(LineSpan::width)
476 .filter(|w| !w.is_nan())
477 .max_by(f32::total_cmp)
478 .unwrap_or(0.0);
479 let total_height = spans
480 .iter()
481 .map(|s| s.line_index)
482 .max()
483 .map(|line| (line as f32 + 1.0) * lh)
484 .unwrap_or(0.0);
485 let rect = Rect {
486 x: start_x,
487 y: start_y,
488 width: total_width,
489 height: total_height,
490 };
491 let box_model = BoxModel {
492 sticky_edges: None,
493 border_box: rect,
494 padding_box: rect,
495 content_box: rect,
496 children_box: rect,
497 };
498
499 LayoutBox::InlineBox(InlineBox {
500 box_model,
501 line_spans: spans,
502 })
503 }
504
505 fn measure(&self, _ctx: &LayoutContext) -> MeasureResult {
506 let total_width: f32 = self.clusters.iter().map(|c| c.width).sum();
507 let total_height = self.line_height();
508 MeasureResult {
509 width: total_width,
510 height: total_height,
511 }
512 }
513
514 fn write_debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 write!(f, "TextFlowLayouter [{}]", self.text.escape_debug())
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 use crate::engine::layouter::types::TextFlowStyle;
524
525 fn cluster(byte_offset: usize, width: f32, break_allowed: bool) -> GlyphCluster {
526 GlyphCluster {
527 byte_offset,
528 width,
529 break_allowed,
530 }
531 }
532
533 fn layout(text: &str, clusters: Vec<GlyphCluster>, line_width: f32) -> TextLayoutResult {
534 TextFlowLayouter::new(text.to_string(), TextFlowStyle::default(), clusters).compute_layout(
535 line_width,
536 line_width,
537 (0.0, 0.0),
538 )
539 }
540
541 fn layout_with(
542 text: &str,
543 clusters: Vec<GlyphCluster>,
544 line_width: f32,
545 white_space: WhiteSpace,
546 ) -> TextLayoutResult {
547 let flow = TextFlowStyle {
548 white_space,
549 ..TextFlowStyle::default()
550 };
551 TextFlowLayouter::new(text.to_string(), flow, clusters).compute_layout(
552 line_width,
553 line_width,
554 (0.0, 0.0),
555 )
556 }
557
558 #[test]
559 fn wraps_at_word_boundaries() {
560 let result = layout(
561 "aaa bbb ccc",
562 vec![
563 cluster(0, 30.0, false),
564 cluster(3, 5.0, true),
565 cluster(4, 30.0, false),
566 cluster(7, 5.0, true),
567 cluster(8, 30.0, false),
568 ],
569 70.0,
570 );
571 assert_eq!(result.line_texts.len(), 2);
572 assert_eq!(result.line_texts[0].trim_end(), "aaa bbb");
573 assert_eq!(result.line_texts[1], "ccc");
574 assert_eq!(result.spans[0].width(), 70.0);
575 assert_eq!(result.spans[1].width(), 30.0);
576 assert_eq!(result.spans[0].line_index, 0);
577 assert_eq!(result.spans[1].line_index, 1);
578 }
579
580 #[test]
581 fn aligns_each_line_within_the_available_inline_space() {
582 let clusters = vec![
583 cluster(0, 20.0, false),
584 cluster(2, 10.0, true),
585 cluster(3, 20.0, false),
586 ];
587 let centered = TextFlowStyle {
588 text_align: TextAlign::Center,
589 ..TextFlowStyle::default()
590 };
591 let result = TextFlowLayouter::new("aa bb".to_string(), centered, clusters.clone())
592 .compute_layout(40.0, 40.0, (0.0, 0.0));
593 assert_eq!(result.line_texts, vec!["aa ", "bb"]);
594 assert_eq!(result.spans[0].line_pos.0, 5.0);
595 assert_eq!(result.spans[1].line_pos.0, 10.0);
596 assert_eq!(result.spans[0].x_range, 5.0..35.0);
597 assert_eq!(result.spans[1].x_range, 10.0..30.0);
598
599 let right = TextFlowStyle {
600 text_align: TextAlign::Right,
601 ..TextFlowStyle::default()
602 };
603 let result = TextFlowLayouter::new("aa".to_string(), right, clusters[..1].to_vec())
604 .compute_layout(40.0, 40.0, (0.0, 0.0));
605 assert_eq!(result.spans[0].line_pos.0, 20.0);
606 assert_eq!(result.spans[0].x_range, 20.0..40.0);
607 }
608
609 #[test]
610 fn wraps_cjk_per_character() {
611 let clusters: Vec<GlyphCluster> = (0..10).map(|i| cluster(i, 10.0, true)).collect();
612 let result = layout("aaaaaaaaaa", clusters, 40.0);
613 assert_eq!(result.line_texts, vec!["aaaa", "aaaa", "aa"]);
614 assert_eq!(result.spans.len(), 3);
615 for span in &result.spans {
616 assert!(span.width() <= 40.0);
617 }
618 }
619
620 #[test]
621 fn splits_unbreakable_run() {
622 let clusters: Vec<GlyphCluster> = (0..6).map(|i| cluster(i, 20.0, false)).collect();
623 let result = layout("abcdef", clusters, 80.0);
624 assert_eq!(result.line_texts, vec!["abcd", "ef"]);
625 assert_eq!(result.spans[0].width(), 80.0);
626 assert_eq!(result.spans[1].width(), 40.0);
627 }
628
629 #[test]
630 fn no_overhang_for_word_wider_than_line() {
631 let result = layout(
632 "hello supercalifragilistic",
633 vec![
634 cluster(0, 30.0, false),
635 cluster(5, 5.0, true),
636 cluster(6, 93.0, false),
637 ],
638 80.0,
639 );
640 assert_eq!(result.line_texts.len(), 2);
641 assert_eq!(result.line_texts[0].trim_end(), "hello");
642 assert_eq!(result.spans[0].width(), 35.0);
643 assert_eq!(result.spans[1].width(), 93.0);
644 }
645
646 #[test]
647 fn single_cluster_wider_than_line_stays_alone() {
648 let result = layout("x", vec![cluster(0, 93.0, false)], 80.0);
649 assert_eq!(result.line_texts, vec!["x"]);
650 assert_eq!(result.spans.len(), 1);
651 assert_eq!(result.spans[0].width(), 93.0);
652 }
653
654 #[test]
655 fn first_line_uses_narrower_space() {
656 let clusters = vec![
657 cluster(0, 20.0, false),
658 cluster(2, 4.0, true),
659 cluster(3, 20.0, false),
660 cluster(5, 4.0, true),
661 cluster(6, 20.0, false),
662 cluster(8, 4.0, true),
663 cluster(9, 20.0, false),
664 ];
665 let result = TextFlowLayouter::new(
666 "aa bb cc dd".to_string(),
667 TextFlowStyle::default(),
668 clusters,
669 )
670 .compute_layout(32.0, 64.0, (0.0, 0.0));
671 let texts: Vec<String> = result
672 .line_texts
673 .iter()
674 .map(|s| s.trim_end().to_string())
675 .collect();
676 assert_eq!(texts, vec!["aa", "bb cc", "dd"]);
677 assert_eq!(result.spans[0].width(), 24.0);
678 assert_eq!(result.spans[1].width(), 48.0);
679 assert_eq!(result.spans[2].width(), 20.0);
680 }
681
682 #[test]
683 fn first_word_moves_to_next_line_when_first_line_is_narrow() {
684 let clusters = vec![
685 cluster(0, 9.0, false),
686 cluster(1, 9.0, false),
687 cluster(2, 9.0, false),
688 cluster(3, 9.0, false),
689 cluster(4, 9.0, false),
690 cluster(5, 4.0, true),
691 cluster(6, 10.0, false),
692 cluster(7, 10.0, false),
693 cluster(8, 10.0, false),
694 cluster(9, 10.0, false),
695 cluster(10, 10.0, false),
696 ];
697 let result = TextFlowLayouter::new(
698 "Hello world".to_string(),
699 TextFlowStyle::default(),
700 clusters,
701 )
702 .compute_layout(30.0, 100.0, (0.0, 0.0));
703 assert_eq!(result.line_texts, vec!["Hello world"]);
704 assert_eq!(result.spans.len(), 1);
705 assert_eq!(result.spans[0].line_index, 1);
706 assert_eq!(result.spans[0].width(), 99.0);
707 }
708
709 #[test]
710 fn first_word_splits_when_too_wide_for_every_line() {
711 let clusters = vec![
712 cluster(0, 9.0, false),
713 cluster(1, 9.0, false),
714 cluster(2, 9.0, false),
715 cluster(3, 9.0, false),
716 cluster(4, 9.0, false),
717 ];
718 let result = TextFlowLayouter::new("Hello".to_string(), TextFlowStyle::default(), clusters)
719 .compute_layout(30.0, 40.0, (0.0, 0.0));
720 assert_eq!(result.line_texts, vec!["Hell", "o"]);
721 }
722
723 #[test]
724 fn nowrap_never_wraps_on_overflow() {
725 let clusters = vec![
726 cluster(0, 30.0, false),
727 cluster(3, 5.0, true),
728 cluster(4, 30.0, false),
729 cluster(7, 5.0, true),
730 cluster(8, 30.0, false),
731 ];
732 let result = layout_with("aaa bbb ccc", clusters, 50.0, WhiteSpace::Nowrap);
733 assert_eq!(result.line_texts, vec!["aaa bbb ccc"]);
734 assert_eq!(result.spans.len(), 1);
735 assert_eq!(result.spans[0].width(), 100.0);
736 }
737
738 #[test]
739 fn pre_breaks_only_on_newline() {
740 let no_wrap_clusters = vec![
741 cluster(0, 40.0, false),
742 cluster(3, 4.0, true),
743 cluster(4, 40.0, false),
744 cluster(7, 4.0, true),
745 cluster(8, 40.0, false),
746 ];
747 let no_wrap = layout_with("aaa bbb ccc", no_wrap_clusters, 60.0, WhiteSpace::Pre);
748 assert_eq!(no_wrap.line_texts, vec!["aaa bbb ccc"]);
749 assert_eq!(no_wrap.spans.len(), 1);
750 assert_eq!(no_wrap.spans[0].width(), 128.0);
751
752 let newline_clusters = vec![
753 cluster(0, 40.0, false),
754 cluster(4, 4.0, true),
755 cluster(5, 40.0, false),
756 ];
757 let forced = layout_with("aaaa\nbbbb", newline_clusters, 50.0, WhiteSpace::Pre);
758 assert_eq!(forced.line_texts, vec!["aaaa", "bbbb"]);
759 assert_eq!(forced.spans.len(), 2);
760 assert_eq!(forced.spans[0].line_index, 0);
761 assert_eq!(forced.spans[1].line_index, 1);
762 }
763
764 #[test]
765 fn pre_wrap_breaks_on_newline_and_wraps() {
766 let clusters = vec![
767 cluster(0, 20.0, false),
768 cluster(2, 4.0, true),
769 cluster(3, 20.0, false),
770 cluster(5, 4.0, true),
771 cluster(6, 20.0, false),
772 ];
773 let result = layout_with("aa bb cc", clusters, 34.0, WhiteSpace::PreWrap);
774 let texts: Vec<String> = result
775 .line_texts
776 .iter()
777 .map(|s| s.trim_end().to_string())
778 .collect();
779 assert_eq!(texts, vec!["aa", "bb", "cc"]);
780 }
781
782 #[test]
783 fn pre_line_breaks_on_newline_and_wraps() {
784 let clusters = vec![
785 cluster(0, 20.0, false),
786 cluster(2, 4.0, true),
787 cluster(3, 20.0, false),
788 cluster(5, 4.0, true),
789 cluster(6, 20.0, false),
790 ];
791 let result = layout_with("aa bb cc", clusters, 34.0, WhiteSpace::PreLine);
792 let texts: Vec<String> = result
793 .line_texts
794 .iter()
795 .map(|s| s.trim_end().to_string())
796 .collect();
797 assert_eq!(texts, vec!["aa", "bb", "cc"]);
798 assert_eq!(result.spans.len(), 3);
799 }
800
801 #[test]
802 fn break_spaces_wraps_inside_whitespace_run() {
803 let clusters = vec![
804 cluster(0, 20.0, false),
805 cluster(2, 5.0, true),
806 cluster(3, 5.0, true),
807 cluster(4, 20.0, false),
808 cluster(5, 20.0, false),
809 ];
810 let result = layout_with("aa bb", clusters, 25.0, WhiteSpace::BreakSpaces);
811 assert_eq!(result.line_texts, vec!["aa ", " ", "bb"]);
812 assert_eq!(result.spans.len(), 3);
813 }
814
815 #[test]
816 fn leading_newline_emits_empty_first_line() {
817 let clusters = vec![
818 cluster(1, 10.0, false),
819 cluster(2, 10.0, false),
820 cluster(3, 10.0, false),
821 ];
822 let result = layout_with("\nabc", clusters, 100.0, WhiteSpace::PreWrap);
823 assert_eq!(result.line_texts, vec!["", "abc"]);
824 assert_eq!(result.spans.len(), 2);
825 assert_eq!(result.spans[0].line_index, 0);
826 assert_eq!(result.spans[0].width(), 0.0);
827 assert_eq!(result.spans[1].line_index, 1);
828 assert_eq!(result.spans[1].width(), 30.0);
829 }
830
831 #[test]
832 fn trailing_newline_emits_empty_last_line() {
833 let clusters = vec![
834 cluster(0, 10.0, false),
835 cluster(1, 10.0, false),
836 cluster(2, 10.0, false),
837 ];
838 let result = layout_with("abc\n", clusters, 100.0, WhiteSpace::PreWrap);
839 assert_eq!(result.line_texts, vec!["abc", ""]);
841 assert_eq!(result.spans.len(), 2);
842 assert_eq!(result.spans[1].width(), 0.0);
843 }
844
845 #[test]
846 fn leading_and_trailing_newline_emit_empty_lines() {
847 let clusters = vec![
848 cluster(1, 10.0, false),
849 cluster(2, 10.0, false),
850 cluster(3, 10.0, false),
851 ];
852 let result = layout_with("\nabc\n", clusters, 100.0, WhiteSpace::PreWrap);
853 assert_eq!(result.line_texts, vec!["", "abc", ""]);
854 assert_eq!(result.spans.len(), 3);
855 }
856
857 #[test]
858 fn multiple_trailing_newlines_emit_empty_lines() {
859 let clusters = vec![
860 cluster(0, 10.0, false),
861 cluster(1, 10.0, false),
862 cluster(2, 10.0, false),
863 ];
864 let result = layout_with("abc\n\n", clusters, 100.0, WhiteSpace::PreWrap);
865 assert_eq!(result.line_texts, vec!["abc", "", ""]);
866 assert_eq!(result.spans.len(), 3);
867 }
868
869 #[test]
870 fn all_newlines_emit_n_plus_one_lines() {
871 let result = layout_with("\n\n", vec![], 100.0, WhiteSpace::PreWrap);
874 assert_eq!(result.line_texts, vec!["", "", ""]);
875 assert_eq!(result.spans.len(), 3);
876 }
877
878 #[test]
879 fn trailing_newline_ignored_for_normal_whitespace() {
880 let clusters = vec![
881 cluster(0, 10.0, false),
882 cluster(1, 10.0, false),
883 cluster(2, 10.0, false),
884 ];
885 let result = layout_with("abc\n", clusters, 100.0, WhiteSpace::Normal);
888 assert_eq!(result.line_texts, vec!["abc"]);
889 assert_eq!(result.spans.len(), 1);
890 }
891
892 #[test]
893 fn line_after_newline_starts_at_left_edge() {
894 let clusters = vec![
897 cluster(0, 10.0, false),
898 cluster(1, 10.0, false),
899 cluster(2, 10.0, false),
900 cluster(4, 10.0, false),
901 cluster(5, 10.0, false),
902 cluster(6, 10.0, false),
903 ];
904 let flow = TextFlowStyle {
905 white_space: WhiteSpace::PreWrap,
906 ..TextFlowStyle::default()
907 };
908 let result = TextFlowLayouter::new("abc\ndef".to_string(), flow, clusters).compute_layout(
909 100.0,
910 100.0,
911 (0.0, 0.0),
912 );
913 assert_eq!(result.spans.len(), 2);
914 assert_eq!(result.spans[0].line_pos.0, 0.0, "first line x");
915 assert_eq!(result.spans[1].line_pos.0, 0.0, "line after newline x");
916 }
917
918 #[test]
919 fn line_after_newline_resets_to_absolute_zero() {
920 let clusters = vec![
923 cluster(0, 10.0, false),
924 cluster(1, 10.0, false),
925 cluster(2, 10.0, false),
926 cluster(4, 10.0, false),
927 cluster(5, 10.0, false),
928 cluster(6, 10.0, false),
929 ];
930 let flow = TextFlowStyle {
931 white_space: WhiteSpace::PreWrap,
932 ..TextFlowStyle::default()
933 };
934 let result = TextFlowLayouter::new("abc\ndef".to_string(), flow, clusters).compute_layout(
935 100.0,
936 100.0,
937 (50.0, 0.0),
938 );
939 assert_eq!(result.spans.len(), 2);
940 assert_eq!(result.spans[0].line_pos.0, 50.0, "first line x");
941 assert_eq!(result.spans[1].line_pos.0, 0.0, "line after newline x");
942 }
943
944 #[test]
945 fn wrapped_line_starts_at_absolute_zero() {
946 let clusters = vec![
949 cluster(0, 30.0, false),
950 cluster(3, 5.0, true),
951 cluster(4, 30.0, false),
952 ];
953 let result =
954 TextFlowLayouter::new("aaa bbb".to_string(), TextFlowStyle::default(), clusters)
955 .compute_layout(40.0, 40.0, (50.0, 0.0));
956 assert_eq!(result.spans.len(), 2);
957 assert_eq!(result.spans[0].line_pos.0, 50.0, "first line x");
958 assert_eq!(result.spans[1].line_pos.0, 0.0, "wrapped line x");
959 }
960
961 #[test]
962 fn consecutive_newlines_emit_empty_lines() {
963 let clusters = vec![
964 cluster(0, 10.0, false),
965 cluster(1, 10.0, false),
966 cluster(4, 10.0, false),
967 cluster(5, 10.0, false),
968 ];
969 let result = layout_with("ab\n\ncd", clusters, 100.0, WhiteSpace::PreWrap);
970 assert_eq!(result.line_texts, vec!["ab", "", "cd"]);
971 assert_eq!(result.spans.len(), 3);
972 assert_eq!(result.spans[0].width(), 20.0);
973 assert_eq!(result.spans[1].width(), 0.0);
974 assert_eq!(result.spans[2].width(), 20.0);
975 }
976
977 #[test]
978 fn newline_between_paragraphs_keeps_span_width() {
979 let clusters = vec![
980 cluster(0, 15.0, false),
981 cluster(1, 15.0, false),
982 cluster(3, 15.0, false),
983 ];
984 let result = layout_with("ab\nc", clusters, 100.0, WhiteSpace::Pre);
985 assert_eq!(result.line_texts, vec!["ab", "c"]);
986 assert_eq!(result.spans[0].width(), 30.0);
987 assert_eq!(result.spans[1].width(), 15.0);
988 }
989
990 #[test]
991 fn pre_wrap_does_not_split_unbreakable_word() {
992 let clusters: Vec<GlyphCluster> = (0..6).map(|i| cluster(i, 20.0, false)).collect();
993 let result = layout_with("abcdef", clusters, 80.0, WhiteSpace::PreWrap);
994 assert_eq!(result.line_texts, vec!["abcdef"]);
995 assert_eq!(result.spans.len(), 1);
996 assert_eq!(result.spans[0].width(), 120.0);
997 }
998
999 #[test]
1000 fn pre_line_does_not_split_unbreakable_word() {
1001 let clusters: Vec<GlyphCluster> = (0..6).map(|i| cluster(i, 20.0, false)).collect();
1002 let result = layout_with("abcdef", clusters, 80.0, WhiteSpace::PreLine);
1003 assert_eq!(result.line_texts, vec!["abcdef"]);
1004 assert_eq!(result.spans.len(), 1);
1005 }
1006
1007 #[test]
1008 fn break_spaces_does_not_split_unbreakable_word() {
1009 let clusters: Vec<GlyphCluster> = (0..6).map(|i| cluster(i, 20.0, false)).collect();
1010 let result = layout_with("abcdef", clusters, 80.0, WhiteSpace::BreakSpaces);
1011 assert_eq!(result.line_texts, vec!["abcdef"]);
1012 assert_eq!(result.spans.len(), 1);
1013 }
1014
1015 #[test]
1016 fn pre_wrap_wraps_at_whitespace_but_not_inside_word() {
1017 let clusters = vec![
1018 cluster(0, 20.0, false),
1019 cluster(1, 20.0, false),
1020 cluster(2, 20.0, false),
1021 cluster(3, 20.0, false),
1022 cluster(4, 4.0, true),
1023 cluster(5, 15.0, false),
1024 cluster(6, 15.0, false),
1025 ];
1026 let result = layout_with("abcd ef", clusters, 70.0, WhiteSpace::PreWrap);
1027 let texts: Vec<String> = result
1028 .line_texts
1029 .iter()
1030 .map(|s| s.trim_end().to_string())
1031 .collect();
1032 assert_eq!(texts, vec!["abcd", "ef"]);
1033 }
1034
1035 #[test]
1036 fn empty_text_emits_one_empty_line() {
1037 let result = layout("", vec![], 100.0);
1038 assert_eq!(result.line_texts, vec![""]);
1039 assert_eq!(result.spans.len(), 1);
1040 }
1041
1042 #[test]
1043 fn trailing_newlines_match_split_semantics() {
1044 let cases: &[(&str, &[&str])] = &[
1047 ("foo", &["foo"]),
1048 ("foo\n", &["foo", ""]),
1049 ("foo\n\n", &["foo", "", ""]),
1050 ("\n", &["", ""]),
1051 ("\n\n", &["", "", ""]),
1052 ];
1053 for (text, expected) in cases {
1054 let clusters: Vec<GlyphCluster> = text
1055 .chars()
1056 .enumerate()
1057 .filter(|(_, c)| !c.is_whitespace())
1058 .map(|(i, _)| cluster(i, 10.0, false))
1059 .collect();
1060 let result = layout_with(text, clusters, 100.0, WhiteSpace::Pre);
1061 assert_eq!(&result.line_texts[..], *expected, "text = {:?}", text);
1062 }
1063 }
1064
1065 #[test]
1066 fn trailing_newlines_do_not_accumulate_x_pos() {
1067 let clusters = vec![cluster(0, 10.0, false), cluster(1, 10.0, false)];
1071 let flow = TextFlowStyle {
1072 white_space: WhiteSpace::Pre,
1073 text_align: TextAlign::Center,
1074 ..TextFlowStyle::default()
1075 };
1076 let result = TextFlowLayouter::new("ab\n\n".to_string(), flow, clusters).compute_layout(
1077 100.0,
1078 100.0,
1079 (0.0, 0.0),
1080 );
1081
1082 assert_eq!(result.line_texts, vec!["ab", "", ""]);
1083 assert_eq!(result.spans[0].line_pos.0, 40.0);
1085 assert_eq!(result.spans[1].line_pos.0, 50.0);
1088 assert_eq!(result.spans[2].line_pos.0, 50.0);
1089 }
1090
1091 fn ctx_with_unbounded_containing_width(available_inline: f32) -> LayoutContext {
1092 LayoutContext {
1093 containing_block_width: None,
1094 containing_block_height: None,
1095 start_pos: (0.0, 0.0),
1096 available_inline_size: available_inline,
1097 float_space: None,
1098 line_height: 24.0,
1099 viewport_width: 1200.0,
1100 viewport_height: 900.0,
1101 }
1102 }
1103
1104 #[test]
1105 fn subsequent_lines_are_not_capped_by_the_first_line_slice() {
1106 let flow = TextFlowStyle {
1113 text_align: TextAlign::Center,
1114 ..TextFlowStyle::default()
1115 };
1116 let clusters = vec![
1117 cluster(0, 40.0, false),
1118 cluster(4, 5.0, true),
1119 cluster(5, 40.0, false),
1120 cluster(9, 5.0, true),
1121 cluster(10, 40.0, false),
1122 ];
1123 let mut layouter = TextFlowLayouter::new("aaaa bbbb cccc".to_string(), flow, clusters);
1124 let LayoutBox::InlineBox(inline) =
1125 layouter.layout(&ctx_with_unbounded_containing_width(60.0))
1126 else {
1127 panic!("expected InlineBox");
1128 };
1129
1130 assert!(
1133 inline.line_spans.len() >= 2,
1134 "text must wrap on multiple lines"
1135 );
1136 assert_eq!(
1137 inline.line_spans[1].width(),
1138 85.0,
1139 "line 1 was capped by the first line's slice"
1140 );
1141 for span in &inline.line_spans {
1142 assert!(
1143 span.line_pos.0.is_finite(),
1144 "x must be finite, got {}",
1145 span.line_pos.0
1146 );
1147 assert!(
1148 span.line_pos.0 < 1000.0,
1149 "x must stay on-screen, got {}",
1150 span.line_pos.0
1151 );
1152 }
1153 assert_eq!(inline.line_spans[0].line_pos.0, 7.5);
1155 }
1156
1157 #[test]
1158 fn centered_text_with_infinite_first_line_width_keeps_position() {
1159 let flow = TextFlowStyle {
1163 text_align: TextAlign::Center,
1164 ..TextFlowStyle::default()
1165 };
1166 let clusters = vec![cluster(0, 20.0, false), cluster(2, 20.0, false)];
1167 let mut layouter = TextFlowLayouter::new("aaa bbb".to_string(), flow, clusters);
1168 let LayoutBox::InlineBox(inline) =
1169 layouter.layout(&ctx_with_unbounded_containing_width(f32::MAX))
1170 else {
1171 panic!("expected InlineBox");
1172 };
1173
1174 assert_eq!(inline.line_spans.len(), 1);
1175 assert_eq!(inline.line_spans[0].line_pos.0, 0.0);
1176 }
1177
1178 fn float_flow() -> TextFlowStyle {
1179 TextFlowStyle {
1180 line_height: LineHeight::Px(20.0),
1181 ..TextFlowStyle::default()
1182 }
1183 }
1184
1185 fn word_clusters(words: usize, word_width: f32, space_width: f32) -> Vec<GlyphCluster> {
1188 let mut clusters = Vec::new();
1189 for word in 0..words {
1190 let start = word * 5;
1191 clusters.push(cluster(start, word_width, false));
1192 if word + 1 < words {
1193 clusters.push(cluster(start + 4, space_width, true));
1194 }
1195 }
1196 clusters
1197 }
1198
1199 fn ctx_with_float_space(
1200 available_inline: f32,
1201 containing_width: f32,
1202 start_pos: (f32, f32),
1203 float_space: FloatAvoider,
1204 ) -> LayoutContext {
1205 LayoutContext {
1206 containing_block_width: Some(containing_width),
1207 containing_block_height: Some(1000.0),
1208 start_pos,
1209 available_inline_size: available_inline,
1210 float_space: Some(float_space),
1211 line_height: 20.0,
1212 viewport_width: 1200.0,
1213 viewport_height: 900.0,
1214 }
1215 }
1216
1217 const FLOAT_TEXT: &str = "aaaa bbbb cccc dddd eeee ffff gggg hhhh iiii jjjj";
1218
1219 #[test]
1220 fn continuation_line_origin_follows_the_float() {
1221 let clusters = word_clusters(4, 90.0, 10.0);
1226 let avoider =
1227 FloatAvoider::from_rects([(ui_layout::Float::Left, 0.0, 0.0, 60.0, 40.0)], 400.0);
1228 let ctx = ctx_with_float_space(300.0, 400.0, (100.0, 0.0), avoider);
1229 let mut layouter =
1230 TextFlowLayouter::new("aaaa bbbb cccc dddd".to_string(), float_flow(), clusters);
1231 let LayoutBox::InlineBox(inline) = layouter.layout(&ctx) else {
1232 panic!("expected InlineBox");
1233 };
1234
1235 assert_eq!(inline.line_spans[0].line_pos.0, 100.0);
1236 assert!(inline.line_spans.len() >= 2, "text should wrap");
1237 assert_eq!(inline.line_spans[1].line_pos.0, 60.0);
1238 }
1239
1240 #[test]
1241 fn continuation_lines_wrap_beside_a_left_float() {
1242 let clusters = word_clusters(9, 90.0, 10.0);
1246 let avoider =
1247 FloatAvoider::from_rects([(ui_layout::Float::Left, 0.0, 0.0, 100.0, 40.0)], 400.0);
1248 let ctx = ctx_with_float_space(300.0, 400.0, (100.0, 0.0), avoider);
1249 let mut layouter = TextFlowLayouter::new(
1250 "aaaa bbbb cccc dddd eeee ffff gggg hhhh iiii".to_string(),
1251 float_flow(),
1252 clusters,
1253 );
1254 let LayoutBox::InlineBox(inline) = layouter.layout(&ctx) else {
1255 panic!("expected InlineBox");
1256 };
1257
1258 for span in inline.line_spans.iter().filter(|s| s.line_index < 2) {
1261 assert_eq!(
1262 span.line_pos.0, 100.0,
1263 "line {} must clear the float",
1264 span.line_index
1265 );
1266 assert!(
1267 span.width() <= 300.0 + 0.01,
1268 "line {} wrapped too wide ({})",
1269 span.line_index,
1270 span.width()
1271 );
1272 }
1273 let last = inline.line_spans.last().unwrap();
1276 assert!(last.line_index >= 2);
1277 assert_eq!(last.line_pos.0, 0.0);
1278 }
1279
1280 #[test]
1281 fn continuation_lines_wrap_beside_a_right_float() {
1282 let clusters = word_clusters(10, 90.0, 10.0);
1283 let avoider =
1284 FloatAvoider::from_rects([(ui_layout::Float::Right, 300.0, 0.0, 100.0, 40.0)], 400.0);
1285 let ctx = ctx_with_float_space(300.0, 400.0, (0.0, 0.0), avoider);
1286 let mut layouter = TextFlowLayouter::new(FLOAT_TEXT.to_string(), float_flow(), clusters);
1287 let LayoutBox::InlineBox(inline) = layouter.layout(&ctx) else {
1288 panic!("expected InlineBox");
1289 };
1290
1291 for span in inline.line_spans.iter().filter(|s| s.line_index < 2) {
1292 assert_eq!(span.line_pos.0, 0.0);
1293 assert!(
1294 span.width() <= 300.0 + 0.01,
1295 "line {} overlapped the right float: {}",
1296 span.line_index,
1297 span.width()
1298 );
1299 }
1300 let reclaimed = inline
1302 .line_spans
1303 .iter()
1304 .find(|s| s.line_index >= 2)
1305 .expect("a line past the float");
1306 assert!(reclaimed.width() > 300.0);
1307 }
1308
1309 #[test]
1310 fn continuation_lines_start_at_the_containing_block_without_floats() {
1311 let clusters = word_clusters(4, 90.0, 10.0);
1312 let result =
1315 TextFlowLayouter::new("aaaa bbbb cccc dddd".to_string(), float_flow(), clusters)
1316 .compute_layout(120.0, 400.0, (100.0, 0.0));
1317 assert_eq!(result.spans[0].line_pos.0, 100.0);
1318 assert_eq!(result.spans[1].line_pos.0, 0.0);
1319 assert!(result.spans[1].width() > 120.0);
1320 }
1321}