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, 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/// Result of laying out a text chunk into lines.
22#[derive(Debug, Clone)]
23pub struct TextLayoutResult {
24    /// Per-line spans (positions and extents).
25    pub spans: Vec<LineSpan>,
26    /// Per-line text strings (one per span).
27    pub line_texts: Vec<String>,
28}
29
30/// A self-layouting text object that implements [`CustomLayouter`].
31///
32/// Constructed with pre-shaped cluster data from a text measurer.
33/// During layout it wraps text at word boundaries using
34/// `available_inline_size`. Results are cached in a thread-local store
35/// keyed by a unique ID for the rendering layer to consume.
36#[derive(Debug)]
37pub struct TextFlowLayouter {
38    /// Unique identifier for cache lookup.
39    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    /// Resolved line height in pixels, derived from the flow style.
58    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    /// Retrieve the layout result for `id` from the thread-local cache.
68    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        /*
85         * `start_pos` is position of the FIRST LINE.
86         * Don't wrap to the start_pos.0 (which is x).
87         * Wrap to `0.0`.
88         */
89        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        // Edge-case: no clusters but non-empty text (e.g. spaces-only).
109        if clusters.is_empty() && !self.text.is_empty() {
110            if forced_newline && self.text.bytes().all(|b| b == b'\n') {
111                // Every preserved newline is a segment break, so N newlines
112                // produce N + 1 lines.
113                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        // Resolved line widths. The first line may share its line with
142        // preceding inline content, so it can be narrower than subsequent
143        // lines. If a width is unknown (zero at the start of a layout pass
144        // with no prior content), fall back to the other one, or to a large
145        // value so nothing wraps unexpectedly.
146        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; // byte offset where current line starts
174        let mut x_pos = start_pos.0; // Actual line cordination
175        let mut y_pos = start_pos.1;
176        let mut line_index: usize = 0;
177        let mut accumulated: f32 = 0.0; // Running width placed on the current line; used for wrap/overflow checks.
178        let mut last_breakable_cluster: Option<usize> = None; // cluster index (exclusive)
179
180        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                    // Empty line (e.g. consecutive newlines)
226                    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                // A newline before this cluster's glyph means the cluster
243                // starts the new line, so re-process it below.
244                if !nl_before_cluster {
245                    i += 1;
246                }
247                continue;
248            }
249
250            // Check if placing this cluster would overflow the line.
251            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                    // Break at the last known breakable cluster (word boundary).
255                    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                        // Carry over the width of any non-breakable clusters
271                        // that move to the new line together with this one.
272                        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                    // The current line holds a single unbreakable run and the
280                    // next line is wide enough to take the whole word: move it
281                    // there instead of splitting it mid-word. This keeps the
282                    // first word intact when the first line is narrower than
283                    // the following ones.
284                    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                    // Unbreakable run that is wider than the next line as well:
291                    // split at the current cluster boundary.
292                    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            // Accumulate width
307            accumulated += frag.width;
308
309            if frag.break_allowed {
310                last_breakable_cluster = Some(i + 1);
311            }
312
313            i += 1;
314        }
315
316        // Emit the final line(s). For preserved newlines we split the remaining
317        // text exactly on '\n', so every newline yields a line and a trailing
318        // newline adds one final empty line — identical to `str::split('\n')`.
319        // The main loop above already emitted a line for each *internal*
320        // newline; this pass emits the current line plus any lines opened by
321        // trailing newlines, so newline tail handling lives in a single place.
322        if forced_newline {
323            let rest = &self.text[line_start..text_len];
324            if rest.is_empty() {
325                // The text ended on a newline (e.g. "abc\n"): split('\n') still
326                // yields one trailing empty line. This line follows a break, so
327                // its coordinate origin is the box left edge (x_pos = 0).
328                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                    // The first segment uses the incoming `x_pos` (the box
340                    // origin for the first line, or 0 after a prior break); every
341                    // later segment is an empty line following a break, so its
342                    // origin is the box left edge. Resetting `x_pos` here keeps
343                    // the carry-over value from accumulating across segments.
344                    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; // step over the '\n'
354                    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            // Empty text: produce one empty line so the node contributes height
372            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        // "abc\n" has one preserved segment break → "abc" plus an empty line.
767        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        // No glyph clusters: every preserved newline is a segment break, so
799        // N newlines produce N + 1 lines.
800        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        // For Normal whitespace a trailing newline is collapsed, not a forced
813        // break, so no extra empty line is emitted.
814        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        // After a line break, the new line must start at absolute x = 0,
822        // regardless of the text node's position.
823        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        // After a line break, the new line starts at absolute x = 0,
846        // not at the text node's start_pos.x.
847        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        // A width-triggered wrap (no newline) must also resume at absolute
870        // x = 0, not at the text node's start_pos.x.
871        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        // The whole tail is owned by a single split('\n') pass, so the line
968        // count matches `str::split('\n')` exactly.
969        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        // Multiple trailing newlines under center alignment must place every
991        // empty line at the same centered x. The carry-over `x_pos` must reset
992        // to 0 between segments so it does not accumulate across them.
993        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        // First line "ab" is centered: (100 - 20) / 2 == 40.
1005        assert_eq!(result.spans[0].line_pos.0, 40.0);
1006        // Both trailing empty lines are centered at x = 50
1007        // (aligned_x(0.0, 0.0) = (100 - 0) / 2 = 50) — no accumulation.
1008        assert_eq!(result.spans[1].line_pos.0, 50.0);
1009        assert_eq!(result.spans[2].line_pos.0, 50.0);
1010    }
1011}