Skip to main content

orinium_browser/engine/layouter/
text_layouter.rs

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
22/// Whether a line width is a real, constrained measure. The engine uses
23/// `f32::MAX` as an "unbounded" sentinel during measure / shrink-to-fit
24/// passes; alignment against such a width would resolve to `f32::MAX / 2`
25/// and push text off-screen, and wrapping can never happen on it.
26fn is_usable_width(width: f32) -> bool {
27    width.is_finite() && width < f32::MAX / 2.0
28}
29
30/// Result of laying out a text chunk into lines.
31#[derive(Debug, Clone)]
32pub struct TextLayoutResult {
33    /// Per-line spans (positions and extents).
34    pub spans: Vec<LineSpan>,
35    /// Per-line text strings (one per span).
36    pub line_texts: Vec<String>,
37}
38
39/// A self-layouting text object that implements [`CustomLayouter`].
40///
41/// Constructed with pre-shaped cluster data from a text measurer.
42/// During layout it wraps text at word boundaries using
43/// `available_inline_size`. Results are cached in a thread-local store
44/// keyed by a unique ID for the rendering layer to consume.
45#[derive(Debug)]
46pub struct TextFlowLayouter {
47    /// Unique identifier for cache lookup.
48    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    /// Resolved line height in pixels, derived from the flow style.
67    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    /// Retrieve the layout result for `id` from the thread-local cache.
77    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    /// Float-aware [`Self::compute_layout`].
98    ///
99    /// `float_space`, when present, reports how much of each line the floats
100    /// surrounding the object leave free. The first line is positioned by the
101    /// engine (via `start_pos` / `available_first_line_space`); every later
102    /// line is placed at, and wrapped to, the float-free span returned for its
103    /// vertical position. Without this, only the first line of a paragraph
104    /// would clear a float and the rest would overflow it.
105    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        /*
113         * `start_pos` is position of the FIRST LINE.
114         * The first line starts at `start_pos.0` (which may already clear a
115         * float). Subsequent lines start at the containing block's left edge
116         * unless a float narrows them, in which case `float_space` supplies
117         * the per-line origin and width.
118         */
119        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        // Edge-case: no clusters but non-empty text (e.g. spaces-only).
139        if clusters.is_empty() && !self.text.is_empty() {
140            if forced_newline && self.text.bytes().all(|b| b == b'\n') {
141                // Every preserved newline is a segment break, so N newlines
142                // produce N + 1 lines.
143                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        // Resolved line widths. The first line may share its line with
172        // preceding inline content, so it can be narrower than subsequent
173        // lines. If a width is unknown (zero at the start of a layout pass
174        // with no prior content), fall back to the other one, or to a large
175        // value so nothing wraps unexpectedly.
176        let first_line_width = if available_first_line_space > 0.0 {
177            available_first_line_space
178        } else {
179            0.0
180        };
181        // Per-line origin and usable width. The first line starts where the
182        // engine placed it (`start_pos`), which may already clear a float;
183        // later lines start at the containing block's left edge unless a float
184        // still overlaps them, in which case `float_space` supplies the origin
185        // and the width remaining beside the float.
186        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            // With an unbounded line width (sentinel f32::MAX from measure /
202            // shrink-to-fit passes) there is no finite box to center or
203            // right-align within: alignment must not push the text off toward
204            // f32::MAX / 2.
205            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; // byte offset where current line starts
221        let mut x_pos = start_pos.0; // Actual line cordination
222        let mut y_pos = start_pos.1;
223        let mut line_index: usize = 0;
224        let mut accumulated: f32 = 0.0; // Running width placed on the current line; used for wrap/overflow checks.
225        let mut last_breakable_cluster: Option<usize> = None; // cluster index (exclusive)
226
227        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                    // Empty line (e.g. consecutive newlines)
273                    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                // A newline before this cluster's glyph means the cluster
290                // starts the new line, so re-process it below.
291                if !nl_before_cluster {
292                    i += 1;
293                }
294                continue;
295            }
296
297            // Check if placing this cluster would overflow the line.
298            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                    // Break at the last known breakable cluster (word boundary).
302                    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                        // Carry over the width of any non-breakable clusters
318                        // that move to the new line together with this one.
319                        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                    // The current line holds a single unbreakable run and the
327                    // next line is wide enough to take the whole word: move it
328                    // there instead of splitting it mid-word. This keeps the
329                    // first word intact when the first line is narrower than
330                    // the following ones.
331                    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                    // Unbreakable run that is wider than the next line as well:
338                    // split at the current cluster boundary.
339                    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            // Accumulate width
354            accumulated += frag.width;
355
356            if frag.break_allowed {
357                last_breakable_cluster = Some(i + 1);
358            }
359
360            i += 1;
361        }
362
363        // Emit the final line(s). For preserved newlines we split the remaining
364        // text exactly on '\n', so every newline yields a line and a trailing
365        // newline adds one final empty line — identical to `str::split('\n')`.
366        // The main loop above already emitted a line for each *internal*
367        // newline; this pass emits the current line plus any lines opened by
368        // trailing newlines, so newline tail handling lives in a single place.
369        if forced_newline {
370            let rest = &self.text[line_start..text_len];
371            if rest.is_empty() {
372                // The text ended on a newline (e.g. "abc\n"): split('\n') still
373                // yields one trailing empty line. This line follows a break, so
374                // its origin is the line's (float-free) start.
375                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                    // The first segment uses the incoming `x_pos` (the box
387                    // origin for the first line, or 0 after a prior break); every
388                    // later segment is an empty line following a break, so its
389                    // origin is the box left edge. Resetting `x_pos` here keeps
390                    // the carry-over value from accumulating across segments.
391                    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; // step over the '\n'
401                    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            // Empty text: produce one empty line so the node contributes height
419            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        // Every line resolves its own usable span at its own vertical
440        // position. The first line was already positioned and sized by the
441        // engine (`start_pos` + `available_inline_size`), so its width already
442        // accounts for floats and any inline content that shares the line.
443        // Later lines either ask `float_space` how much room the floats leave
444        // at their `y`, or use the containing block width directly. The
445        // containing block width must NEVER be approximated by the first
446        // line's remaining slice: text that started mid-line would otherwise
447        // keep wrapping inside that narrow slice forever. When the width is
448        // indeterminate (measure / shrink-to-fit passes) lines are left
449        // unwrapped and the engine re-lays out with a definite width later.
450        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        // The box width is the widest *line content* — alignment offsets
470        // (center/right within the line) must not widen the inline box,
471        // otherwise an auto-width inline-block containing centered text
472        // shrink-wraps to the alignment offset instead of the text.
473        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        // "abc\n" has one preserved segment break → "abc" plus an empty line.
840        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        // No glyph clusters: every preserved newline is a segment break, so
872        // N newlines produce N + 1 lines.
873        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        // For Normal whitespace a trailing newline is collapsed, not a forced
886        // break, so no extra empty line is emitted.
887        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        // After a line break, the new line must start at absolute x = 0,
895        // regardless of the text node's position.
896        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        // After a line break, the new line starts at absolute x = 0,
921        // not at the text node's start_pos.x.
922        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        // A width-triggered wrap (no newline) must also resume at absolute
947        // x = 0, not at the text node's start_pos.x.
948        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        // The whole tail is owned by a single split('\n') pass, so the line
1045        // count matches `str::split('\n')` exactly.
1046        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        // Multiple trailing newlines under center alignment must place every
1068        // empty line at the same centered x. The carry-over `x_pos` must reset
1069        // to 0 between segments so it does not accumulate across them.
1070        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        // First line "ab" is centered: (100 - 20) / 2 == 40.
1084        assert_eq!(result.spans[0].line_pos.0, 40.0);
1085        // Both trailing empty lines are centered at x = 50
1086        // (aligned_x(0.0, 0.0) = (100 - 0) / 2 = 50) — no accumulation.
1087        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        // GitHub-style hero subheading: containing_block_width is None inside a
1107        // centered flex item, and the first line only has a 60px slice left
1108        // (e.g. after a float or preceding inline content). That slice belongs
1109        // to the first line alone: later lines must not be wrapped inside it
1110        // forever. With an unbounded containing width they do not wrap at all,
1111        // and center alignment must never resolve to f32::MAX / 2.
1112        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        // The first line wraps inside its genuine 60px slice; the next line
1131        // starts fresh and is not constrained by it.
1132        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        // Line 0 ("aaaa ", 45px wide) is centered within its 60px slice.
1154        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        // Measure / shrink-to-fit passes hand out an unbounded inline size.
1160        // There is no finite box to center within, so alignment must not shift
1161        // the text toward f32::MAX / 2.
1162        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    /// One cluster per word (4 bytes) plus a breakable 1-byte space between
1186    /// them, matching `"aaaa bbbb ..."`.
1187    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        // The engine places the first line at `start_pos` (here x=100, e.g.
1222        // after preceding inline content), but the float only reaches x=60.
1223        // The second line must start at the float's right edge (60), not repeat
1224        // the first line's x.
1225        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        // A 100px-wide, 40px-tall left float in a 400px box leaves 300px for
1243        // the first two lines (y = 0 and 20); from y = 40 onward the full
1244        // width is usable again.
1245        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        // Lines overlapping the 40px-tall float start after it and wrap within
1259        // the 300px the float leaves.
1260        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        // Past the float the line is laid out from the containing block's left
1274        // edge instead of after the float.
1275        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        // The line past the float may use the whole 400px containing block.
1301        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        // Narrow first line but no floats: later lines use the full width and
1313        // start at the containing block's left edge.
1314        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}