Skip to main content

ferroui_base/media/
formatted_text.rs

1use std::cell::{Cell, RefCell};
2use std::rc::Rc;
3
4use crate::media::text_formatting::{
5    GenericTextParagraphProperties, GenericTextRunProperties, ITextDrawingSink, ITextSource, TextCharacters,
6    TextEndOfParagraph, TextFormatter, TextLine, TextLineBreak, TextParagraphProperties, TextRun, TextRunProperties,
7};
8use crate::media::{
9    BaselineAlignment, Drawing, DrawingGroup, FillRule, FlowDirection, FontFamily, FontFeatureCollection,
10    FontStretch, FontStyle, FontWeight, Geometry, GeometryCombineMode, GeometryDrawing, GeometryGroup,
11    GlyphRunDrawing, IBrush, LineGeometry, RectangleGeometry, TextAlignment, TextCollapsingCreateInfo,
12    TextDecorationCollection, TextTrimming, TextWrapping, Transform, TranslateTransform, Typeface,
13};
14use crate::utilities::span::{SpanPosition, SpanRider, SpanVector};
15use crate::utilities::{CultureInfo, ReadOnlyMemory};
16use crate::{Point, Rect, Ref};
17
18/// The FormattedText class is targeted at programmers needing to add some
19/// simple text to a visual.
20///
21/// All character indices and counts are UTF-16 code units of the text.
22pub struct FormattedText {
23    // properties and format runs
24    text: ReadOnlyMemory<u16>,
25    format_runs: RefCell<SpanVector<Rc<dyn TextRunProperties>>>,
26    latest_position: Cell<SpanPosition>,
27
28    default_para_props: Rc<GenericTextParagraphProperties>,
29
30    max_text_width: Cell<f64>,
31    max_text_widths: RefCell<Option<Vec<f64>>>,
32    max_text_height: Cell<f64>,
33    max_line_count: Cell<i32>,
34    trimming: RefCell<Rc<dyn TextTrimming>>,
35
36    // cached metrics
37    metrics: Cell<Option<CachedMetrics>>,
38}
39
40impl FormattedText {
41    pub const DEFAULT_REAL_TO_IDEAL: f64 = 28800.0 / 96.0;
42    pub const DEFAULT_IDEAL_TO_REAL: f64 = 1.0 / Self::DEFAULT_REAL_TO_IDEAL;
43    pub const IDEAL_INFINITE_WIDTH: i32 = 0x3FFFFFFE;
44    pub const REAL_INFINITE_WIDTH: f64 = Self::IDEAL_INFINITE_WIDTH as f64 * Self::DEFAULT_IDEAL_TO_REAL;
45
46    pub const GREATEST_MULTIPLIER_OF_EM: f64 = 100.0;
47
48    const MAX_FONT_EM_SIZE: f64 = Self::REAL_INFINITE_WIDTH / Self::GREATEST_MULTIPLIER_OF_EM;
49
50    /// Construct a FormattedText object.
51    ///
52    /// * `text_to_format` — string of text to be displayed.
53    /// * `culture` — culture of text.
54    /// * `flow_direction` — flow direction of text.
55    /// * `typeface` — type face used to display text.
56    /// * `em_size` — font em size in visual units (1/96 of an inch).
57    /// * `foreground` — foreground brush used to render text.
58    ///
59    /// Panics when the em size is not a positive number or is too large.
60    pub fn new(
61        text_to_format: &str,
62        culture: CultureInfo,
63        flow_direction: FlowDirection,
64        typeface: Typeface,
65        em_size: f64,
66        foreground: Option<Rc<dyn IBrush>>,
67    ) -> Self {
68        Self::validate_font_size(em_size);
69
70        let text = ReadOnlyMemory::<u16>::from_str(text_to_format);
71
72        let run_props: Rc<dyn TextRunProperties> = Rc::new(GenericTextRunProperties::with_all(
73            typeface,
74            em_size,
75            None, // decorations
76            foreground,
77            None, // highlight background
78            BaselineAlignment::Baseline,
79            Some(culture),
80            None,
81        ));
82
83        let mut format_runs = SpanVector::new(None);
84
85        let latest_position =
86            format_runs.set_value_at(0, text.len() as i32, run_props.clone(), SpanPosition::default());
87
88        let default_para_props = Rc::new(GenericTextParagraphProperties::with_all(
89            flow_direction,
90            TextAlignment::Left,
91            false,
92            false,
93            run_props,
94            TextWrapping::WrapWithOverflow,
95            0.0, // line height not specified
96            0.0, // indentation not specified
97            0.0,
98        ));
99
100        let formatted_text = Self {
101            text,
102            format_runs: RefCell::new(format_runs),
103            latest_position: Cell::new(latest_position),
104            default_para_props,
105            max_text_width: Cell::new(f64::INFINITY),
106            max_text_widths: RefCell::new(None),
107            max_text_height: Cell::new(f64::INFINITY),
108            max_line_count: Cell::new(i32::MAX),
109            trimming: RefCell::new(<dyn TextTrimming>::word_ellipsis()),
110            metrics: Cell::new(None),
111        };
112
113        formatted_text.invalidate_metrics();
114
115        formatted_text
116    }
117
118    fn validate_font_size(em_size: f64) {
119        if em_size <= 0.0 {
120            panic!("em_size: The parameter value must be greater than zero.");
121        }
122
123        if em_size > Self::MAX_FONT_EM_SIZE {
124            panic!("em_size: The parameter value cannot be greater than '{}'", Self::MAX_FONT_EM_SIZE);
125        }
126
127        if em_size.is_nan() {
128            panic!("em_size: The parameter value must be a number.");
129        }
130    }
131
132    fn text_length(&self) -> i32 {
133        self.text.len() as i32
134    }
135
136    fn validate_range(&self, start_index: i32, count: i32) -> i32 {
137        if start_index < 0 || start_index > self.text_length() {
138            panic!("start_index is out of range.");
139        }
140
141        let limit = start_index.wrapping_add(count);
142
143        if count < 0 || limit < start_index || limit > self.text_length() {
144            panic!("count is out of range.");
145        }
146
147        limit
148    }
149
150    fn invalidate_metrics(&self) {
151        self.metrics.set(None);
152    }
153
154    /// The loop every range setter of upstream consists of: walks the format
155    /// runs of the range and replaces the properties of each run for which
156    /// `create_new_props` returns new ones (`None` means the run already has
157    /// the value).
158    fn set_run_properties(
159        &self,
160        start_index: i32,
161        count: i32,
162        invalidate_metrics: bool,
163        create_new_props: impl Fn(&dyn TextRunProperties) -> Option<GenericTextRunProperties>,
164    ) {
165        let limit = self.validate_range(start_index, count);
166
167        let mut i = start_index;
168
169        while i < limit {
170            let (rider_length, current_position, span_position, run_props) = {
171                let format_runs = self.format_runs.borrow();
172                let format_rider = SpanRider::new(&format_runs, self.latest_position.get(), i);
173
174                (
175                    format_rider.length(),
176                    format_rider.current_position(),
177                    format_rider.span_position(),
178                    format_rider.current_element().cloned(),
179                )
180            };
181
182            i = limit.min(i.saturating_add(rider_length));
183
184            // run_props can never be null because the rider is already checked to be in range
185            let Some(run_props) = run_props else {
186                panic!("run_props can not be null.");
187            };
188
189            let Some(new_props) = create_new_props(&*run_props) else {
190                continue;
191            };
192
193            let new_props: Rc<dyn TextRunProperties> = Rc::new(new_props);
194
195            let latest_position = self.format_runs.borrow_mut().set_value_at(
196                current_position,
197                i - current_position,
198                new_props,
199                span_position,
200            );
201
202            self.latest_position.set(latest_position);
203
204            if invalidate_metrics {
205                self.invalidate_metrics();
206            }
207        }
208    }
209
210    /// Sets foreground brush used for drawing text.
211    ///
212    /// * `foreground_brush` — foreground brush
213    pub fn set_foreground_brush(&self, foreground_brush: Rc<dyn IBrush>) {
214        self.set_foreground_brush_range(Some(foreground_brush), 0, self.text_length());
215    }
216
217    /// Sets foreground brush used for drawing text.
218    ///
219    /// * `foreground_brush` — foreground brush
220    /// * `start_index` — the start index of initial character to apply the change to.
221    /// * `count` — the number of characters the change should be applied to.
222    pub fn set_foreground_brush_range(&self, foreground_brush: Option<Rc<dyn IBrush>>, start_index: i32, count: i32) {
223        self.set_run_properties(start_index, count, false, |run_props| {
224            let is_same = match (run_props.foreground_brush(), &foreground_brush) {
225                (None, None) => true,
226                (Some(current), Some(new)) => Rc::ptr_eq(current, new),
227                _ => false,
228            };
229
230            if is_same {
231                return None;
232            }
233
234            Some(GenericTextRunProperties::with_all(
235                run_props.typeface().clone(),
236                run_props.font_rendering_em_size(),
237                run_props.text_decorations().cloned(),
238                foreground_brush.clone(),
239                run_props.background_brush().cloned(),
240                run_props.baseline_alignment(),
241                run_props.culture_info().cloned(),
242                run_props.font_features().cloned(),
243            ))
244        });
245    }
246
247    /// Sets or changes the font features for the text object.
248    ///
249    /// * `font_features` — feature collection
250    pub fn set_font_features(&self, font_features: Option<FontFeatureCollection>) {
251        self.set_font_features_range(font_features, 0, self.text_length());
252    }
253
254    /// Sets or changes the font features for the text object.
255    ///
256    /// * `font_features` — feature collection
257    /// * `start_index` — the start index of initial character to apply the change to.
258    /// * `count` — the number of characters the change should be applied to.
259    pub fn set_font_features_range(&self, font_features: Option<FontFeatureCollection>, start_index: i32, count: i32) {
260        self.set_run_properties(start_index, count, false, |run_props| {
261            let is_same = match (&font_features, run_props.font_features()) {
262                (None, None) => true,
263                (Some(new), Some(current)) => new.to_vec() == current.to_vec(),
264                _ => false,
265            };
266
267            if is_same {
268                return None;
269            }
270
271            Some(GenericTextRunProperties::with_all(
272                run_props.typeface().clone(),
273                run_props.font_rendering_em_size(),
274                run_props.text_decorations().cloned(),
275                run_props.foreground_brush().cloned(),
276                run_props.background_brush().cloned(),
277                run_props.baseline_alignment(),
278                run_props.culture_info().cloned(),
279                font_features.clone(),
280            ))
281        });
282    }
283
284    /// Sets or changes the font family for the text object.
285    ///
286    /// * `font_family` — font family name
287    pub fn set_font_family_name(&self, font_family: &str) {
288        self.set_font_family_name_range(font_family, 0, self.text_length());
289    }
290
291    /// Sets or changes the font family for the text object.
292    ///
293    /// * `font_family` — font family name
294    /// * `start_index` — the start index of initial character to apply the change to.
295    /// * `count` — the number of characters the change should be applied to.
296    pub fn set_font_family_name_range(&self, font_family: &str, start_index: i32, count: i32) {
297        self.set_font_family_range(FontFamily::new(font_family), start_index, count);
298    }
299
300    /// Sets or changes the font family for the text object.
301    ///
302    /// * `font_family` — font family
303    pub fn set_font_family(&self, font_family: FontFamily) {
304        self.set_font_family_range(font_family, 0, self.text_length());
305    }
306
307    /// Sets or changes the font family for the text object.
308    ///
309    /// * `font_family` — font family
310    /// * `start_index` — the start index of initial character to apply the change to.
311    /// * `count` — the number of characters the change should be applied to.
312    pub fn set_font_family_range(&self, font_family: FontFamily, start_index: i32, count: i32) {
313        self.set_run_properties(start_index, count, true, |run_props| {
314            let old_typeface = run_props.typeface();
315
316            if font_family == *old_typeface.font_family() {
317                return None;
318            }
319
320            Some(GenericTextRunProperties::with_all(
321                Typeface::with_style(
322                    font_family.clone(),
323                    old_typeface.style(),
324                    old_typeface.weight(),
325                    FontStretch::Normal,
326                ),
327                run_props.font_rendering_em_size(),
328                run_props.text_decorations().cloned(),
329                run_props.foreground_brush().cloned(),
330                run_props.background_brush().cloned(),
331                run_props.baseline_alignment(),
332                run_props.culture_info().cloned(),
333                run_props.font_features().cloned(),
334            ))
335        });
336    }
337
338    /// Sets or changes the font em size measured in device independent units.
339    ///
340    /// * `em_size` — font em size
341    pub fn set_font_size(&self, em_size: f64) {
342        self.set_font_size_range(em_size, 0, self.text_length());
343    }
344
345    /// Sets or changes the font em size measured in device independent units.
346    ///
347    /// * `em_size` — font em size
348    /// * `start_index` — the start index of initial character to apply the change to.
349    /// * `count` — the number of characters the change should be applied to.
350    pub fn set_font_size_range(&self, em_size: f64, start_index: i32, count: i32) {
351        Self::validate_font_size(em_size);
352
353        self.set_run_properties(start_index, count, true, |run_props| {
354            if run_props.font_rendering_em_size() == em_size {
355                return None;
356            }
357
358            Some(GenericTextRunProperties::with_all(
359                run_props.typeface().clone(),
360                em_size,
361                run_props.text_decorations().cloned(),
362                run_props.foreground_brush().cloned(),
363                run_props.background_brush().cloned(),
364                run_props.baseline_alignment(),
365                run_props.culture_info().cloned(),
366                run_props.font_features().cloned(),
367            ))
368        });
369    }
370
371    /// Sets or changes the culture for the text object.
372    ///
373    /// * `culture` — the new culture for the text object.
374    pub fn set_culture(&self, culture: CultureInfo) {
375        self.set_culture_range(culture, 0, self.text_length());
376    }
377
378    /// Sets or changes the culture for the text object.
379    ///
380    /// * `culture` — the new culture for the text object.
381    /// * `start_index` — the start index of initial character to apply the change to.
382    /// * `count` — the number of characters the change should be applied to.
383    pub fn set_culture_range(&self, culture: CultureInfo, start_index: i32, count: i32) {
384        self.set_run_properties(start_index, count, true, |run_props| {
385            if run_props.culture_info() == Some(&culture) {
386                return None;
387            }
388
389            Some(GenericTextRunProperties::with_all(
390                run_props.typeface().clone(),
391                run_props.font_rendering_em_size(),
392                run_props.text_decorations().cloned(),
393                run_props.foreground_brush().cloned(),
394                run_props.background_brush().cloned(),
395                run_props.baseline_alignment(),
396                Some(culture.clone()),
397                run_props.font_features().cloned(),
398            ))
399        });
400    }
401
402    /// Sets or changes the font weight.
403    ///
404    /// * `weight` — font weight
405    pub fn set_font_weight(&self, weight: FontWeight) {
406        self.set_font_weight_range(weight, 0, self.text_length());
407    }
408
409    /// Sets or changes the font weight.
410    ///
411    /// * `weight` — font weight
412    /// * `start_index` — the start index of initial character to apply the change to.
413    /// * `count` — the number of characters the change should be applied to.
414    pub fn set_font_weight_range(&self, weight: FontWeight, start_index: i32, count: i32) {
415        self.set_run_properties(start_index, count, true, |run_props| {
416            let old_typeface = run_props.typeface();
417
418            if old_typeface.weight() == weight {
419                return None;
420            }
421
422            Some(GenericTextRunProperties::with_all(
423                Typeface::with_style(
424                    old_typeface.font_family().clone(),
425                    old_typeface.style(),
426                    weight,
427                    FontStretch::Normal,
428                ),
429                run_props.font_rendering_em_size(),
430                run_props.text_decorations().cloned(),
431                run_props.foreground_brush().cloned(),
432                run_props.background_brush().cloned(),
433                run_props.baseline_alignment(),
434                run_props.culture_info().cloned(),
435                run_props.font_features().cloned(),
436            ))
437        });
438    }
439
440    /// Sets or changes the font style.
441    ///
442    /// * `style` — font style
443    pub fn set_font_style(&self, style: FontStyle) {
444        self.set_font_style_range(style, 0, self.text_length());
445    }
446
447    /// Sets or changes the font style.
448    ///
449    /// * `style` — font style
450    /// * `start_index` — the start index of initial character to apply the change to.
451    /// * `count` — the number of characters the change should be applied to.
452    pub fn set_font_style_range(&self, style: FontStyle, start_index: i32, count: i32) {
453        self.set_run_properties(start_index, count, true, |run_props| {
454            let old_typeface = run_props.typeface();
455
456            if old_typeface.style() == style {
457                return None;
458            }
459
460            Some(GenericTextRunProperties::with_all(
461                Typeface::with_style(
462                    old_typeface.font_family().clone(),
463                    style,
464                    old_typeface.weight(),
465                    FontStretch::Normal,
466                ),
467                run_props.font_rendering_em_size(),
468                run_props.text_decorations().cloned(),
469                run_props.foreground_brush().cloned(),
470                run_props.background_brush().cloned(),
471                run_props.baseline_alignment(),
472                run_props.culture_info().cloned(),
473                run_props.font_features().cloned(),
474            ))
475        });
476    }
477
478    /// Sets or changes the type face.
479    ///
480    /// * `typeface` — typeface
481    pub fn set_font_typeface(&self, typeface: Typeface) {
482        self.set_font_typeface_range(typeface, 0, self.text_length());
483    }
484
485    /// Sets or changes the type face.
486    ///
487    /// * `typeface` — typeface
488    /// * `start_index` — the start index of initial character to apply the change to.
489    /// * `count` — the number of characters the change should be applied to.
490    pub fn set_font_typeface_range(&self, typeface: Typeface, start_index: i32, count: i32) {
491        self.set_run_properties(start_index, count, true, |run_props| {
492            if *run_props.typeface() == typeface {
493                return None;
494            }
495
496            Some(GenericTextRunProperties::with_all(
497                typeface.clone(),
498                run_props.font_rendering_em_size(),
499                run_props.text_decorations().cloned(),
500                run_props.foreground_brush().cloned(),
501                run_props.background_brush().cloned(),
502                run_props.baseline_alignment(),
503                run_props.culture_info().cloned(),
504                run_props.font_features().cloned(),
505            ))
506        });
507    }
508
509    /// Sets or changes the text decorations.
510    ///
511    /// * `text_decorations` — text decorations
512    pub fn set_text_decorations(&self, text_decorations: TextDecorationCollection) {
513        self.set_text_decorations_range(text_decorations, 0, self.text_length());
514    }
515
516    /// Sets or changes the text decorations.
517    ///
518    /// * `text_decorations` — text decorations
519    /// * `start_index` — the start index of initial character to apply the change to.
520    /// * `count` — the number of characters the change should be applied to.
521    pub fn set_text_decorations_range(&self, text_decorations: TextDecorationCollection, start_index: i32, count: i32) {
522        self.set_run_properties(start_index, count, false, |run_props| {
523            if run_props.text_decorations() == Some(&text_decorations) {
524                return None;
525            }
526
527            Some(GenericTextRunProperties::with_all(
528                run_props.typeface().clone(),
529                run_props.font_rendering_em_size(),
530                Some(text_decorations.clone()),
531                run_props.foreground_brush().cloned(),
532                run_props.background_brush().cloned(),
533                run_props.baseline_alignment(),
534                run_props.culture_info().cloned(),
535                run_props.font_features().cloned(),
536            ))
537        });
538    }
539
540    /// Returns an enumerator that can iterate through the text line collection.
541    fn get_enumerator(&self) -> LineEnumerator<'_> {
542        LineEnumerator::new(self)
543    }
544
545    fn advance_line_origin(&self, line_origin: &mut Point, current_line: &dyn TextLine) {
546        let height = current_line.height();
547
548        // advance line origin according to the flow direction
549        match self.default_para_props.flow_direction() {
550            FlowDirection::LeftToRight | FlowDirection::RightToLeft => {
551                *line_origin = line_origin.with_y(line_origin.y + height);
552            }
553        }
554    }
555
556    /// Defines the flow direction.
557    pub fn flow_direction(&self) -> FlowDirection {
558        self.default_para_props.flow_direction()
559    }
560
561    /// Defines the flow direction.
562    pub fn set_flow_direction(&self, value: FlowDirection) {
563        self.default_para_props.set_flow_direction(value);
564        self.invalidate_metrics();
565    }
566
567    /// Defines the alignment of text within the column.
568    pub fn text_alignment(&self) -> TextAlignment {
569        self.default_para_props.text_alignment()
570    }
571
572    /// Defines the alignment of text within the column.
573    pub fn set_text_alignment(&self, value: TextAlignment) {
574        self.default_para_props.set_text_alignment(value);
575        self.invalidate_metrics();
576    }
577
578    /// Gets the height of, or the spacing between, each line where zero
579    /// represents the default line height.
580    pub fn line_height(&self) -> f64 {
581        self.default_para_props.line_height()
582    }
583
584    /// Sets the height of, or the spacing between, each line where zero
585    /// represents the default line height.
586    ///
587    /// Panics when the value is negative.
588    pub fn set_line_height(&self, value: f64) {
589        if value < 0.0 {
590            panic!("value: Parameter must be greater than or equal to zero.");
591        }
592
593        self.default_para_props.set_line_height(value);
594
595        self.invalidate_metrics();
596    }
597
598    /// The MaxTextWidth property defines the alignment edges for the FormattedText.
599    /// For example, left aligned text is wrapped such that the leftmost glyph alignment point
600    /// on each line falls exactly on the left edge of the rectangle.
601    /// Note that for many fonts, especially in italic style, some glyph strokes may extend beyond
602    /// the edges of the alignment rectangle. For this reason, it is recommended that clients draw
603    /// text with at least 1/6 em (i.e of the font size) unused margin space either side.
604    /// Zero value of MaxTextWidth is equivalent to the maximum possible paragraph width.
605    pub fn max_text_width(&self) -> f64 {
606        self.max_text_width.get()
607    }
608
609    /// Sets the maximum text width. Panics when the value is negative.
610    pub fn set_max_text_width(&self, value: f64) {
611        if value < 0.0 {
612            panic!("value: Parameter must be greater than or equal to zero.");
613        }
614
615        self.max_text_width.set(value);
616
617        self.invalidate_metrics();
618    }
619
620    /// Sets the array of lengths, which will be applied to each line of text in turn.
621    /// If the text covers more lines than there are entries in the length array,
622    /// the last entry is reused as many times as required.
623    /// The max text widths array overrides the MaxTextWidth property.
624    ///
625    /// Panics when the array is empty.
626    ///
627    /// * `max_text_widths` — the max text width array
628    pub fn set_max_text_widths(&self, max_text_widths: &[f64]) {
629        if max_text_widths.is_empty() {
630            panic!("max_text_widths must not be empty.");
631        }
632
633        *self.max_text_widths.borrow_mut() = Some(max_text_widths.to_vec());
634
635        self.invalidate_metrics();
636    }
637
638    /// Obtains a copy of the array of lengths, which will be applied to each line of text in turn.
639    /// If the text covers more lines than there are entries in the length array,
640    /// the last entry is reused as many times as required.
641    /// The max text widths array overrides the MaxTextWidth property.
642    pub fn get_max_text_widths(&self) -> Vec<f64> {
643        self.max_text_widths.borrow().clone().unwrap_or_default()
644    }
645
646    /// Gets the maximum length of a column of text.
647    /// The last line of text displayed is the last whole line that will fit within this limit,
648    /// or the nth line as specified by MaxLineCount, whichever occurs first.
649    /// Use the Trimming property to control how the omission of text is indicated.
650    pub fn max_text_height(&self) -> f64 {
651        self.max_text_height.get()
652    }
653
654    /// Sets the maximum length of a column of text. Panics when the value is
655    /// not greater than zero or is NaN.
656    pub fn set_max_text_height(&self, value: f64) {
657        if value <= 0.0 {
658            panic!("value: 'MaxTextHeight' property value must be greater than zero.");
659        }
660
661        if value.is_nan() {
662            panic!("value: 'MaxTextHeight' property value cannot be NaN.");
663        }
664
665        self.max_text_height.set(value);
666
667        self.invalidate_metrics();
668    }
669
670    /// Defines the maximum number of lines to display.
671    /// The last line of text displayed is the lineCount-1'th line,
672    /// or the last whole line that will fit within the count set by MaxTextHeight,
673    /// whichever occurs first.
674    /// Use the Trimming property to control how the omission of text is indicated.
675    pub fn max_line_count(&self) -> i32 {
676        self.max_line_count.get()
677    }
678
679    /// Sets the maximum number of lines to display. Panics when the value is
680    /// not greater than zero.
681    pub fn set_max_line_count(&self, value: i32) {
682        if value <= 0 {
683            panic!("value: The parameter value must be greater than zero.");
684        }
685
686        self.max_line_count.set(value);
687
688        self.invalidate_metrics();
689    }
690
691    /// Defines how omission of text is indicated.
692    /// CharacterEllipsis trimming allows partial words to be displayed,
693    /// while WordEllipsis removes whole words to fit.
694    /// Both guarantee to include an ellipsis ('...') at the end of the lines
695    /// where text has been trimmed as a result of line and column limits.
696    pub fn trimming(&self) -> Rc<dyn TextTrimming> {
697        self.trimming.borrow().clone()
698    }
699
700    /// Defines how omission of text is indicated.
701    pub fn set_trimming(&self, value: Rc<dyn TextTrimming>) {
702        *self.trimming.borrow_mut() = value;
703
704        self.default_para_props.set_text_wrapping(if self.is_trimming_none() {
705            TextWrapping::Wrap
706        } else {
707            TextWrapping::WrapWithOverflow
708        });
709
710        self.invalidate_metrics();
711    }
712
713    /// `_trimming == TextTrimming.None`.
714    fn is_trimming_none(&self) -> bool {
715        Rc::ptr_eq(&self.trimming.borrow(), &<dyn TextTrimming>::none())
716    }
717
718    /// Lazily initializes the cached metrics EXCEPT for black box metrics and
719    /// returns the CachedMetrics structure.
720    fn metrics(&self) -> CachedMetrics {
721        if let Some(metrics) = self.metrics.get() {
722            return metrics;
723        }
724
725        let metrics = self.draw_and_calculate_metrics(
726            None,            // drawing context
727            Point::default(), // drawing offset
728            false,
729        );
730
731        self.metrics.set(Some(metrics));
732
733        metrics
734    }
735
736    /// Lazily initializes the cached metrics INCLUDING black box metrics and
737    /// returns the CachedMetrics structure.
738    fn black_box_metrics(&self) -> CachedMetrics {
739        match self.metrics.get() {
740            Some(metrics) if !metrics.extent.is_nan() => metrics,
741            _ => {
742                // We need to obtain the metrics, including black box metrics.
743
744                let metrics = self.draw_and_calculate_metrics(
745                    None,             // drawing context
746                    Point::default(), // drawing offset
747                    true,             // calculate black box metrics
748                );
749
750                self.metrics.set(Some(metrics));
751
752                metrics
753            }
754        }
755    }
756
757    /// The distance from the top of the first line to the bottom of the last line.
758    pub fn height(&self) -> f64 {
759        self.metrics().height
760    }
761
762    /// The distance from the topmost black pixel of the first line to the
763    /// bottommost black pixel of the last line.
764    pub fn extent(&self) -> f64 {
765        self.black_box_metrics().extent
766    }
767
768    /// The distance from the top of the first line to the baseline of the first line.
769    pub fn baseline(&self) -> f64 {
770        self.metrics().baseline
771    }
772
773    /// The distance from the bottom of the last line to the extent bottom.
774    pub fn overhang_after(&self) -> f64 {
775        self.black_box_metrics().overhang_after
776    }
777
778    /// The maximum distance from the leading black pixel to the leading alignment point of a line.
779    pub fn overhang_leading(&self) -> f64 {
780        self.black_box_metrics().overhang_leading
781    }
782
783    /// The maximum distance from the trailing black pixel to the trailing alignment point of a line.
784    pub fn overhang_trailing(&self) -> f64 {
785        self.black_box_metrics().overhang_trailing
786    }
787
788    /// The maximum advance width between the leading and trailing alignment points of a line,
789    /// excluding the width of whitespace characters at the end of the line.
790    pub fn width(&self) -> f64 {
791        self.metrics().width
792    }
793
794    /// The maximum advance width between the leading and trailing alignment points of a line,
795    /// including the width of whitespace characters at the end of the line.
796    pub fn width_including_trailing_whitespace(&self) -> f64 {
797        self.metrics().width_including_trailing_whitespace
798    }
799
800    /// Obtains geometry for the text, including underlines and strikethroughs.
801    ///
802    /// * `origin` — the left top origin of the resulting geometry.
803    ///
804    /// Returns the geometry object containing the outline of the formatted text: the union
805    /// of all of the glyphs, underlines and strikethroughs that represent the formatted text.
806    pub fn build_geometry(&self, origin: Point) -> Option<Ref<Geometry>> {
807        let mut accumulated_geometry: Option<Ref<GeometryGroup>> = None;
808        let mut line_origin = origin;
809
810        let drawing = DrawingGroup::new();
811
812        {
813            let mut ctx = drawing.open();
814
815            {
816                let mut enumerator = self.get_enumerator();
817
818                while enumerator.move_next() {
819                    let Some(current_line) = enumerator.current() else {
820                        continue;
821                    };
822
823                    current_line.draw(&mut ctx, line_origin);
824
825                    self.advance_line_origin(&mut line_origin, &*current_line);
826                }
827
828                enumerator.dispose();
829            }
830
831            ctx.dispose();
832        }
833
834        let mut transform: Option<Ref<Transform>> = Some(TranslateTransform::with_offset(origin.x, origin.y).upcast());
835
836        //  recursively go down the DrawingGroup to build up the geometry
837        Self::combine_geometry_recursive(&drawing.upcast(), &mut transform, &mut accumulated_geometry);
838
839        accumulated_geometry.map(|geometry| geometry.upcast())
840    }
841
842    fn combine_geometry_recursive(
843        drawing: &Ref<Drawing>,
844        transform: &mut Option<Ref<Transform>>,
845        accumulated_geometry: &mut Option<Ref<GeometryGroup>>,
846    ) {
847        fn accumulated(accumulated_geometry: &mut Option<Ref<GeometryGroup>>) -> &Ref<GeometryGroup> {
848            accumulated_geometry.get_or_insert_with(|| {
849                let group = GeometryGroup::new();
850                group.set_fill_rule(FillRule::NonZero);
851                group
852            })
853        }
854
855        if let Some(group) = drawing.cast::<DrawingGroup>() {
856            *transform = group.transform();
857
858            // recursively go down for DrawingGroup
859            for child in group.children().iter() {
860                Self::combine_geometry_recursive(&child, transform, accumulated_geometry);
861            }
862        } else if let Some(glyph_run_drawing) = drawing.cast::<GlyphRunDrawing>() {
863            // process glyph run
864            if let Some(glyph_run) = glyph_run_drawing.glyph_run() {
865                let glyph_run_geometry = glyph_run.build_geometry();
866
867                glyph_run_geometry.set_transform(transform.clone());
868
869                accumulated(accumulated_geometry).children().add(glyph_run_geometry);
870            }
871        } else if let Some(geometry_drawing) = drawing.cast::<GeometryDrawing>() {
872            // process geometry (i.e. TextDecoration on the line)
873            if let Some(mut geometry) = geometry_drawing.geometry() {
874                geometry.set_transform(transform.clone());
875
876                if let Some(line_geometry) = geometry.cast::<LineGeometry>() {
877                    // For TextDecoration drawn by DrawLine(), the geometry is a LineGeometry which has no
878                    // bounding area. So this line won't show up. Work aroud it by increase the Bounding rect
879                    // to be Pen's thickness
880
881                    let mut bounds = line_geometry.bounds();
882
883                    let thickness = geometry_drawing.pen().map_or(0.0, |pen| pen.thickness());
884
885                    if bounds.height == 0.0 {
886                        bounds = bounds.with_height(thickness);
887                    } else if bounds.width == 0.0 {
888                        bounds = bounds.with_width(thickness);
889                    }
890
891                    // convert the line geometry into a rectangle geometry
892                    // we lost line cap info here
893                    geometry = RectangleGeometry::with_rect(bounds).upcast();
894                }
895
896                accumulated(accumulated_geometry).children().add(geometry);
897            }
898        }
899    }
900
901    /// Builds a highlight geometry object.
902    ///
903    /// * `origin` — the origin of the highlight region
904    ///
905    /// Returns geometry that surrounds the text.
906    pub fn build_highlight_geometry(&self, origin: Point) -> Option<Ref<Geometry>> {
907        self.build_highlight_geometry_range(origin, 0, self.text_length())
908    }
909
910    /// Builds a highlight geometry object for a given character range.
911    ///
912    /// * `origin` — the origin of the highlight region.
913    /// * `start_index` — the start index of initial character the bounds should be obtained for.
914    /// * `count` — the number of characters the bounds should be obtained for.
915    ///
916    /// Returns geometry that surrounds the specified character range.
917    pub fn build_highlight_geometry_range(&self, origin: Point, start_index: i32, count: i32) -> Option<Ref<Geometry>> {
918        self.validate_range(start_index, count);
919
920        let mut accumulated_bounds: Option<Ref<Geometry>> = None;
921
922        {
923            let mut enumerator = self.get_enumerator();
924
925            let mut line_origin = origin;
926
927            while enumerator.move_next() {
928                let Some(current_line) = enumerator.current() else {
929                    continue;
930                };
931
932                let x0 = enumerator.position().max(start_index);
933                let x1 = (enumerator.position() + enumerator.length()).min(start_index + count);
934
935                // check if this line is intersects with the specified character range
936                if x0 < x1 {
937                    let highlight_bounds = current_line.get_text_bounds(x0, x1 - x0);
938
939                    for bound in &highlight_bounds {
940                        let mut rect = bound.rectangle();
941
942                        if self.flow_direction() == FlowDirection::RightToLeft {
943                            // Convert logical units (which extend leftward from the right edge
944                            // of the paragraph) to physical units.
945                            //
946                            // Note that since rect is in logical units, rect.Right corresponds to
947                            // the visual *left* edge of the rectangle in the RTL case. Specifically,
948                            // is the distance leftward from the right edge of the formatting rectangle
949                            // whose width is the paragraph width passed to FormatLine.
950                            //
951                            rect = rect.with_x(enumerator.current_paragraph_width() - rect.right());
952                        }
953
954                        rect = Rect::new(rect.x + line_origin.x, rect.y + line_origin.y, rect.width, rect.height);
955
956                        let rectangle_geometry = RectangleGeometry::with_rect(rect);
957
958                        accumulated_bounds = Some(match accumulated_bounds {
959                            None => rectangle_geometry.upcast(),
960                            Some(accumulated_bounds) => Geometry::combine(
961                                accumulated_bounds,
962                                &rectangle_geometry,
963                                GeometryCombineMode::Union,
964                                None::<Ref<Transform>>,
965                            ),
966                        });
967                    }
968                }
969
970                self.advance_line_origin(&mut line_origin, &*current_line);
971            }
972
973            enumerator.dispose();
974        }
975
976        let accumulated_bounds = accumulated_bounds?;
977
978        let bounds = accumulated_bounds.platform_impl()?.bounds();
979
980        if bounds.width == 0.0 && bounds.height == 0.0 {
981            return None;
982        }
983
984        Some(accumulated_bounds)
985    }
986
987    /// Draws the text object.
988    pub(crate) fn draw(&self, drawing_context: &mut dyn ITextDrawingSink, origin: Point) {
989        let mut line_origin = origin;
990
991        if self.metrics.get().is_some_and(|metrics| !metrics.extent.is_nan()) {
992            let mut enumerator = self.get_enumerator();
993
994            while enumerator.move_next() {
995                let Some(current_line) = enumerator.current() else {
996                    continue;
997                };
998
999                current_line.draw(drawing_context, line_origin);
1000
1001                self.advance_line_origin(&mut line_origin, &*current_line);
1002            }
1003
1004            enumerator.dispose();
1005        } else {
1006            // Calculate metrics as we draw to avoid formatting again if we need metrics later; we compute
1007            // black box metrics too because these are already known as a side-effect of drawing
1008
1009            let metrics = self.draw_and_calculate_metrics(Some(drawing_context), origin, true);
1010
1011            self.metrics.set(Some(metrics));
1012        }
1013    }
1014
1015    fn draw_and_calculate_metrics(
1016        &self,
1017        mut drawing_context: Option<&mut dyn ITextDrawingSink>,
1018        drawing_offset: Point,
1019        get_black_box_metrics: bool,
1020    ) -> CachedMetrics {
1021        let mut metrics = CachedMetrics::default();
1022
1023        if self.text.is_empty() {
1024            return metrics;
1025        }
1026
1027        let mut enumerator = self.get_enumerator();
1028
1029        let mut first = true;
1030
1031        let mut acc_black_box_left = f64::MAX;
1032        let mut acc_black_box_top = f64::MAX;
1033        let mut acc_black_box_right = f64::MIN;
1034        let mut acc_black_box_bottom = f64::MIN;
1035
1036        let mut origin = Point::new(0.0, 0.0);
1037
1038        // Holds the TextLine.Start of the longest line. Thus it will hold the minimum value
1039        // of TextLine.Start among all the lines that forms the text. The overhangs (leading and trailing)
1040        // are calculated with an offset as a result of the same issue with TextLine.Start.
1041        // So, we compute this offset and remove it later from the values of the overhangs.
1042        let mut line_start_of_longest_line = f64::MAX;
1043
1044        while enumerator.move_next() {
1045            let Some(current_line) = enumerator.current() else {
1046                continue;
1047            };
1048
1049            // if we're drawing, do it first as this will compute black box metrics as a side-effect
1050            if let Some(drawing_context) = drawing_context.as_deref_mut() {
1051                current_line
1052                    .draw(drawing_context, Point::new(origin.x + drawing_offset.x, origin.y + drawing_offset.y));
1053            }
1054
1055            if get_black_box_metrics {
1056                let black_box_left = origin.x + current_line.start() + current_line.overhang_leading();
1057                let black_box_right =
1058                    origin.x + current_line.start() + current_line.width() - current_line.overhang_trailing();
1059                let black_box_bottom = origin.y + current_line.height() + current_line.overhang_after();
1060                let black_box_top = black_box_bottom - current_line.extent();
1061
1062                acc_black_box_left = acc_black_box_left.min(black_box_left);
1063                acc_black_box_right = acc_black_box_right.max(black_box_right);
1064                acc_black_box_bottom = acc_black_box_bottom.max(black_box_bottom);
1065                acc_black_box_top = acc_black_box_top.min(black_box_top);
1066
1067                metrics.overhang_after = current_line.overhang_after();
1068            }
1069
1070            metrics.height += current_line.height();
1071            metrics.width = metrics.width.max(current_line.width());
1072            metrics.width_including_trailing_whitespace = metrics
1073                .width_including_trailing_whitespace
1074                .max(current_line.width_including_trailing_whitespace());
1075            line_start_of_longest_line = line_start_of_longest_line.min(current_line.start());
1076
1077            if first {
1078                metrics.baseline = current_line.baseline();
1079                first = false;
1080            }
1081
1082            self.advance_line_origin(&mut origin, &*current_line);
1083        }
1084
1085        if get_black_box_metrics {
1086            metrics.extent = acc_black_box_bottom - acc_black_box_top;
1087            metrics.overhang_leading = acc_black_box_left - line_start_of_longest_line;
1088            metrics.overhang_trailing = metrics.width - (acc_black_box_right - line_start_of_longest_line);
1089        } else {
1090            // indicate that black box metrics are not known
1091            metrics.extent = f64::NAN;
1092        }
1093
1094        enumerator.dispose();
1095
1096        metrics
1097    }
1098}
1099
1100/// Enumerator used for enumerating text lines.
1101struct LineEnumerator<'a> {
1102    line_count: i32,
1103    total_height: f64,
1104    next_line: Option<Rc<dyn TextLine>>,
1105    formatter: Rc<dyn TextFormatter>,
1106    that: &'a FormattedText,
1107    text_source: TextSourceImplementation<'a>,
1108    para_props: Rc<dyn TextParagraphProperties>,
1109
1110    // these are needed because the current line can be disposed before the next move_next() call
1111    previous_height: f64,
1112
1113    // line break before the current line, needed in case we have to reformat it with collapsing symbol
1114    previous_line_break: Option<Rc<TextLineBreak>>,
1115    position: i32,
1116    length: i32,
1117
1118    current: Option<Rc<dyn TextLine>>,
1119}
1120
1121impl<'a> LineEnumerator<'a> {
1122    fn new(text: &'a FormattedText) -> Self {
1123        Self {
1124            previous_height: 0.0,
1125            length: 0,
1126            previous_line_break: None,
1127            position: 0,
1128            line_count: 0,
1129            total_height: 0.0,
1130            current: None,
1131            next_line: None,
1132            formatter: <dyn TextFormatter>::current(),
1133            that: text,
1134            text_source: TextSourceImplementation::new(text),
1135            para_props: text.default_para_props.clone(),
1136        }
1137    }
1138
1139    fn dispose(&mut self) {
1140        self.current = None;
1141
1142        self.next_line = None;
1143    }
1144
1145    fn position(&self) -> i32 {
1146        self.position
1147    }
1148
1149    fn length(&self) -> i32 {
1150        self.length
1151    }
1152
1153    /// Gets the current text line in the collection.
1154    fn current(&self) -> Option<Rc<dyn TextLine>> {
1155        self.current.clone()
1156    }
1157
1158    /// Gets the paragraph width used to format the current text line.
1159    fn current_paragraph_width(&self) -> f64 {
1160        self.max_line_length(self.line_count)
1161    }
1162
1163    fn max_line_length(&self, line: i32) -> f64 {
1164        match &*self.that.max_text_widths.borrow() {
1165            None => self.that.max_text_width.get(),
1166            Some(max_text_widths) => max_text_widths[(line as usize).min(max_text_widths.len() - 1)],
1167        }
1168    }
1169
1170    /// Advances the enumerator to the next text line of the collection.
1171    ///
1172    /// Returns true if the enumerator was successfully advanced to the next
1173    /// element; false if the enumerator has passed the end of the collection.
1174    fn move_next(&mut self) -> bool {
1175        let that = self.that;
1176
1177        let current = match self.current.take() {
1178            None => {
1179                // this is the first line
1180                if that.text.is_empty() {
1181                    return false;
1182                }
1183
1184                let current = self.format_line(
1185                    self.position,
1186                    self.max_line_length(self.line_count),
1187                    None, // no previous line break
1188                );
1189
1190                let Some(current) = current else {
1191                    return false;
1192                };
1193
1194                // check if this line fits the text height
1195                if self.total_height + current.height() > that.max_text_height.get() {
1196                    return false;
1197                }
1198
1199                debug_assert!(self.next_line.is_none());
1200
1201                current
1202            }
1203            Some(previous) => {
1204                // there is no next line or it didn't fit
1205                // either way we're finished
1206                let Some(next_line) = self.next_line.take() else {
1207                    self.current = Some(previous);
1208
1209                    return false;
1210                };
1211
1212                self.total_height += self.previous_height;
1213                self.position += self.length;
1214                self.line_count += 1;
1215
1216                next_line
1217            }
1218        };
1219
1220        let mut current = Some(current);
1221
1222        let mut current_line_break = current.as_ref().and_then(|current| current.text_line_break());
1223
1224        if let Some(current_line) = current.clone() {
1225            // this line is guaranteed to fit the text height
1226            debug_assert!(self.total_height + current_line.height() <= that.max_text_height.get());
1227
1228            // now, check if the next line fits, we need to do this on this iteration
1229            // because we might need to add ellipsis to the current line
1230            // as a result of the next line measurement
1231
1232            // maybe there is no next line at all
1233            if self.position + current_line.length() < that.text_length() {
1234                let mut next_line_fits = false;
1235
1236                if self.line_count + 1 >= that.max_line_count.get() {
1237                    next_line_fits = false;
1238                } else {
1239                    self.next_line = self.format_line(
1240                        self.position + current_line.length(),
1241                        self.max_line_length(self.line_count + 1),
1242                        current_line_break.as_ref(),
1243                    );
1244
1245                    if let Some(next_line) = &self.next_line {
1246                        next_line_fits = self.total_height + current_line.height() + next_line.height()
1247                            <= that.max_text_height.get();
1248                    }
1249                }
1250
1251                if !next_line_fits {
1252                    self.next_line = None;
1253
1254                    if !that.is_trimming_none() && !current_line.has_collapsed() {
1255                        // recreate the current line with ellipsis added
1256                        // Note: Paragraph ellipsis is not supported today. We'll workaround
1257                        // it here by faking a non-wrap text on finite column width.
1258                        let current_wrap = that.default_para_props.text_wrapping();
1259
1260                        that.default_para_props.set_text_wrapping(TextWrapping::NoWrap);
1261
1262                        current = self.format_line(
1263                            self.position,
1264                            self.max_line_length(self.line_count),
1265                            self.previous_line_break.as_ref(),
1266                        );
1267
1268                        if let Some(current) = &current {
1269                            current_line_break = current.text_line_break();
1270                        }
1271
1272                        that.default_para_props.set_text_wrapping(current_wrap);
1273                    }
1274                }
1275            }
1276        }
1277
1278        if let Some(current) = &current {
1279            self.previous_height = current.height();
1280
1281            self.length = current.length();
1282        }
1283
1284        self.current = current;
1285
1286        self.previous_line_break = current_line_break;
1287
1288        true
1289    }
1290
1291    /// Wrapper of `TextFormatter::format_line` that auto-collapses the line if needed.
1292    fn format_line(
1293        &self,
1294        text_source_position: i32,
1295        max_line_length: f64,
1296        line_break: Option<&Rc<TextLineBreak>>,
1297    ) -> Option<Rc<dyn TextLine>> {
1298        let that = self.that;
1299
1300        let mut line = self.formatter.format_line(
1301            &self.text_source,
1302            text_source_position,
1303            max_line_length,
1304            &self.para_props,
1305            line_break,
1306        );
1307
1308        if let Some(overflowed_line) = &line {
1309            if !that.is_trimming_none() && overflowed_line.has_overflowed() && overflowed_line.length() > 0 {
1310                // what I really need here is the last displayed text run of the line
1311                // text_source_position + line.length - 1 works except the end of paragraph case,
1312                // where line length includes the fake paragraph break run
1313                debug_assert!(
1314                    that.text_length() > 0
1315                        && text_source_position + overflowed_line.length() <= that.text_length() + 1
1316                );
1317
1318                let last_run_props = {
1319                    let format_runs = that.format_runs.borrow();
1320
1321                    let that_format_rider = SpanRider::new(
1322                        &format_runs,
1323                        that.latest_position.get(),
1324                        (text_source_position + overflowed_line.length() - 1).min(that.text_length() - 1),
1325                    );
1326
1327                    that_format_rider.current_element().cloned().expect("the format runs cover the text")
1328                };
1329
1330                let collapsing_properties =
1331                    that.trimming.borrow().create_collapsing_properties(&TextCollapsingCreateInfo::new(
1332                        max_line_length,
1333                        last_run_props,
1334                        self.para_props.flow_direction(),
1335                    ));
1336
1337                let collapsed_line = overflowed_line.clone().collapse(&[Some(collapsing_properties)]);
1338
1339                line = Some(collapsed_line);
1340            }
1341        }
1342
1343        line
1344    }
1345
1346    /// Sets the enumerator to its initial position, which is before the first
1347    /// element in the collection.
1348    #[allow(dead_code)] // upstream member nothing uses
1349    fn reset(&mut self) {
1350        self.position = 0;
1351        self.line_count = 0;
1352        self.total_height = 0.0;
1353        self.current = None;
1354        self.next_line = None;
1355    }
1356}
1357
1358#[derive(Clone, Copy, Default)]
1359struct CachedMetrics {
1360    // vertical
1361    height: f64,
1362    baseline: f64,
1363
1364    // horizontal
1365    width: f64,
1366    width_including_trailing_whitespace: f64,
1367
1368    // vertical bounding box metrics
1369    extent: f64,
1370    overhang_after: f64,
1371
1372    // horizontal bounding box metrics
1373    overhang_leading: f64,
1374    overhang_trailing: f64,
1375}
1376
1377/// The text source of a formatted text: its text with the properties of the
1378/// format runs.
1379struct TextSourceImplementation<'a> {
1380    that: &'a FormattedText,
1381}
1382
1383impl<'a> TextSourceImplementation<'a> {
1384    fn new(text: &'a FormattedText) -> Self {
1385        Self { that: text }
1386    }
1387}
1388
1389impl ITextSource for TextSourceImplementation<'_> {
1390    fn get_text_run(&self, text_source_character_index: i32) -> Option<Rc<dyn TextRun>> {
1391        let that = self.that;
1392
1393        if text_source_character_index >= that.text_length() {
1394            return Some(Rc::new(TextEndOfParagraph::new()));
1395        }
1396
1397        let format_runs = that.format_runs.borrow();
1398
1399        let that_format_rider = SpanRider::new(&format_runs, that.latest_position.get(), text_source_character_index);
1400
1401        let text = that.text.slice(text_source_character_index as usize, that_format_rider.length() as usize);
1402
1403        let properties = that_format_rider.current_element().cloned().expect("the format runs cover the text");
1404
1405        Some(Rc::new(TextCharacters::new(text, properties)))
1406    }
1407}
1408
1409#[cfg(test)]
1410#[path = "formatted_text_tests.rs"]
1411mod tests;