Skip to main content

ferroui_base/media/text_formatting/
text_formatter_impl.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use crate::media::text_formatting::formatting_object_pool::{FormattingObjectPool, RentedList};
5use crate::media::text_formatting::text_line_impl::TextLineImpl;
6use crate::media::text_formatting::text_run_cache::CachedShapingResult;
7use crate::media::text_formatting::unicode::{
8    BidiAlgorithm, BidiData, Codepoint, GraphemeEnumerator, LineBreak, LineBreakEnumerator,
9};
10use crate::media::text_formatting::wrapping_text_line_break::WrappingTextLineBreak;
11use crate::media::text_formatting::{
12    GlyphInfo, ITextSource, ShapedBuffer, ShapedTextRun, SplitResult, TextCharacters,
13    TextFormatter, TextLine, TextLineBreak, TextParagraphProperties, TextRun, TextRunCache, TextRunProperties,
14    TextShaper, TextShaperOptions, UnshapedTextRun, DEFAULT_TEXT_SOURCE_LENGTH,
15};
16use crate::media::{FlowDirection, FontManager, TextWrapping};
17use crate::utilities::{MathUtilities, ReadOnlyMemory};
18
19const DEFAULT_TEXT: [u16; DEFAULT_TEXT_SOURCE_LENGTH as usize] = [b'a' as u16; DEFAULT_TEXT_SOURCE_LENGTH as usize];
20
21crate::ferro_thread_local! {
22    static EMPTY: ReadOnlyMemory<u16> = ReadOnlyMemory::from_vec(vec![b' ' as u16]);
23
24    /// The bidi scratch state of the thread; reused by every formatted line.
25    static BIDI: RefCell<Option<Box<(BidiData, BidiAlgorithm)>>> = const { RefCell::new(None) };
26}
27
28/// The text formatter.
29///
30/// Internal upstream; public so that the Skia unit tests reach it.
31pub struct TextFormatterImpl;
32
33impl TextFormatterImpl {
34    pub fn new() -> Self {
35        Self
36    }
37
38    /// Formats a line from cached shaped runs, skipping shaping and bidi processing.
39    fn format_line_from_cache(
40        cached: &CachedShapingResult,
41        first_text_source_index: i32,
42        paragraph_width: f64,
43        paragraph_properties: &Rc<dyn TextParagraphProperties>,
44        object_pool: &FormattingObjectPool,
45    ) -> Rc<TextLineImpl> {
46        let resolved_flow_direction = cached.resolved_flow_direction;
47
48        let next_line_break = cached
49            .text_end_of_line
50            .as_ref()
51            .map(|text_end_of_line| Rc::new(TextLineBreak::new(Some(text_end_of_line.clone()), resolved_flow_direction, false)));
52
53        match paragraph_properties.text_wrapping() {
54            TextWrapping::NoWrap => {
55                let line_runs = Self::add_ref_shaped_runs(&cached.shaped_runs);
56
57                let text_line = TextLineImpl::new(
58                    line_runs,
59                    first_text_source_index,
60                    cached.text_source_length,
61                    paragraph_width,
62                    paragraph_properties.clone(),
63                    resolved_flow_direction,
64                    next_line_break,
65                    false,
66                );
67
68                text_line.finalize_line();
69
70                text_line
71            }
72            TextWrapping::WrapWithOverflow | TextWrapping::Wrap => {
73                let mut runs = Self::add_ref_shaped_runs(&cached.shaped_runs);
74
75                Self::perform_text_wrapping(
76                    &mut runs,
77                    false,
78                    first_text_source_index,
79                    paragraph_width,
80                    paragraph_properties,
81                    resolved_flow_direction,
82                    next_line_break.as_ref(),
83                    object_pool,
84                )
85            }
86        }
87    }
88
89    /// Produces a list of text runs for a line, adding an extra reference to each
90    /// [`ShapedTextRun`] so that the caller owns a disposable reference.
91    fn add_ref_shaped_runs(runs: &[Rc<dyn TextRun>]) -> Vec<Rc<dyn TextRun>> {
92        let mut result = Vec::with_capacity(runs.len());
93
94        for run in runs {
95            if let Some(shaped) = run.downcast_ref::<ShapedTextRun>() {
96                shaped.add_reference();
97            }
98
99            result.push(run.clone());
100        }
101
102        result
103    }
104
105    /// Split a sequence of runs into two segments at specified length.
106    ///
107    /// Both lists of the result are rented from `object_pool` and have to be
108    /// returned to it.
109    pub(crate) fn split_text_runs(
110        text_runs: &[Rc<dyn TextRun>],
111        length: i32,
112        object_pool: &FormattingObjectPool,
113    ) -> SplitResult<RentedList<Rc<dyn TextRun>>> {
114        Self::split_text_runs_with_length(text_runs, length, object_pool).0
115    }
116
117    /// Split a sequence of runs into two segments at specified length. The actual
118    /// length of the first segment (which may differ from `length`
119    /// when the split lands on a cluster boundary) is returned as the second
120    /// value. This lets the wrap caller avoid a separate
121    /// second pass to sum run lengths.
122    // Internal upstream; public so the Skia unit tests reach it.
123    pub fn split_text_runs_with_length(
124        text_runs: &[Rc<dyn TextRun>],
125        length: i32,
126        object_pool: &FormattingObjectPool,
127    ) -> (SplitResult<RentedList<Rc<dyn TextRun>>>, i32) {
128        if length == 0 {
129            let mut second = object_pool.text_run_lists.rent();
130
131            second.extend(text_runs.iter().cloned());
132
133            return (SplitResult::new(None, Some(second)), 0);
134        }
135
136        let mut first = object_pool.text_run_lists.rent();
137        let mut current_length = 0;
138
139        for i in 0..text_runs.len() {
140            let current_run = &text_runs[i];
141            let current_run_length = current_run.length();
142
143            if current_length + current_run_length < length {
144                current_length += current_run_length;
145
146                continue;
147            }
148
149            let first_count = if current_run_length >= 1 { i + 1 } else { i };
150
151            if first_count > 1 {
152                first.extend(text_runs[..i].iter().cloned());
153            }
154
155            let mut second_count = text_runs.len() - first_count;
156
157            if current_length + current_run_length == length {
158                let second = if second_count > 0 {
159                    let mut second = object_pool.text_run_lists.rent();
160
161                    let offset = if current_run_length >= 1 { 1 } else { 0 };
162
163                    for j in 0..second_count {
164                        second.push(text_runs[i + j + offset].clone());
165                    }
166
167                    Some(second)
168                } else {
169                    None
170                };
171
172                first.push(current_run.clone());
173
174                return (SplitResult::new(Some(first), second), current_length + current_run_length);
175            } else {
176                second_count += 1;
177
178                let mut second = object_pool.text_run_lists.rent();
179                let mut added_first_length = 0;
180                let trailing_loop_start;
181
182                if let Some(shaped_text_characters) = current_run.downcast_ref::<ShapedTextRun>() {
183                    let split = shaped_text_characters.split(length - current_length);
184
185                    if let Some(split_first) = split.first {
186                        added_first_length = split_first.length();
187                        first.push(split_first);
188                    }
189
190                    if let Some(split_second) = split.second {
191                        second.push(split_second);
192                    }
193
194                    // The split produced fresh ShapedTextRuns for each half, so the
195                    // caller's reference to the original is no longer needed — release it.
196                    shaped_text_characters.dispose();
197
198                    // current_run is consumed by the split; the trailing loop adds the
199                    // runs *after* it.
200                    trailing_loop_start = 1;
201                } else if current_length == 0 {
202                    // Non-splittable run at the very start of the list, asked to split
203                    // strictly inside it. Snapping before would leave first empty and
204                    // the wrap caller would loop forever — same situation as the wrap
205                    // algorithm's "include at least one cluster" overflow rule. Place
206                    // current_run in first and let the line overflow.
207                    first.push(current_run.clone());
208                    added_first_length = current_run_length;
209                    trailing_loop_start = 1;
210                } else {
211                    // Non-splittable run at the split point. Snap the boundary BEFORE
212                    // it: current_run goes into second along with the remaining runs,
213                    // first ends at current_length (shorter than requested but
214                    // content-preserving). Without this branch the run would be
215                    // dropped from both halves.
216                    trailing_loop_start = 0;
217                }
218
219                for j in trailing_loop_start..second_count {
220                    second.push(text_runs[i + j].clone());
221                }
222
223                return (SplitResult::new(Some(first), Some(second)), current_length + added_first_length);
224            }
225        }
226
227        first.extend(text_runs.iter().cloned());
228
229        (SplitResult::new(Some(first), None), current_length)
230    }
231
232    /// Shape specified text runs with specified paragraph embedding.
233    ///
234    /// Returns a rented list of shaped text characters and the resolved flow
235    /// direction.
236    fn shape_text_runs(
237        text_runs: &[Rc<dyn TextRun>],
238        paragraph_properties: &dyn TextParagraphProperties,
239        object_pool: &FormattingObjectPool,
240        font_manager: &FontManager,
241    ) -> (RentedList<Rc<dyn TextRun>>, FlowDirection) {
242        let flow_direction = paragraph_properties.flow_direction();
243        let mut shaped_runs = object_pool.text_run_lists.rent();
244
245        if text_runs.is_empty() {
246            return (shaped_runs, flow_direction);
247        }
248
249        // The scratch state is taken out of its slot while it is used, so no
250        // borrow is held across the calls into fonts, runs and the shaper.
251        let mut bidi = BIDI
252            .with(|slot| slot.borrow_mut().take())
253            .unwrap_or_else(|| Box::new((BidiData::new(), BidiAlgorithm::new())));
254
255        let (bidi_data, bidi_algorithm) = &mut *bidi;
256
257        bidi_data.reset();
258        bidi_data.set_paragraph_embedding_level(flow_direction as i8);
259
260        for text_run in text_runs {
261            let text = text_run.text_span();
262
263            if !text.is_empty() {
264                bidi_data.append(text);
265            } else if text_run.length() == DEFAULT_TEXT_SOURCE_LENGTH {
266                bidi_data.append(&DEFAULT_TEXT);
267            } else {
268                bidi_data.append(&vec![b'a' as u16; text_run.length().max(0) as usize]);
269            }
270        }
271
272        bidi_algorithm.process(bidi_data);
273
274        let resolved_embedding_level = bidi_algorithm.resolve_embedding_level(bidi_data.classes());
275
276        let resolved_flow_direction =
277            if (resolved_embedding_level & 1) == 0 { FlowDirection::LeftToRight } else { FlowDirection::RightToLeft };
278
279        let mut processed_runs = object_pool.text_run_lists.rent();
280        let mut grouped_runs = object_pool.unshaped_text_run_lists.rent();
281
282        Self::coalesce_levels(text_runs, bidi_algorithm.resolved_levels(), font_manager, &mut processed_runs);
283
284        bidi_data.reset();
285        bidi_algorithm.reset();
286
287        BIDI.with(|slot| *slot.borrow_mut() = Some(bidi));
288
289        let text_shaper = TextShaper::current();
290
291        let mut index = 0;
292
293        while index < processed_runs.len() {
294            let current_run = &processed_runs[index];
295
296            match current_run.clone().downcast_rc::<UnshapedTextRun>() {
297                Some(mut shapeable_run) => {
298                    grouped_runs.clear();
299                    grouped_runs.push(shapeable_run.clone());
300
301                    let mut text = shapeable_run.text();
302                    let properties = shapeable_run.run_properties().clone();
303
304                    while index + 1 < processed_runs.len() {
305                        let Some(next_run) = processed_runs[index + 1].clone().downcast_rc::<UnshapedTextRun>() else {
306                            break;
307                        };
308
309                        if shapeable_run.bidi_level() == next_run.bidi_level() {
310                            if let Some(joined_text) = Self::try_join_contiguous_memories(&text, &next_run.text()) {
311                                if Self::can_shape_together(&*properties, &**next_run.run_properties()) {
312                                    grouped_runs.push(next_run.clone());
313                                    index += 1;
314                                    shapeable_run = next_run;
315                                    text = joined_text;
316                                    continue;
317                                }
318                            }
319                        }
320
321                        break;
322                    }
323
324                    let shaper_options = TextShaperOptions::with_all(
325                        properties.cached_glyph_typeface(),
326                        properties.font_rendering_em_size(),
327                        shapeable_run.bidi_level(),
328                        properties.culture_info().cloned(),
329                        paragraph_properties.default_incremental_tab(),
330                        paragraph_properties.letter_spacing(),
331                        properties.font_features().map(|features| Rc::new(features.to_vec())),
332                    );
333
334                    Self::shape_together(&grouped_runs, &text, &shaper_options, &text_shaper, &mut shaped_runs);
335                }
336                None => {
337                    shaped_runs.push(current_run.clone());
338                }
339            }
340
341            index += 1;
342        }
343
344        object_pool.text_run_lists.return_list(processed_runs);
345        object_pool.unshaped_text_run_lists.return_list(grouped_runs);
346
347        (shaped_runs, resolved_flow_direction)
348    }
349
350    /// Tries to join two potentially contiguous memory regions.
351    ///
352    /// Returns a memory region representing the union of the two regions when
353    /// they are contiguous parts of the same buffer.
354    fn try_join_contiguous_memories(x: &ReadOnlyMemory<u16>, y: &ReadOnlyMemory<u16>) -> Option<ReadOnlyMemory<u16>> {
355        if !x.shares_owner_with(y) {
356            return None;
357        }
358
359        let owner = x.owner()?;
360
361        let x_range = (x.offset_in_owner(), x.len());
362        let y_range = (y.offset_in_owner(), y.len());
363
364        let (first_range, second_range) = if x_range.0 <= y_range.0 { (x_range, y_range) } else { (y_range, x_range) };
365
366        if first_range.0 + first_range.1 == second_range.0 {
367            return Some(ReadOnlyMemory::with_range(owner.clone(), first_range.0, x_range.1 + y_range.1));
368        }
369
370        None
371    }
372
373    fn can_shape_together(x: &dyn TextRunProperties, y: &dyn TextRunProperties) -> bool {
374        MathUtilities::are_close(x.font_rendering_em_size(), y.font_rendering_em_size())
375            && x.typeface() == y.typeface()
376            && x.baseline_alignment() == y.baseline_alignment()
377    }
378
379    fn shape_together(
380        text_runs: &[Rc<UnshapedTextRun>],
381        text: &ReadOnlyMemory<u16>,
382        options: &TextShaperOptions,
383        text_shaper: &TextShaper,
384        results: &mut Vec<Rc<dyn TextRun>>,
385    ) {
386        let mut shaped_buffer = text_shaper.shape_text(text, options);
387
388        let mut previous_length = 0;
389
390        for current_run in text_runs {
391            let split_result = shaped_buffer.split(previous_length + current_run.length());
392
393            // Split by text, not by glyph count: a run can legitimately shape to no glyphs at
394            // all and still own its characters. Shapers drop default ignorables that the font
395            // cannot hide behind a space glyph, so a run holding nothing but a line break can
396            // come back empty. Skipping it there would delete its characters from the line and
397            // leave the caller stuck at the same text position.
398            match split_result.first {
399                Some(first) if !first.text().is_empty() => {
400                    previous_length = 0;
401
402                    results.push(Rc::new(ShapedTextRun::new(first, current_run.run_properties().clone())));
403                }
404                _ => {
405                    previous_length += current_run.length();
406                }
407            }
408
409            match split_result.second {
410                None => return,
411                Some(second) => shaped_buffer = second,
412            }
413        }
414    }
415
416    /// Coalesces ranges of the same bidi level to form [`UnshapedTextRun`]s.
417    ///
418    /// * `text_characters` — the text characters to form unshaped runs from.
419    /// * `levels` — the bidi levels (one per codepoint).
420    /// * `font_manager` — the font manager to use.
421    /// * `processed_runs` — a list that will be filled with the processed runs.
422    fn coalesce_levels(
423        text_characters: &[Rc<dyn TextRun>],
424        levels: &[i8],
425        font_manager: &FontManager,
426        processed_runs: &mut Vec<Rc<dyn TextRun>>,
427    ) {
428        if levels.is_empty() {
429            return;
430        }
431
432        let mut level_index = 0usize;
433        let mut run_level = levels[0];
434
435        let mut previous_properties: Option<Rc<dyn TextRunProperties>> = None;
436        let mut current_run: Option<&TextCharacters> = None;
437        let mut run_text: ReadOnlyMemory<u16> = ReadOnlyMemory::empty();
438
439        for text_run in text_characters {
440            let mut j = 0usize;
441            current_run = text_run.downcast_ref::<TextCharacters>();
442
443            let Some(current_run) = current_run else {
444                processed_runs.push(text_run.clone());
445
446                level_index += text_run.length().max(0) as usize;
447
448                continue;
449            };
450
451            run_text = current_run.text();
452
453            while j < run_text.len() {
454                let (_, count) = Codepoint::read_at(run_text.span(), j);
455
456                if level_index + 1 == levels.len() {
457                    break;
458                }
459
460                level_index += 1;
461                j += count;
462
463                if j == run_text.len() {
464                    current_run.get_shapeable_characters(
465                        run_text.slice(0, j),
466                        run_level,
467                        font_manager,
468                        &mut previous_properties,
469                        processed_runs,
470                    );
471
472                    run_level = levels[level_index];
473
474                    continue;
475                }
476
477                if levels[level_index] == run_level {
478                    continue;
479                }
480
481                // End of this run
482                current_run.get_shapeable_characters(
483                    run_text.slice(0, j),
484                    run_level,
485                    font_manager,
486                    &mut previous_properties,
487                    processed_runs,
488                );
489
490                run_text = run_text.slice_from(j);
491
492                j = 0;
493
494                // Move to next run
495                run_level = levels[level_index];
496            }
497        }
498
499        let Some(current_run) = current_run else {
500            return;
501        };
502
503        if run_text.is_empty() {
504            return;
505        }
506
507        current_run.get_shapeable_characters(run_text, run_level, font_manager, &mut previous_properties, processed_runs);
508    }
509
510    /// Fetches text runs.
511    ///
512    /// Returns the (rented) formatted text runs, the end of line, if any, and
513    /// the processed text source length.
514    fn fetch_text_runs(
515        text_source: &dyn ITextSource,
516        first_text_source_index: i32,
517        object_pool: &FormattingObjectPool,
518    ) -> (RentedList<Rc<dyn TextRun>>, Option<Rc<dyn TextRun>>, i32) {
519        let mut text_source_length = 0;
520
521        let mut end_of_line = None;
522
523        let mut text_runs = object_pool.text_run_lists.rent();
524
525        let mut text_run_enumerator = TextRunEnumerator::new(text_source, first_text_source_index);
526
527        while let Some(text_run) = text_run_enumerator.move_next() {
528            if let Some(text_end_of_line) = text_run.as_text_end_of_line() {
529                text_source_length += text_end_of_line.length();
530
531                end_of_line = Some(text_run.clone());
532
533                text_runs.push(text_run);
534
535                break;
536            }
537
538            if let Some(text_characters) = text_run.downcast_ref::<TextCharacters>() {
539                if let Some(run_line_break) = Self::try_get_line_break(text_characters) {
540                    let split_result = TextCharacters::new(
541                        text_characters.text().slice(0, run_line_break.position_wrap()),
542                        text_characters.run_properties().clone(),
543                    );
544
545                    text_runs.push(Rc::new(split_result));
546
547                    text_source_length += run_line_break.position_wrap() as i32;
548
549                    return (text_runs, end_of_line, text_source_length);
550                }
551            }
552
553            text_source_length += text_run.length();
554
555            text_runs.push(text_run);
556        }
557
558        (text_runs, end_of_line, text_source_length)
559    }
560
561    fn try_get_line_break(text_run: &dyn TextRun) -> Option<LineBreak> {
562        let text = text_run.text_span();
563
564        if text.is_empty() {
565            return None;
566        }
567
568        let mut line_break_enumerator = LineBreakEnumerator::new(text);
569
570        while let Some(line_break) = line_break_enumerator.move_next() {
571            if !line_break.required() {
572                continue;
573            }
574
575            return Some(line_break);
576        }
577
578        None
579    }
580
581    fn measure_length(text_runs: &[Rc<dyn TextRun>], paragraph_width: f64) -> i32 {
582        let mut measured_length = 0;
583        let mut current_width = 0.0;
584
585        for (run_index, current_run) in text_runs.iter().enumerate() {
586            if let Some(shaped_text_characters) = current_run.downcast_ref::<ShapedTextRun>() {
587                // cluster-width prefix sum lets us answer "how much fits"
588                // in O(log clusters) instead of walking every glyph. The total
589                // advance and the per-cluster start char are cached on the
590                // ShapedBuffer (which lives in the run cache), so the first
591                // layout pays the O(glyphs) cost and every subsequent layout
592                // is constant-time.
593                let buffer = shaped_text_characters.shaped_buffer();
594
595                if buffer.length() == 0 {
596                    continue;
597                }
598
599                let remaining = paragraph_width - current_width;
600                let buffer_width = buffer.total_glyph_advance();
601
602                if !MathUtilities::greater_than(buffer_width, remaining) {
603                    // Whole buffer fits: consume it and continue to the next run.
604                    current_width += buffer_width;
605                    measured_length += current_run.length();
606                    continue;
607                }
608
609                // Some part of the buffer overflows: find the cluster boundary.
610                let mut run_length = buffer.find_leading_char_count_within_width(remaining);
611
612                // "Include at least one cluster" rule preserves the existing
613                // contract that the caller always advances by at least one
614                // grapheme even when the first cluster overflows the line.
615                if run_length == 0 && measured_length == 0 {
616                    run_length = buffer.first_cluster_char_length();
617                }
618
619                measured_length += run_length;
620
621                if run_index < text_runs.len() - 1 && run_length == current_run.length() {
622                    if let Some(end_of_line) = text_runs[run_index + 1].as_text_end_of_line() {
623                        measured_length += end_of_line.length();
624                    }
625                }
626
627                return measured_length;
628            } else if let Some(drawable_text_run) = current_run.as_drawable() {
629                if MathUtilities::greater_than(current_width + drawable_text_run.size().width, paragraph_width) {
630                    return measured_length;
631                }
632
633                measured_length += current_run.length();
634                current_width += drawable_text_run.size().width;
635            } else {
636                measured_length += current_run.length();
637            }
638        }
639
640        measured_length
641    }
642
643    /// Creates an empty text line.
644    pub fn create_empty_text_line(
645        first_text_source_index: i32,
646        paragraph_width: f64,
647        paragraph_properties: &Rc<dyn TextParagraphProperties>,
648    ) -> Rc<TextLineImpl> {
649        let flow_direction = paragraph_properties.flow_direction();
650        let properties = paragraph_properties.default_text_run_properties();
651        let glyph_typeface = properties.cached_glyph_typeface();
652        let glyph = glyph_typeface.character_to_glyph_map().get_glyph(b' ' as i32);
653
654        let shaped_buffer = ShapedBuffer::new(
655            EMPTY.with(|empty| empty.clone()),
656            1,
657            glyph_typeface,
658            properties.font_rendering_em_size(),
659            flow_direction as i8,
660        );
661        shaped_buffer.set(0, GlyphInfo::new(glyph, first_text_source_index, 0.0));
662
663        let text_runs: Vec<Rc<dyn TextRun>> = vec![Rc::new(ShapedTextRun::new(shaped_buffer, properties.clone()))];
664
665        let line = TextLineImpl::new(
666            text_runs,
667            first_text_source_index,
668            0,
669            paragraph_width,
670            paragraph_properties.clone(),
671            flow_direction,
672            None,
673            false,
674        );
675
676        line.finalize_line();
677
678        line
679    }
680
681    /// Performs text wrapping and returns the wrapped text line.
682    ///
683    /// * `text_runs` — the text runs to wrap.
684    /// * `can_reuse_text_run_list` — whether `text_runs` can be reused to store the split runs.
685    /// * `first_text_source_index` — the first text source index.
686    /// * `paragraph_width` — the paragraph width.
687    /// * `paragraph_properties` — the text paragraph properties.
688    /// * `current_line_break` — the current line break if the line was explicitly broken.
689    /// * `object_pool` — a pool used to get reusable formatting objects.
690    #[allow(clippy::too_many_arguments)]
691    fn perform_text_wrapping(
692        text_runs: &mut Vec<Rc<dyn TextRun>>,
693        can_reuse_text_run_list: bool,
694        first_text_source_index: i32,
695        paragraph_width: f64,
696        paragraph_properties: &Rc<dyn TextParagraphProperties>,
697        resolved_flow_direction: FlowDirection,
698        current_line_break: Option<&Rc<TextLineBreak>>,
699        object_pool: &FormattingObjectPool,
700    ) -> Rc<TextLineImpl> {
701        if text_runs.is_empty() {
702            return Self::create_empty_text_line(first_text_source_index, paragraph_width, paragraph_properties);
703        }
704
705        let measured_length = Self::find_wrap_length(text_runs, paragraph_width, &**paragraph_properties);
706
707        let (split, split_length) = Self::split_text_runs_with_length(text_runs, measured_length, object_pool);
708
709        let (mut pre_split_runs, mut post_split_runs) = split.deconstruct();
710
711        let has_post_split_runs = post_split_runs.as_ref().is_some_and(|runs| !runs.is_empty());
712
713        let text_line_break = match &mut post_split_runs {
714            Some(post_split_runs) if has_post_split_runs => {
715                let post_split_count = post_split_runs.len();
716
717                // reuse the list as much as possible:
718                // if can_reuse_text_run_list == true it's coming from previous remaining runs
719                let mut remaining_runs = if can_reuse_text_run_list {
720                    let mut remaining_runs = std::mem::take(text_runs);
721                    remaining_runs.clear();
722                    // ensure capacity up front so pushing does not resize mid-loop.
723                    remaining_runs.reserve(post_split_count);
724                    remaining_runs
725                } else {
726                    Vec::with_capacity(post_split_count)
727                };
728
729                // The runs are moved out of the rented list, which is returned below.
730                remaining_runs.append(post_split_runs);
731
732                Some(Rc::new(WrappingTextLineBreak::new(None, resolved_flow_direction, remaining_runs)))
733            }
734            _ => current_line_break.and_then(|line_break| line_break.text_end_of_line()).map(|text_end_of_line| {
735                Rc::new(TextLineBreak::new(Some(text_end_of_line.clone()), resolved_flow_direction, false))
736            }),
737        };
738
739        let text_line = match &mut pre_split_runs {
740            None => Self::create_empty_text_line(first_text_source_index, paragraph_width, paragraph_properties),
741            Some(pre_split_runs) => {
742                if has_post_split_runs {
743                    Self::reset_trailing_whitespace_bidi_levels(
744                        pre_split_runs,
745                        paragraph_properties.flow_direction(),
746                        object_pool,
747                    );
748                }
749
750                // split_text_runs has already computed the actual length of the first
751                // segment (the cluster boundary may land slightly off the requested
752                // length), so we just need to materialise the run list for the line:
753                // no second-pass length sum required.
754                let remaining_text_runs: Vec<Rc<dyn TextRun>> = pre_split_runs.drain(..).collect();
755
756                let text_line = TextLineImpl::new(
757                    remaining_text_runs,
758                    first_text_source_index,
759                    split_length,
760                    paragraph_width,
761                    paragraph_properties.clone(),
762                    resolved_flow_direction,
763                    text_line_break,
764                    false,
765                );
766
767                text_line.finalize_line();
768
769                text_line
770            }
771        };
772
773        object_pool.text_run_lists.return_optional(pre_split_runs);
774        object_pool.text_run_lists.return_optional(post_split_runs);
775
776        text_line
777    }
778
779    /// The first part of upstream's `PerformTextWrapping`: finds the length
780    /// (in text source positions) of the wrapped line.
781    fn find_wrap_length(
782        text_runs: &[Rc<dyn TextRun>],
783        paragraph_width: f64,
784        paragraph_properties: &dyn TextParagraphProperties,
785    ) -> i32 {
786        let mut measured_length = Self::measure_length(text_runs, paragraph_width);
787
788        if measured_length == 0 {
789            if paragraph_properties.text_wrapping() == TextWrapping::NoWrap {
790                for text_run in text_runs {
791                    measured_length += text_run.length();
792                }
793            } else {
794                let first_run = &text_runs[0];
795
796                if first_run.is::<ShapedTextRun>() {
797                    let mut grapheme_enumerator = GraphemeEnumerator::new(first_run.text_span());
798
799                    measured_length = match grapheme_enumerator.move_next() {
800                        Some(grapheme) => grapheme.length() as i32,
801                        None => 1,
802                    };
803                } else {
804                    measured_length = first_run.length();
805                }
806            }
807        }
808
809        let mut current_length = 0;
810
811        let mut last_wrap_position = 0;
812
813        let mut current_position = 0;
814
815        let wrapping_mode = paragraph_properties.text_wrapping();
816        let run_count = text_runs.len();
817
818        let mut index = 0;
819
820        while index < run_count {
821            let mut break_found = false;
822
823            let mut current_run = &text_runs[index];
824            let mut current_run_length = current_run.length();
825
826            if current_run.is::<ShapedTextRun>() {
827                let mut line_breaker = LineBreakEnumerator::new(current_run.text_span());
828
829                while let Some(mut line_break) = line_breaker.move_next() {
830                    if line_break.required() && current_length + line_break.position_measure() as i32 <= measured_length {
831                        // Explicit break found
832                        break_found = true;
833
834                        current_position = current_length + line_break.position_wrap() as i32;
835
836                        break;
837                    }
838
839                    if current_length + line_break.position_measure() as i32 > measured_length {
840                        if wrapping_mode == TextWrapping::WrapWithOverflow {
841                            if last_wrap_position > 0 {
842                                current_position = last_wrap_position;
843
844                                break_found = true;
845
846                                break;
847                            }
848
849                            // Find next possible wrap position (overflow)
850                            if index < run_count - 1 {
851                                if line_break.position_wrap() as i32 != current_run_length {
852                                    // We already found the next possible wrap position.
853                                    break_found = true;
854
855                                    current_position = current_length + line_break.position_wrap() as i32;
856
857                                    break;
858                                }
859
860                                while let Some(next_line_break) = line_breaker.move_next() {
861                                    line_break = next_line_break;
862
863                                    current_position += line_break.position_wrap() as i32;
864
865                                    if line_break.position_wrap() as i32 != current_run_length {
866                                        break;
867                                    }
868
869                                    index += 1;
870
871                                    if index >= run_count {
872                                        break;
873                                    }
874
875                                    current_run = &text_runs[index];
876                                    current_run_length = current_run.length();
877
878                                    line_breaker = LineBreakEnumerator::new(current_run.text_span());
879                                }
880                            } else {
881                                current_position = current_length + line_break.position_wrap() as i32;
882                            }
883
884                            if current_position == 0 && measured_length > 0 {
885                                current_position = measured_length;
886                            }
887
888                            break_found = true;
889
890                            break;
891                        }
892
893                        // We overflowed so we use the last available wrap position.
894                        current_position = if last_wrap_position == 0 { measured_length } else { last_wrap_position };
895
896                        break_found = true;
897
898                        break;
899                    }
900
901                    if line_break.position_measure() != line_break.position_wrap()
902                        || line_break.position_wrap() as i32 != current_run_length
903                    {
904                        last_wrap_position = current_length + line_break.position_wrap() as i32;
905                    }
906                }
907            }
908
909            if !break_found {
910                current_length += current_run_length;
911
912                index += 1;
913
914                continue;
915            }
916
917            measured_length = current_position;
918
919            break;
920        }
921
922        measured_length
923    }
924
925    fn reset_trailing_whitespace_bidi_levels(
926        line_text_runs: &mut Vec<Rc<dyn TextRun>>,
927        paragraph_flow_direction: FlowDirection,
928        object_pool: &FormattingObjectPool,
929    ) {
930        if line_text_runs.is_empty() {
931            return;
932        }
933
934        let last_text_run_index = line_text_runs.len() - 1;
935
936        let last_text_run = line_text_runs[last_text_run_index].clone();
937
938        let Some(shaped_text) = last_text_run.downcast_ref::<ShapedTextRun>() else {
939            return;
940        };
941
942        let paragraph_embedding_level = paragraph_flow_direction as i8;
943
944        if shaped_text.bidi_level() == paragraph_embedding_level {
945            return;
946        }
947
948        let trailing_whitespace_length = shaped_text.glyph_run().metrics().trailing_whitespace_length;
949
950        if trailing_whitespace_length == 0 {
951            return;
952        }
953
954        let split_index = shaped_text.length() - trailing_whitespace_length;
955
956        let (text_runs, trailing_whitespace_runs) =
957            Self::split_text_runs(std::slice::from_ref(&last_text_run), split_index, object_pool).deconstruct();
958
959        let trailing_whitespace_runs = trailing_whitespace_runs.map(|mut trailing_whitespace_runs| {
960            for run in trailing_whitespace_runs.iter_mut() {
961                let replacement = run.downcast_ref::<ShapedTextRun>().and_then(|shaped_text_run| {
962                    let new_buffer = shaped_text_run.shaped_buffer().with_bidi_level(paragraph_embedding_level);
963
964                    if Rc::ptr_eq(&new_buffer, shaped_text_run.shaped_buffer()) {
965                        return None;
966                    }
967
968                    let replacement: Rc<dyn TextRun> =
969                        Rc::new(ShapedTextRun::new(new_buffer, shaped_text_run.run_properties().clone()));
970
971                    shaped_text_run.dispose();
972
973                    Some(replacement)
974                });
975
976                if let Some(replacement) = replacement {
977                    *run = replacement;
978                }
979            }
980
981            line_text_runs.remove(last_text_run_index);
982
983            if let Some(text_runs) = &text_runs {
984                line_text_runs.extend(text_runs.iter().cloned());
985            }
986
987            line_text_runs.extend(trailing_whitespace_runs.iter().cloned());
988
989            trailing_whitespace_runs
990        });
991
992        object_pool.text_run_lists.return_optional(text_runs);
993        object_pool.text_run_lists.return_optional(trailing_whitespace_runs);
994    }
995}
996
997impl TextFormatter for TextFormatterImpl {
998    fn format_line(
999        &self,
1000        text_source: &dyn ITextSource,
1001        first_text_source_index: i32,
1002        paragraph_width: f64,
1003        paragraph_properties: &Rc<dyn TextParagraphProperties>,
1004        previous_line_break: Option<&Rc<TextLineBreak>>,
1005    ) -> Option<Rc<dyn TextLine>> {
1006        self.format_line_with_cache(
1007            text_source,
1008            first_text_source_index,
1009            paragraph_width,
1010            paragraph_properties,
1011            previous_line_break,
1012            None,
1013        )
1014    }
1015
1016    fn format_line_with_cache(
1017        &self,
1018        text_source: &dyn ITextSource,
1019        first_text_source_index: i32,
1020        paragraph_width: f64,
1021        paragraph_properties: &Rc<dyn TextParagraphProperties>,
1022        previous_line_break: Option<&Rc<TextLineBreak>>,
1023        text_run_cache: Option<&TextRunCache>,
1024    ) -> Option<Rc<dyn TextLine>> {
1025        crate::perf_count!(TextLinesFormatted);
1026        let object_pool = FormattingObjectPool::instance();
1027        let font_manager = FontManager::current();
1028
1029        // we've wrapped the previous line and need to continue wrapping: ignore the text source and do that instead
1030        if let Some(previous_line_break) = previous_line_break {
1031            if WrappingTextLineBreak::is_wrapping(previous_line_break) {
1032                if let Some(mut remaining_runs) = WrappingTextLineBreak::acquire_remaining_runs(previous_line_break) {
1033                    if paragraph_properties.text_wrapping() != TextWrapping::NoWrap {
1034                        return Some(Self::perform_text_wrapping(
1035                            &mut remaining_runs,
1036                            true,
1037                            first_text_source_index,
1038                            paragraph_width,
1039                            paragraph_properties,
1040                            previous_line_break.flow_direction(),
1041                            Some(previous_line_break),
1042                            object_pool,
1043                        ));
1044                    }
1045                }
1046            }
1047        }
1048
1049        // Try to use cached shaped runs to avoid redundant shaping/bidi processing.
1050        if let Some(text_run_cache) = text_run_cache {
1051            if let Some(cached) = text_run_cache.try_get_shaped_runs(first_text_source_index) {
1052                crate::perf_count!(TextRunCacheHits);
1053                return Some(Self::format_line_from_cache(
1054                    &cached,
1055                    first_text_source_index,
1056                    paragraph_width,
1057                    paragraph_properties,
1058                    object_pool,
1059                ));
1060            }
1061        }
1062
1063        let (fetched_runs, text_end_of_line, text_source_length) =
1064            Self::fetch_text_runs(text_source, first_text_source_index, object_pool);
1065
1066        if fetched_runs.is_empty() {
1067            object_pool.text_run_lists.return_list(fetched_runs);
1068
1069            return None;
1070        }
1071
1072        let (mut shaped_text_runs, resolved_flow_direction) =
1073            Self::shape_text_runs(&fetched_runs, &**paragraph_properties, object_pool, &font_manager);
1074
1075        let next_line_break = text_end_of_line
1076            .as_ref()
1077            .map(|text_end_of_line| Rc::new(TextLineBreak::new(Some(text_end_of_line.clone()), resolved_flow_direction, false)));
1078
1079        // Store shaped runs in cache for reuse. The cache takes its own references;
1080        // the formatter keeps the fresh-from-shape references for the current line.
1081        if let Some(text_run_cache) = text_run_cache {
1082            crate::perf_count!(TextRunCacheMisses);
1083            text_run_cache.add(
1084                first_text_source_index,
1085                CachedShapingResult::new(
1086                    Rc::from(&shaped_text_runs[..]),
1087                    resolved_flow_direction,
1088                    text_end_of_line,
1089                    text_source_length,
1090                ),
1091            );
1092        }
1093
1094        let text_line = match paragraph_properties.text_wrapping() {
1095            TextWrapping::NoWrap => {
1096                // The runs are moved out of the rented list, which is returned below.
1097                let line_runs: Vec<Rc<dyn TextRun>> = shaped_text_runs.drain(..).collect();
1098
1099                let text_line = TextLineImpl::new(
1100                    line_runs,
1101                    first_text_source_index,
1102                    text_source_length,
1103                    paragraph_width,
1104                    paragraph_properties.clone(),
1105                    resolved_flow_direction,
1106                    next_line_break,
1107                    false,
1108                );
1109
1110                text_line.finalize_line();
1111
1112                text_line
1113            }
1114            TextWrapping::WrapWithOverflow | TextWrapping::Wrap => Self::perform_text_wrapping(
1115                &mut shaped_text_runs,
1116                false,
1117                first_text_source_index,
1118                paragraph_width,
1119                paragraph_properties,
1120                resolved_flow_direction,
1121                next_line_break.as_ref(),
1122                object_pool,
1123            ),
1124        };
1125
1126        object_pool.text_run_lists.return_list(shaped_text_runs);
1127        object_pool.text_run_lists.return_list(fetched_runs);
1128
1129        Some(text_line)
1130    }
1131}
1132
1133struct TextRunEnumerator<'a> {
1134    text_source: &'a dyn ITextSource,
1135    pos: i32,
1136}
1137
1138impl<'a> TextRunEnumerator<'a> {
1139    fn new(text_source: &'a dyn ITextSource, first_text_source_index: i32) -> Self {
1140        Self { text_source, pos: first_text_source_index }
1141    }
1142
1143    fn move_next(&mut self) -> Option<Rc<dyn TextRun>> {
1144        let current = self.text_source.get_text_run(self.pos)?;
1145
1146        if current.length() == 0 {
1147            return None;
1148        }
1149
1150        self.pos += current.length();
1151
1152        Some(current)
1153    }
1154}