Skip to main content

ferroui_base/media/text_formatting/
text_layout.rs

1use std::rc::Rc;
2
3use crate::media::text_formatting::{
4    FormattedTextSource, FormattingObjectPool, GenericTextParagraphProperties, GenericTextRunProperties,
5    ITextDrawingSink, ITextSource, InterWordJustification, TextCollapsingProperties, TextEndOfParagraph,
6    TextFormatter, TextFormatterImpl, TextLine, TextLineImpl, TextParagraphProperties, TextRun, TextRunCache,
7    TextRunProperties,
8};
9use crate::media::{
10    BaselineAlignment, CharacterHit, FlowDirection, FontFeatureCollection, IBrush, TextAlignment,
11    TextCollapsingCreateInfo, TextDecorationCollection, TextHitTestResult, TextTrimming, TextWrapping, Typeface,
12};
13use crate::utilities::{MathUtilities, ReadOnlyMemory, ValueSpan};
14use crate::{Point, Rect};
15
16/// The optional parameters of [`TextLayout::new`]. `Default` gives upstream's
17/// parameter defaults.
18#[derive(Clone)]
19pub struct TextLayoutOptions {
20    /// Size of the font.
21    pub font_size: f64,
22    /// The foreground.
23    pub foreground: Option<Rc<dyn IBrush>>,
24    /// The text alignment.
25    pub text_alignment: TextAlignment,
26    /// The text wrapping.
27    pub text_wrapping: TextWrapping,
28    /// The text trimming (`None` means no trimming).
29    pub text_trimming: Option<Rc<dyn TextTrimming>>,
30    /// The text decorations.
31    pub text_decorations: Option<TextDecorationCollection>,
32    /// The text flow direction.
33    pub flow_direction: FlowDirection,
34    /// The maximum width.
35    pub max_width: f64,
36    /// The maximum height.
37    pub max_height: f64,
38    /// The height of each line of text.
39    pub line_height: f64,
40    /// The letter spacing that is applied to rendered glyphs.
41    pub letter_spacing: f64,
42    /// The maximum number of text lines.
43    pub max_lines: i32,
44    /// Optional list of turned on/off features.
45    pub font_features: Option<FontFeatureCollection>,
46    /// The text style overrides.
47    pub text_style_overrides: Option<Rc<[ValueSpan<Rc<dyn TextRunProperties>>]>>,
48    /// An optional cache for shaped text runs to avoid redundant shaping.
49    pub text_run_cache: Option<Rc<TextRunCache>>,
50}
51
52impl Default for TextLayoutOptions {
53    fn default() -> Self {
54        Self {
55            font_size: GenericTextRunProperties::DEFAULT_FONT_RENDERING_EM_SIZE,
56            foreground: None,
57            text_alignment: TextAlignment::Left,
58            text_wrapping: TextWrapping::NoWrap,
59            text_trimming: None,
60            text_decorations: None,
61            flow_direction: FlowDirection::LeftToRight,
62            max_width: f64::INFINITY,
63            max_height: f64::INFINITY,
64            line_height: f64::NAN,
65            letter_spacing: 0.0,
66            max_lines: 0,
67            font_features: None,
68            text_style_overrides: None,
69            text_run_cache: None,
70        }
71    }
72}
73
74/// Represents a multi line text layout.
75pub struct TextLayout {
76    text_source: Rc<dyn ITextSource>,
77    paragraph_properties: Rc<dyn TextParagraphProperties>,
78    text_trimming: Rc<dyn TextTrimming>,
79    text_lines: Vec<Rc<dyn TextLine>>,
80    metrics: CachedMetrics,
81    text_run_cache: Option<Rc<TextRunCache>>,
82
83    text_source_length: i32,
84
85    max_width: f64,
86    max_height: f64,
87    max_lines: i32,
88}
89
90impl TextLayout {
91    /// Initializes a new instance of the `TextLayout` class.
92    ///
93    /// * `text` — the text.
94    /// * `typeface` — the typeface.
95    /// * `options` — the remaining (optional) parameters.
96    pub fn new(text: &str, typeface: Typeface, options: TextLayoutOptions) -> Self {
97        Self::from_utf16(ReadOnlyMemory::<u16>::from_str(text), typeface, options)
98    }
99
100    /// Initializes a new instance of the `TextLayout` class from UTF-16 text
101    /// (no transcoding).
102    pub fn from_utf16(text: ReadOnlyMemory<u16>, typeface: Typeface, options: TextLayoutOptions) -> Self {
103        let paragraph_properties = Self::create_text_paragraph_properties(
104            typeface,
105            options.font_size,
106            options.foreground,
107            options.text_alignment,
108            options.text_wrapping,
109            options.text_decorations,
110            options.flow_direction,
111            options.line_height,
112            options.letter_spacing,
113            options.font_features,
114        );
115
116        let text_source: Rc<dyn ITextSource> = Rc::new(FormattedTextSource::new(
117            text,
118            paragraph_properties.default_text_run_properties().clone(),
119            options.text_style_overrides,
120        ));
121
122        Self::from_text_source(
123            text_source,
124            paragraph_properties,
125            options.text_trimming,
126            options.max_width,
127            options.max_height,
128            options.max_lines,
129            options.text_run_cache,
130        )
131    }
132
133    /// Initializes a new instance of the `TextLayout` class.
134    ///
135    /// * `text_source` — the text source.
136    /// * `paragraph_properties` — the default text paragraph properties.
137    /// * `text_trimming` — the text trimming (upstream default: `None`).
138    /// * `max_width` — the maximum width (upstream default: positive infinity).
139    /// * `max_height` — the maximum height (upstream default: positive infinity).
140    /// * `max_lines` — the maximum number of text lines (upstream default: 0).
141    /// * `text_run_cache` — an optional cache for shaped text runs to avoid redundant shaping.
142    pub fn from_text_source(
143        text_source: Rc<dyn ITextSource>,
144        paragraph_properties: Rc<dyn TextParagraphProperties>,
145        text_trimming: Option<Rc<dyn TextTrimming>>,
146        max_width: f64,
147        max_height: f64,
148        max_lines: i32,
149        text_run_cache: Option<Rc<TextRunCache>>,
150    ) -> Self {
151        crate::perf_count!(TextLayoutsCreated);
152        let mut text_layout = Self {
153            text_source,
154            paragraph_properties,
155            text_trimming: text_trimming.unwrap_or_else(<dyn TextTrimming>::none),
156            text_lines: Vec::new(),
157            metrics: CachedMetrics::default(),
158            text_run_cache,
159            text_source_length: 0,
160            max_width,
161            max_height,
162            max_lines,
163        };
164
165        text_layout.text_lines = text_layout.create_text_lines();
166
167        text_layout
168    }
169
170    /// Gets the height of each line of text.
171    ///
172    /// A value of NaN (equivalent to an attribute value of "Auto") indicates
173    /// that the line height is determined automatically from the current font
174    /// characteristics. The default is NaN.
175    pub fn line_height(&self) -> f64 {
176        self.paragraph_properties.line_height()
177    }
178
179    /// Gets the maximum width.
180    pub fn max_width(&self) -> f64 {
181        self.max_width
182    }
183
184    /// Gets the maximum height.
185    pub fn max_height(&self) -> f64 {
186        self.max_height
187    }
188
189    /// Gets the maximum number of text lines.
190    pub fn max_lines(&self) -> i32 {
191        self.max_lines
192    }
193
194    /// Gets the text spacing.
195    pub fn letter_spacing(&self) -> f64 {
196        self.paragraph_properties.letter_spacing()
197    }
198
199    /// Gets the text lines.
200    pub fn text_lines(&self) -> &[Rc<dyn TextLine>] {
201        &self.text_lines
202    }
203
204    /// The distance from the top of the first line to the bottom of the last line.
205    pub fn height(&self) -> f64 {
206        self.metrics.height
207    }
208
209    /// The distance from the topmost black pixel of the first line to the
210    /// bottommost black pixel of the last line.
211    pub fn extent(&self) -> f64 {
212        self.metrics.extent
213    }
214
215    /// The distance from the top of the first line to the baseline of the first line.
216    pub fn baseline(&self) -> f64 {
217        self.metrics.baseline
218    }
219
220    /// The distance from the bottom of the last line to the extent bottom.
221    pub fn overhang_after(&self) -> f64 {
222        self.metrics.overhang_after
223    }
224
225    /// The maximum distance from the leading black pixel to the leading alignment point of a line.
226    pub fn overhang_leading(&self) -> f64 {
227        self.metrics.overhang_leading
228    }
229
230    /// The maximum distance from the trailing black pixel to the trailing alignment point of a line.
231    pub fn overhang_trailing(&self) -> f64 {
232        self.metrics.overhang_trailing
233    }
234
235    /// The maximum advance width between the leading and trailing alignment
236    /// points of a line, excluding the width of whitespace characters at the
237    /// end of the line.
238    pub fn width(&self) -> f64 {
239        self.metrics.width
240    }
241
242    /// The maximum advance width between the leading and trailing alignment
243    /// points of a line, including the width of whitespace characters at the
244    /// end of the line.
245    pub fn width_including_trailing_whitespace(&self) -> f64 {
246        self.metrics.width_including_trailing_whitespace
247    }
248
249    /// Draws the text layout.
250    ///
251    /// * `context` — the drawing context.
252    /// * `origin` — the origin.
253    pub fn draw(&self, context: &mut dyn ITextDrawingSink, origin: Point) {
254        if self.text_lines.is_empty() {
255            return;
256        }
257
258        let (current_x, mut current_y) = (origin.x, origin.y);
259
260        for text_line in &self.text_lines {
261            text_line.draw(context, Point::new(current_x, current_y));
262
263            current_y += text_line.height();
264        }
265    }
266
267    /// Get the pixel location relative to the top-left of the layout box given the text position.
268    ///
269    /// * `text_position` — the text position.
270    pub fn hit_test_text_position(&self, mut text_position: i32) -> Rect {
271        if self.text_lines.is_empty() {
272            return Rect::default();
273        }
274
275        if text_position < 0 {
276            text_position = self.text_source_length;
277        }
278
279        let mut current_y = 0.0;
280
281        for (i, text_line) in self.text_lines.iter().enumerate() {
282            let end = text_line.first_text_source_index() + text_line.length();
283
284            if end <= text_position && i + 1 < self.text_lines.len() {
285                current_y += text_line.height();
286
287                continue;
288            }
289
290            let character_hit = CharacterHit::new(text_position);
291
292            let start_x = text_line.get_distance_from_character_hit(character_hit);
293
294            let next_character_hit = text_line.get_next_caret_character_hit(character_hit);
295
296            let end_x = text_line.get_distance_from_character_hit(next_character_hit);
297
298            return Rect::new(start_x, current_y, end_x - start_x, text_line.height());
299        }
300
301        Rect::default()
302    }
303
304    /// Gets the bounding rectangles of a range of text, relative to the
305    /// top-left of the layout box.
306    pub fn hit_test_text_range(&self, mut start: i32, mut length: i32) -> Vec<Rect> {
307        if start + length <= 0 {
308            return Vec::new();
309        }
310
311        let mut result: Vec<Rect> = Vec::with_capacity(self.text_lines.len());
312
313        let mut current_y = 0.0;
314
315        for text_line in &self.text_lines {
316            // Current line isn't covered.
317            if text_line.first_text_source_index() + text_line.length() <= start {
318                current_y += text_line.height();
319
320                continue;
321            }
322
323            let text_bounds = text_line.get_text_bounds(start, length);
324
325            for bounds in &text_bounds {
326                let rectangle = bounds.rectangle();
327
328                match result.last_mut() {
329                    Some(last)
330                        if MathUtilities::are_close(last.right(), rectangle.left())
331                            && MathUtilities::are_close(last.top(), current_y) =>
332                    {
333                        *last = last.with_width(last.width + rectangle.width);
334                    }
335                    _ => {
336                        result.push(rectangle.with_y(current_y));
337                    }
338                }
339
340                for run_bounds in bounds.text_run_bounds() {
341                    start += run_bounds.length();
342                    length -= run_bounds.length();
343                }
344            }
345
346            if text_line.first_text_source_index() + text_line.length() >= start + length {
347                break;
348            }
349
350            current_y += text_line.height();
351        }
352
353        result
354    }
355
356    /// Gets the character hit of a point relative to the top-left of the layout box.
357    pub fn hit_test_point(&self, point: Point) -> TextHitTestResult {
358        let mut current_y = 0.0;
359
360        let mut current_line: Option<&Rc<dyn TextLine>> = None;
361
362        for text_line in &self.text_lines {
363            current_line = Some(text_line);
364
365            if current_y + text_line.height() > point.y {
366                let character_hit = text_line.get_character_hit_from_distance(point.x);
367
368                return self.get_hit_test_result(&**text_line, character_hit, point);
369            }
370
371            current_y += text_line.height();
372        }
373
374        let Some(current_line) = current_line else {
375            return TextHitTestResult::default();
376        };
377
378        let character_hit = current_line.get_character_hit_from_distance(point.x);
379
380        self.get_hit_test_result(&**current_line, character_hit, point)
381    }
382
383    /// Gets the index of the line that contains a character index.
384    pub fn get_line_index_from_character_index(&self, char_index: i32, trailing_edge: bool) -> i32 {
385        if char_index < 0 {
386            return 0;
387        }
388
389        if char_index > self.text_source_length {
390            return self.text_lines.len() as i32 - 1;
391        }
392
393        for (index, text_line) in self.text_lines.iter().enumerate() {
394            if text_line.first_text_source_index() + text_line.length() < char_index {
395                continue;
396            }
397
398            if char_index >= text_line.first_text_source_index()
399                && char_index
400                    <= text_line.first_text_source_index() + text_line.length() - if trailing_edge { 0 } else { 1 }
401            {
402                return index as i32;
403            }
404        }
405
406        self.text_lines.len() as i32 - 1
407    }
408
409    fn get_hit_test_result(
410        &self,
411        text_line: &dyn TextLine,
412        mut character_hit: CharacterHit,
413        point: Point,
414    ) -> TextHitTestResult {
415        let (x, y) = (point.x, point.y);
416
417        let is_inside = x >= 0.0 && x <= text_line.width() && y >= 0.0 && y <= text_line.height();
418
419        let mut last_trailing_index = 0;
420
421        let text_end_of_line_length = text_line.text_line_break().and_then(|text_line_break| {
422            text_line_break
423                .text_end_of_line()
424                .and_then(|text_end_of_line| text_end_of_line.as_text_end_of_line())
425                .map(|text_end_of_line| text_end_of_line.length())
426        });
427
428        if self.paragraph_properties.flow_direction() == FlowDirection::LeftToRight {
429            last_trailing_index = text_line.first_text_source_index() + text_line.length();
430
431            if x >= text_line.width() && text_line.length() > 0 && text_line.new_line_length() > 0 {
432                last_trailing_index -= text_line.new_line_length();
433            }
434
435            if let Some(text_end_of_line_length) = text_end_of_line_length {
436                last_trailing_index -= text_end_of_line_length;
437            }
438        } else {
439            if x <= text_line.width_including_trailing_whitespace() - text_line.width()
440                && text_line.length() > 0
441                && text_line.new_line_length() > 0
442            {
443                last_trailing_index += text_line.new_line_length();
444            }
445
446            if let Some(text_end_of_line_length) = text_end_of_line_length {
447                last_trailing_index += text_end_of_line_length;
448            }
449        }
450
451        let mut text_position = character_hit.first_character_index() + character_hit.trailing_length();
452
453        let is_trailing =
454            last_trailing_index == text_position && character_hit.trailing_length() > 0 || y > self.height();
455
456        if text_position == text_line.first_text_source_index() + text_line.length() {
457            text_position -= text_line.new_line_length();
458        }
459
460        if text_line.new_line_length() > 0
461            && text_position + text_line.new_line_length()
462                == character_hit.first_character_index() + character_hit.trailing_length()
463        {
464            character_hit = CharacterHit::new(character_hit.first_character_index());
465        }
466
467        TextHitTestResult::new(character_hit, text_position, is_inside, is_trailing)
468    }
469
470    /// Creates the default `TextParagraphProperties` that are used by the `TextFormatter`.
471    ///
472    /// * `typeface` — the typeface.
473    /// * `font_size` — the font size.
474    /// * `foreground` — the foreground.
475    /// * `text_alignment` — the text alignment.
476    /// * `text_wrapping` — the text wrapping.
477    /// * `text_decorations` — the text decorations.
478    /// * `flow_direction` — the text flow direction.
479    /// * `line_height` — the height of each line of text.
480    /// * `letter_spacing` — the letter spacing that is applied to rendered glyphs.
481    /// * `features` — optional list of turned on/off features.
482    #[allow(clippy::too_many_arguments)]
483    pub(crate) fn create_text_paragraph_properties(
484        typeface: Typeface,
485        font_size: f64,
486        foreground: Option<Rc<dyn IBrush>>,
487        text_alignment: TextAlignment,
488        text_wrapping: TextWrapping,
489        text_decorations: Option<TextDecorationCollection>,
490        flow_direction: FlowDirection,
491        line_height: f64,
492        letter_spacing: f64,
493        features: Option<FontFeatureCollection>,
494    ) -> Rc<dyn TextParagraphProperties> {
495        let text_run_style: Rc<dyn TextRunProperties> = Rc::new(GenericTextRunProperties::with_all(
496            typeface,
497            font_size,
498            text_decorations,
499            foreground,
500            None,
501            BaselineAlignment::Baseline,
502            None,
503            features,
504        ));
505
506        Rc::new(GenericTextParagraphProperties::with_all(
507            flow_direction,
508            text_alignment,
509            true,
510            false,
511            text_run_style,
512            text_wrapping,
513            line_height,
514            0.0,
515            letter_spacing,
516        ))
517    }
518
519    fn create_text_lines(&mut self) -> Vec<Rc<dyn TextLine>> {
520        let object_pool = FormattingObjectPool::instance();
521
522        let mut first = true;
523
524        if MathUtilities::is_zero(self.max_width) || MathUtilities::is_zero(self.max_height) {
525            let text_line: Rc<dyn TextLine> =
526                TextFormatterImpl::create_empty_text_line(0, f64::INFINITY, &self.paragraph_properties);
527
528            self.update_metrics(&*text_line, &mut first);
529
530            return vec![text_line];
531        }
532
533        let mut text_lines = object_pool.text_lines.rent();
534
535        self.text_source_length = 0;
536
537        let mut previous_line: Option<Rc<dyn TextLine>> = None;
538
539        let text_formatter = <dyn TextFormatter>::current();
540
541        loop {
542            let previous_line_break = previous_line.as_ref().and_then(|line| line.text_line_break());
543
544            // Upstream: `FormatLine(..) as TextLineImpl`.
545            let text_line = text_formatter
546                .format_line_with_cache(
547                    &*self.text_source,
548                    self.text_source_length,
549                    self.max_width,
550                    &self.paragraph_properties,
551                    previous_line_break.as_ref(),
552                    self.text_run_cache.as_deref(),
553                )
554                .filter(|text_line| text_line.as_any().is::<TextLineImpl>());
555
556            let Some(mut text_line) = text_line else {
557                if previous_line.as_ref().is_some_and(|previous_line| previous_line.new_line_length() > 0) {
558                    let empty_text_line: Rc<dyn TextLine> = TextFormatterImpl::create_empty_text_line(
559                        self.text_source_length,
560                        self.max_width,
561                        &self.paragraph_properties,
562                    );
563
564                    text_lines.push(empty_text_line.clone());
565
566                    self.update_metrics(&*empty_text_line, &mut first);
567                }
568
569                break;
570            };
571
572            self.text_source_length += text_line.length();
573
574            // Whether this freshly fetched candidate line represents real, visible content rather
575            // than just a trailing line-break sentinel (some text sources emit an explicit
576            // TextEndOfLine/TextEndOfParagraph run object at end-of-text with no visible content).
577            let has_real_content = text_line.length() > text_line.new_line_length();
578
579            // Fulfill max height constraint. The mere existence of this next candidate line (which
580            // hasn't been added yet) proves there is more content than fits. Only add an ellipsis
581            // if the last visible line was cut by word-wrap (is_split). If it ended at a hard break
582            // the line itself was not trimmed, and hiding the following paragraph is clipped
583            // silently like CSS max-height with overflow:hidden.
584            if !text_lines.is_empty()
585                && self.max_height != f64::INFINITY
586                && MathUtilities::greater_than(self.height() + text_line.height(), self.max_height)
587            {
588                let last = text_lines.len() - 1;
589                let last_line = text_lines[last].clone();
590
591                if !self.is_trimming_none()
592                    && has_real_content
593                    && last_line.text_line_break().is_some_and(|text_line_break| text_line_break.is_split())
594                {
595                    text_lines[last] = self.collapse_for_truncation(last_line);
596                }
597
598                break;
599            }
600
601            // Fulfill max lines constraint. The next candidate line's existence proves the previously
602            // added (Max-th) line hides real content. Unlike the height limit this ellipsizes after a
603            // hard break too, like CSS line-clamp.
604            if self.max_lines > 0 && text_lines.len() as i32 >= self.max_lines {
605                if !self.is_trimming_none() && has_real_content {
606                    let last = text_lines.len() - 1;
607                    let last_line = text_lines[last].clone();
608
609                    text_lines[last] = self.collapse_for_truncation(last_line);
610                }
611
612                break;
613            }
614
615            let has_overflowed = text_line.has_overflowed();
616
617            if has_overflowed && !self.is_trimming_none() {
618                text_line = text_line.collapse(&[self.get_collapsing_properties(self.max_width)]);
619            }
620
621            text_lines.push(text_line.clone());
622
623            self.update_metrics(&*text_line, &mut first);
624
625            previous_line = Some(text_line.clone());
626
627            let is_end_of_paragraph = text_line.text_line_break().is_some_and(|text_line_break| {
628                text_line_break
629                    .text_end_of_line()
630                    .is_some_and(|text_end_of_line| text_end_of_line.is::<TextEndOfParagraph>())
631            });
632
633            if is_end_of_paragraph {
634                break;
635            }
636        }
637
638        if text_lines.is_empty() {
639            let text_line: Rc<dyn TextLine> =
640                TextFormatterImpl::create_empty_text_line(0, self.max_width, &self.paragraph_properties);
641
642            text_lines.push(text_line.clone());
643
644            self.update_metrics(&*text_line, &mut first);
645        }
646
647        if self.paragraph_properties.text_alignment() == TextAlignment::Justify {
648            // Justify fills each line to the column width, which is MaxWidth for both
649            // wrapped and non-wrapped text. Targeting the widest produced line instead
650            // (the previous behaviour for wrapping) leaves wrapped text short of the
651            // margin, since the full non-last lines already equal that width. When
652            // MaxWidth is infinite there is no column to fill, so skip.
653            let justification_width = self.max_width;
654
655            if !justification_width.is_infinite() && justification_width > 0.0 {
656                let justification_properties = InterWordJustification::new(justification_width);
657
658                for (i, line) in text_lines.iter().enumerate() {
659                    // Only width-driven wrapped lines are stretched to the column. Skip
660                    // the last line of the layout, any line ended by a newline
661                    // (NewLineLength > 0), and any line ended by a source-provided required
662                    // break (TextEndOfLine) - these stay start-aligned per standard
663                    // typographic convention.
664                    if i == text_lines.len() - 1
665                        || line.new_line_length() > 0
666                        || line
667                            .text_line_break()
668                            .is_some_and(|text_line_break| text_line_break.text_end_of_line().is_some())
669                    {
670                        continue;
671                    }
672
673                    line.justify(&justification_properties);
674                }
675            }
676        }
677
678        let result = text_lines.to_vec();
679
680        object_pool.text_lines.return_list(text_lines);
681
682        result
683    }
684
685    fn update_metrics(&mut self, current_line: &dyn TextLine, first: &mut bool) {
686        let metrics = &mut self.metrics;
687
688        // 1) Accumulate total layout height by adding the line's Height.
689        metrics.height += current_line.height();
690
691        // 2) For the layout's Width and WidthIncludingTrailingWhitespace,
692        //    use the maximum of the line widths rather than the bounding box.
693        metrics.width = metrics.width.max(current_line.width());
694
695        // 3) Extent is the max black-pixel extent among lines.
696        metrics.extent = metrics.extent.max(current_line.extent());
697
698        // 4) TextWidth is the max of the text width among lines.
699        // We choose to update all related metrics at once (OverhangLeading,
700        // WidthIncludingTrailingWhitespace, OverhangTrailing) if the current line has a
701        // larger text width.
702        let previous_text_width = metrics.width_including_trailing_whitespace;
703        let text_width = current_line.width_including_trailing_whitespace();
704
705        if previous_text_width < text_width {
706            metrics.width_including_trailing_whitespace = current_line.width_including_trailing_whitespace();
707            metrics.overhang_leading = current_line.overhang_leading();
708            metrics.overhang_trailing = current_line.overhang_trailing();
709        }
710
711        // 5) OverhangAfter is the last line's OverhangAfter.
712        metrics.overhang_after = current_line.overhang_after();
713
714        // 6) Capture the baseline from the first line.
715        if *first {
716            metrics.baseline = current_line.baseline();
717            *first = false;
718        }
719    }
720
721    /// `_textTrimming == TextTrimming.None`.
722    fn is_trimming_none(&self) -> bool {
723        Rc::ptr_eq(&self.text_trimming, &<dyn TextTrimming>::none())
724    }
725
726    /// Gets the `TextCollapsingProperties` for current text trimming mode.
727    ///
728    /// * `width` — the collapsing width.
729    fn get_collapsing_properties(&self, width: f64) -> Option<Rc<dyn TextCollapsingProperties>> {
730        if self.is_trimming_none() {
731            return None;
732        }
733
734        Some(self.text_trimming.create_collapsing_properties(&TextCollapsingCreateInfo::new(
735            width,
736            self.paragraph_properties.default_text_run_properties().clone(),
737            self.paragraph_properties.flow_direction(),
738        )))
739    }
740
741    fn collapse_for_truncation(&self, line: Rc<dyn TextLine>) -> Rc<dyn TextLine> {
742        // Collapse against the line's own rendered width rather than max_width, so there's
743        // always room reserved for the ellipsis symbol and a trailing ellipsis is reliably
744        // produced, even if the line already fits within max_width on its own.
745        let collapsing_properties = self.get_collapsing_properties(line.width_including_trailing_whitespace());
746
747        if collapsing_properties.is_none() {
748            return line;
749        }
750
751        line.collapse(&[collapsing_properties])
752    }
753
754    /// Releases the text lines.
755    pub fn dispose(&self) {
756        for line in &self.text_lines {
757            line.dispose();
758        }
759    }
760}
761
762#[derive(Default)]
763struct CachedMetrics {
764    // vertical
765    height: f64,
766    baseline: f64,
767
768    // horizontal
769    width: f64,
770    width_including_trailing_whitespace: f64,
771
772    // vertical bounding box metrics
773    extent: f64,
774    overhang_after: f64,
775
776    // horizontal bounding box metrics
777    overhang_leading: f64,
778    overhang_trailing: f64,
779}