Skip to main content

ferroui_controls/
text_block.rs

1use crate::utils::debug_display::{append_optional_value, build_base_debug_display};
2use crate::documents::{EmbeddedControlRun, IInlineHost, Inline, InlineCollection, TextElement};
3use crate::{Border, Control, ControlImpl, Decorator, TextLayoutRoundingPlacement};
4use ferroui_base::collections::FerroList;
5use ferroui_base::input::InputElementImpl;
6use ferroui_base::interactivity::InteractiveImpl;
7use ferroui_base::layout::{LayoutHelper, LayoutableImpl, LayoutableImplExt, VerticalAlignment};
8use ferroui_base::media::text_formatting::{
9    FormattedTextSource, GenericTextParagraphProperties, GenericTextRunProperties, ITextSource, TextCharacters,
10    TextEndOfParagraph, TextLayout, TextLineImpl, TextRun, TextRunCache, TextRunProperties,
11};
12use ferroui_base::media::{
13    DrawingContext, FlowDirection, FontFamily, FontFeatureCollection, FontStretch, FontStyle, FontWeight, IBrush, TextAlignment,
14    TextDecorationCollection, TextTrimming, TextWrapping, Typeface,
15};
16use ferroui_base::reactive::IDisposable;
17use ferroui_base::utilities::{ReadOnlyMemory, ValueSpan};
18use ferroui_base::{
19    ferro_class, ferro_property, instantiate, AttachedProperty, DirectProperty, FerroObjectImpl, FerroObjectImplExt,
20    FerroProperty, FerroPropertyChangedEventArgs, Point, Rect, Ref, Size, StyledElementImpl, StyledProperty,
21    StyledPropertyOptions, Thickness, Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs, WeakRef,
22};
23use std::cell::{Cell, OnceCell, RefCell};
24use std::rc::Rc;
25
26/// A control that displays a block of text.
27#[repr(C)]
28pub struct TextBlock {
29    base: Control,
30    text_layout: RefCell<Option<Rc<TextLayout>>>,
31    text_run_cache: RefCell<Option<Rc<TextRunCache>>>,
32    constraint: Cell<Size>,
33    text_runs: RefCell<Option<Rc<Vec<Rc<dyn TextRun>>>>>,
34    inlines: RefCell<Option<InlineCollection>>,
35    inlines_invalidated: RefCell<Option<Rc<dyn IDisposable>>>,
36    inline_host: OnceCell<Rc<dyn IInlineHost>>,
37    clear_text_internal: Cell<bool>,
38    /// The (resolved) text alignment the cached layout was created with.
39    layout_text_alignment: Cell<TextAlignment>,
40    /// The UTF-16 form of the text the last layout was created for, so that a
41    /// new layout of unchanged text (a new constraint, an arrange) does not
42    /// transcode it again.
43    utf16_text: RefCell<Option<(String, ReadOnlyMemory<u16>)>>,
44}
45
46ferro_class! {
47    TextBlock: Control, virtuals TextBlockImpl: ControlImpl {
48        /// Renders the control (the overridable part of the sealed `render`).
49        #[doc(hidden)]
50        fn render_core(this, context: &mut DrawingContext);
51        /// Renders the text layout to a drawing context.
52        fn render_text_layout(this, context: &mut DrawingContext, origin: Point);
53        /// Creates the `TextLayout` used to render the text.
54        fn create_text_layout(this, text: Option<&str>) -> Rc<TextLayout>;
55    }
56}
57ferroui_base::ferro_class_info!(TextBlock { new: TextBlock::new });
58
59ferroui_base::ferro_impl_classes!(TextBlock: InteractiveImpl);
60ferroui_base::ferro_impl_classes!(TextBlock: InputElementImpl);
61impl ControlImpl for TextBlock {
62    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
63        crate::automation::peers::TextBlockAutomationPeer::new(this).upcast()
64    }
65}
66ferroui_base::ferro_impl_classes!(TextBlock: StyledElementImpl);
67
68ferroui_base::ferro_overrides! { impl FerroObjectImpl for TextBlock {
69    fn constructed(this: &Self) {
70        Self::parent_constructed(this);
71
72        let inlines = InlineCollection::new();
73        inlines.set_logical_children(Some(this));
74        inlines.set_inline_host(Some(this.as_inline_host()));
75        this.set_inlines(Some(inlines));
76    }
77
78    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
79        Self::parent_on_property_changed(this, change);
80
81        if change.property() == Self::text_property().as_property()
82            && this.has_complex_content()
83            && !this.clear_text_internal.get()
84        {
85            if let Some(inlines) = this.inlines() {
86                inlines.clear();
87            }
88        }
89
90        match change.property().name() {
91            // Properties that affect shaping: invalidate the run cache + layout.
92            "FontSize" | "FontWeight" | "FontStyle" | "FontFamily" | "FontStretch" | "FlowDirection"
93            | "LetterSpacing" | "Text" | "TextDecorations" | "FontFeatures" | "Foreground" => {
94                this.invalidate_text_layout();
95            }
96            // Properties that do not affect shaping: preserve the run cache.
97            "TextWrapping" | "TextTrimming" | "TextAlignment" | "Padding" | "LineHeight" | "LineSpacing"
98            | "MaxLines" => {
99                this.invalidate_text_layout_keep_cache();
100            }
101            // A FerroUI addition: where the text goes in the height layout rounding adds. It
102            // moves the text and the embedded controls, not the size.
103            "LayoutRoundingPlacement" => {
104                this.invalidate_visual();
105                this.invalidate_arrange();
106            }
107            "Inlines" => {
108                let (old_value, new_value) = change.get_old_and_new_value::<Option<InlineCollection>>();
109                this.on_inlines_changed(old_value, new_value);
110                this.invalidate_text_layout();
111            }
112            _ => {}
113        }
114    }
115} }
116
117impl VisualImpl for TextBlock {
118    /// Renders the `TextBlock` to a drawing context.
119    ///
120    /// This member is sealed: subclasses override `render_text_layout`.
121    fn render(this: &Self, context: &mut DrawingContext) {
122        this.render_core(context);
123    }
124
125    fn bypass_flow_direction_policies(_this: &Self) -> bool {
126        true
127    }
128
129    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
130        Self::parent_on_detached_from_visual_tree(this, e);
131
132        this.dispose_text_layout();
133    }
134}
135
136impl LayoutableImpl for TextBlock {
137    fn on_measure_invalidated(this: &Self) {
138        this.dispose_text_layout();
139
140        Self::parent_on_measure_invalidated(this);
141    }
142
143    fn measure_override(this: &Self, available_size: Size) -> Size {
144        let mut padding = this.padding();
145
146        if this.use_layout_rounding() {
147            let scale = LayoutHelper::get_layout_scale(this);
148            padding = LayoutHelper::round_layout_thickness(padding, scale);
149        }
150
151        let deflated_size = available_size.deflate(padding);
152
153        if this.constraint.get() != deflated_size {
154            // Reset the text layout when the constraint is not matching.
155            this.dispose_text_layout();
156            this.constraint.set(deflated_size);
157
158            // Force arrange so text will be properly aligned.
159            this.invalidate_arrange();
160        }
161
162        this.ensure_text_runs();
163
164        if this.measure_embedded_controls(deflated_size) {
165            // A line snapshots its metrics when it is formatted, so an
166            // existing layout still reports the width and height the child
167            // had before it was measured again.
168            this.dispose_text_layout();
169        }
170
171        // This implicitly recreates the text layout with a new constraint if
172        // we previously reset it.
173        let text_layout = this.text_layout();
174
175        // The text width used here is matching that the text presenter uses
176        // to measure the text.
177        Size::new(text_layout.width_including_trailing_whitespace(), text_layout.height()).inflate(padding)
178    }
179
180    fn arrange_override(this: &Self, final_size: Size) -> Size {
181        let mut padding = this.padding();
182        let mut layout_scale = None;
183
184        if this.use_layout_rounding() {
185            let scale = LayoutHelper::get_layout_scale(this);
186            padding = LayoutHelper::round_layout_thickness(padding, scale);
187            layout_scale = Some(scale);
188        }
189
190        let available_size = final_size.deflate(padding);
191
192        // Deviation (DEVIATIONS.md, Text block): the layout of the measure is
193        // kept when nothing it depends on differs.
194        // The reference disposes the layout here and creates it again for the
195        // arranged size, reusing the shaped runs of the run cache (a layout
196        // created during measure is left aligned). A layout that was created
197        // for the very same constraint and alignment (and has not been
198        // invalidated since) is that layout already, so it is kept: nothing
199        // is laid out again when the arranged size is the measured one.
200        if this.constraint.get() != available_size
201            || this.layout_text_alignment.get() != this.resolve_text_alignment(this.text_alignment())
202        {
203            this.dispose_text_layout();
204            this.constraint.set(available_size);
205        }
206
207        // This implicitly recreates the text layout with a new constraint.
208        let text_layout = this.text_layout();
209
210        if this.has_complex_content() {
211            // Clear visual children before complex run arrangement.
212            this.visual_children().clear();
213
214            // A FerroUI addition: the embedded controls move with the text when the height
215            // layout rounding adds is above it (`LayoutRoundingPlacement`).
216            let mut current_y = padding.top + this.layout_rounding_offset_of(&text_layout, padding, layout_scale);
217
218            for text_line in text_layout.text_lines() {
219                let mut current_x = padding.left + text_line.start();
220
221                for run in text_line.text_runs().iter() {
222                    if let Some(drawable) = run.as_drawable() {
223                        if let Some(control_run) = run.downcast_ref::<EmbeddedControlRun>() {
224                            let control = control_run.control();
225
226                            // Add again to prevent clipping.
227                            this.visual_children().add(control.clone().upcast());
228
229                            let offset_y = TextLineImpl::get_baseline_offset(&**text_line, drawable);
230
231                            control.arrange(Rect::from_position_size(
232                                Point::new(current_x, current_y + offset_y),
233                                control.desired_size(),
234                            ));
235                        }
236
237                        current_x += drawable.size().width;
238                    }
239                }
240
241                current_y += text_line.height();
242            }
243        }
244
245        final_size
246    }
247}
248
249impl TextBlockImpl for TextBlock {
250    fn render_core(this: &Self, context: &mut DrawingContext) {
251        if let Some(background) = this.background() {
252            context.fill_rectangle(&background, Rect::from_size(this.bounds().size()), 0.0);
253        }
254
255        let mut padding = this.padding();
256        let mut layout_scale = None;
257        if this.use_layout_rounding() {
258            let scale = LayoutHelper::get_layout_scale(this);
259            padding = LayoutHelper::round_layout_thickness(padding, scale);
260            layout_scale = Some(scale);
261        }
262
263        let mut top = padding.top;
264        let text_height = this.text_layout().height();
265        let bounds_height = this.bounds().height;
266
267        if bounds_height < text_height {
268            match this.vertical_alignment() {
269                VerticalAlignment::Center => top += (bounds_height - text_height) / 2.0,
270                VerticalAlignment::Bottom => top += bounds_height - text_height,
271                _ => {}
272            }
273        }
274
275        // A FerroUI addition (DEVIATIONS.md, Text block): with `LayoutRoundingPlacement` set to
276        // `Above` the height layout rounding adds to the block goes above the text, as WinUI's
277        // TextBlock puts it (docs/porting/text-rendering.md, section 6). With the default,
278        // `Below`, the text starts at the top as the reference draws it.
279        top += this.layout_rounding_offset_of(&this.text_layout(), padding, layout_scale);
280
281        this.render_text_layout(context, Point::new(padding.left, top));
282    }
283
284    fn render_text_layout(this: &Self, context: &mut DrawingContext, origin: Point) {
285        this.text_layout().draw(context, origin);
286    }
287
288    fn create_text_layout(this: &Self, text: Option<&str>) -> Rc<TextLayout> {
289        let typeface =
290            Typeface::with_style(this.font_family(), this.font_style(), this.font_weight(), this.font_stretch());
291
292        let default_properties: Rc<dyn TextRunProperties> = Rc::new(GenericTextRunProperties::with_all(
293            typeface,
294            this.font_size(),
295            this.text_decorations(),
296            this.foreground(),
297            None,
298            Default::default(),
299            None,
300            this.font_features(),
301        ));
302
303        let paragraph_properties = GenericTextParagraphProperties::with_all(
304            this.flow_direction(),
305            if this.is_measure_valid() { this.text_alignment() } else { TextAlignment::Left },
306            true,
307            false,
308            default_properties.clone(),
309            this.text_wrapping(),
310            this.line_height(),
311            0.0,
312            this.letter_spacing(),
313        );
314        paragraph_properties.set_line_spacing(this.line_spacing());
315
316        let text_source: Rc<dyn ITextSource> = if this.has_complex_content() {
317            this.ensure_text_runs();
318
319            let text_runs = this.text_runs.borrow().clone().unwrap_or_default();
320
321            Rc::new(InlinesTextSource::new(text_runs, None))
322        } else {
323            Rc::new(SimpleTextSource::new(this.utf16_text(text.unwrap_or("")), default_properties))
324        };
325
326        let max_size = this.get_max_size_from_constraint();
327
328        let text_run_cache = this.text_run_cache.borrow_mut().get_or_insert_with(|| Rc::new(TextRunCache::new())).clone();
329
330        Rc::new(TextLayout::from_text_source(
331            text_source,
332            Rc::new(paragraph_properties),
333            Some(this.text_trimming()),
334            max_size.width,
335            max_size.height,
336            this.max_lines(),
337            Some(text_run_cache),
338        ))
339    }
340}
341
342/// The handle through which the inlines of a text block reach it.
343struct TextBlockInlineHost(WeakRef<TextBlock>);
344
345impl IInlineHost for TextBlockInlineHost {
346    fn invalidate(&self) {
347        if let Some(text_block) = self.0.upgrade() {
348            text_block.invalidate_text_layout();
349        }
350    }
351
352    fn with_visual_children(&self, f: &mut dyn FnMut(&FerroList<Ref<Visual>>)) {
353        if let Some(text_block) = self.0.upgrade() {
354            f(text_block.visual_children());
355        }
356    }
357
358    fn as_text_block(&self) -> Option<Ref<TextBlock>> {
359        self.0.upgrade()
360    }
361}
362
363ferroui_base::ferro_properties! { impl TextBlock {
364    ferro_property!(
365        /// Defines the `Background` property.
366        pub fn background_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
367            Border::background_property().add_owner::<TextBlock>()
368        }
369    );
370
371    ferro_property!(
372        /// Defines the `Padding` property.
373        pub fn padding_property() -> StyledProperty<Thickness> {
374            Decorator::padding_property().add_owner::<TextBlock>()
375        }
376    );
377
378    ferro_property!(
379        /// Defines the `FontFamily` property.
380        pub fn font_family_property() -> StyledProperty<FontFamily> {
381            TextElement::font_family_property().add_owner::<TextBlock>()
382        }
383    );
384
385    ferro_property!(
386        /// Defines the `FontSize` property.
387        pub fn font_size_property() -> StyledProperty<f64> {
388            TextElement::font_size_property().add_owner::<TextBlock>()
389        }
390    );
391
392    ferro_property!(
393        /// Defines the `FontStyle` property.
394        pub fn font_style_property() -> StyledProperty<FontStyle> {
395            TextElement::font_style_property().add_owner::<TextBlock>()
396        }
397    );
398
399    ferro_property!(
400        /// Defines the `FontWeight` property.
401        pub fn font_weight_property() -> StyledProperty<FontWeight> {
402            TextElement::font_weight_property().add_owner::<TextBlock>()
403        }
404    );
405
406    ferro_property!(
407        /// Defines the `FontStretch` property.
408        pub fn font_stretch_property() -> StyledProperty<FontStretch> {
409            TextElement::font_stretch_property().add_owner::<TextBlock>()
410        }
411    );
412
413    ferro_property!(
414        /// Defines the `Foreground` property.
415        pub fn foreground_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
416            TextElement::foreground_property().add_owner::<TextBlock>()
417        }
418    );
419
420    ferro_property!(
421        /// Defines the `BaselineOffset` attached property.
422        pub fn baseline_offset_property() -> AttachedProperty<f64> {
423            FerroProperty::register_attached_with::<TextBlock, Control, _>(
424                "BaselineOffset",
425                StyledPropertyOptions::new(0.0).inherits(true),
426            )
427        }
428    );
429
430    ferro_property!(
431        /// Defines the `LayoutRoundingPlacement` attached property: where a text block puts
432        /// the height layout rounding adds to it (a FerroUI addition). Inherited, so that a
433        /// theme sets it once on the top-level.
434        pub fn layout_rounding_placement_property() -> AttachedProperty<TextLayoutRoundingPlacement> {
435            FerroProperty::register_attached_with::<TextBlock, Control, _>(
436                "LayoutRoundingPlacement",
437                StyledPropertyOptions::new(TextLayoutRoundingPlacement::Below).inherits(true),
438            )
439        }
440    );
441
442    ferro_property!(
443        /// Defines the `LineHeight` property.
444        pub fn line_height_property() -> AttachedProperty<f64> {
445            FerroProperty::register_attached_with::<TextBlock, Control, _>(
446                "LineHeight",
447                StyledPropertyOptions::new(f64::NAN).validate(Self::is_valid_line_height).inherits(true),
448            )
449        }
450    );
451
452    ferro_property!(
453        /// Defines the `LineSpacing` property.
454        pub fn line_spacing_property() -> AttachedProperty<f64> {
455            FerroProperty::register_attached_with::<TextBlock, Control, _>(
456                "LineSpacing",
457                StyledPropertyOptions::new(0.0).validate(Self::is_valid_line_spacing).inherits(true),
458            )
459        }
460    );
461
462    ferro_property!(
463        /// Defines the `LetterSpacing` property.
464        pub fn letter_spacing_property() -> StyledProperty<f64> {
465            TextElement::letter_spacing_property().add_owner::<TextBlock>()
466        }
467    );
468
469    ferro_property!(
470        /// Defines the `MaxLines` property.
471        pub fn max_lines_property() -> AttachedProperty<i32> {
472            FerroProperty::register_attached_with::<TextBlock, Control, _>(
473                "MaxLines",
474                StyledPropertyOptions::new(0).validate(Self::is_valid_max_lines).inherits(true),
475            )
476        }
477    );
478
479    ferro_property!(
480        /// Defines the `Text` property.
481        pub fn text_property() -> StyledProperty<Option<String>> {
482            FerroProperty::register::<TextBlock, _>("Text", None)
483        }
484    );
485
486    ferro_property!(
487        /// Defines the `TextAlignment` property.
488        pub fn text_alignment_property() -> AttachedProperty<TextAlignment> {
489            FerroProperty::register_attached_with::<TextBlock, Control, _>(
490                "TextAlignment",
491                StyledPropertyOptions::new(TextAlignment::Start).inherits(true),
492            )
493        }
494    );
495
496    ferro_property!(
497        /// Defines the `TextWrapping` property.
498        pub fn text_wrapping_property() -> AttachedProperty<TextWrapping> {
499            FerroProperty::register_attached_with::<TextBlock, Control, _>(
500                "TextWrapping",
501                StyledPropertyOptions::new(TextWrapping::default()).inherits(true),
502            )
503        }
504    );
505
506    ferro_property!(
507        /// Defines the `TextTrimming` property.
508        pub fn text_trimming_property() -> AttachedProperty<Rc<dyn TextTrimming>> {
509            FerroProperty::register_attached_with::<TextBlock, Control, _>(
510                "TextTrimming",
511                StyledPropertyOptions::new(<dyn TextTrimming>::none()).inherits(true),
512            )
513        }
514    );
515
516    ferro_property!(
517        /// Defines the `TextDecorations` property.
518        pub fn text_decorations_property() -> StyledProperty<Option<TextDecorationCollection>> {
519            Inline::text_decorations_property().add_owner::<TextBlock>()
520        }
521    );
522
523    ferro_property!(
524        /// Defines the `FontFeatures` property.
525        pub fn font_features_property() -> StyledProperty<Option<FontFeatureCollection>> {
526            TextElement::font_features_property().add_owner::<TextBlock>()
527        }
528    );
529
530    ferro_property!(
531        /// Defines the `Inlines` property.
532        pub fn inlines_property() -> DirectProperty<TextBlock, Option<InlineCollection>> {
533            FerroProperty::register_direct::<TextBlock, _>(
534                "Inlines",
535                |o| o.inlines(),
536                Some(|o: &TextBlock, v| o.set_inlines(v)),
537                None,
538            )
539        }
540    );
541} }
542
543impl TextBlock {
544    fn static_constructor() {
545        Visual::clip_to_bounds_property().override_default_value::<TextBlock>(true);
546
547        Visual::affects_render::<TextBlock>(&[
548            Self::background_property().as_property(),
549            Self::foreground_property().as_property(),
550        ]);
551    }
552
553    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
554    pub fn construct() -> Self {
555        Self {
556            base: Control::construct(),
557            text_layout: RefCell::new(None),
558            text_run_cache: RefCell::new(None),
559            constraint: Cell::new(Size::new(f64::NAN, f64::NAN)),
560            text_runs: RefCell::new(None),
561            inlines: RefCell::new(None),
562            inlines_invalidated: RefCell::new(None),
563            inline_host: OnceCell::new(),
564            clear_text_internal: Cell::new(false),
565            layout_text_alignment: Cell::new(TextAlignment::Left),
566            utf16_text: RefCell::new(None),
567        }
568    }
569
570    /// Initializes a new instance of the `TextBlock` class.
571    pub fn new() -> Ref<Self> {
572        instantiate(Self::construct())
573    }
574
575    /// The `TextLayout` used to render the text.
576    ///
577    /// The layout is cached until a property it depends on, the measure of
578    /// the control or its constraint changes.
579    pub fn text_layout(&self) -> Rc<TextLayout> {
580        if let Some(text_layout) = self.text_layout.borrow().as_ref() {
581            return text_layout.clone();
582        }
583
584        // The alignment `create_text_layout` uses: text is left aligned
585        // until the control has been measured.
586        let text_alignment = if self.is_measure_valid() { self.text_alignment() } else { TextAlignment::Left };
587        self.layout_text_alignment.set(self.resolve_text_alignment(text_alignment));
588
589        let text_layout = self.create_text_layout_core();
590        *self.text_layout.borrow_mut() = Some(text_layout.clone());
591        text_layout
592    }
593
594    /// The physical alignment of a logical one for the flow direction of the
595    /// control.
596    fn resolve_text_alignment(&self, text_alignment: TextAlignment) -> TextAlignment {
597        let left_to_right = self.flow_direction() == FlowDirection::LeftToRight;
598
599        match text_alignment {
600            TextAlignment::Start if left_to_right => TextAlignment::Left,
601            TextAlignment::Start => TextAlignment::Right,
602            TextAlignment::End if left_to_right => TextAlignment::Right,
603            TextAlignment::End => TextAlignment::Left,
604            other => other,
605        }
606    }
607
608    fn create_text_layout_core(&self) -> Rc<TextLayout> {
609        // A layout can be created outside of a measure pass - a render pass
610        // that runs before a queued measure, or a read of this property - so
611        // it cannot rely on the measure pass having brought the complex
612        // content up to date.
613        self.ensure_text_runs();
614
615        // A block whose measure is invalid at render time is one layout cannot reach, such as
616        // one under a hidden ancestor. Measuring its embedded controls here would apply their
617        // templates inside the render pass, which must not attach visuals. The pending measure
618        // pass measures them once the block becomes reachable.
619        if self.is_measure_valid() {
620            self.measure_embedded_controls(self.get_max_size_from_constraint());
621        }
622
623        let text = self.text();
624        self.create_text_layout(text.as_deref())
625    }
626
627    /// The padding to place around the `Text`.
628    pub fn padding(&self) -> Thickness {
629        self.get_value(Self::padding_property())
630    }
631
632    pub fn set_padding(&self, value: Thickness) {
633        self.set_value(Self::padding_property(), value)
634    }
635
636    /// A brush used to paint the control's background.
637    pub fn background(&self) -> Option<Rc<dyn IBrush>> {
638        self.get_value(Self::background_property())
639    }
640
641    pub fn set_background(&self, value: Option<Rc<dyn IBrush>>) {
642        self.set_value(Self::background_property(), value)
643    }
644
645    /// The text.
646    pub fn text(&self) -> Option<String> {
647        self.get_value(Self::text_property())
648    }
649
650    pub fn set_text(&self, value: Option<&str>) {
651        self.set_value(Self::text_property(), value.map(str::to_owned))
652    }
653
654    /// The font family used to draw the control's text.
655    pub fn font_family(&self) -> FontFamily {
656        self.get_value(Self::font_family_property())
657    }
658
659    pub fn set_font_family(&self, value: FontFamily) {
660        self.set_value(Self::font_family_property(), value)
661    }
662
663    /// The size of the control's text in points.
664    pub fn font_size(&self) -> f64 {
665        self.get_value(Self::font_size_property())
666    }
667
668    pub fn set_font_size(&self, value: f64) {
669        self.set_value(Self::font_size_property(), value)
670    }
671
672    /// The font style used to draw the control's text.
673    pub fn font_style(&self) -> FontStyle {
674        self.get_value(Self::font_style_property())
675    }
676
677    pub fn set_font_style(&self, value: FontStyle) {
678        self.set_value(Self::font_style_property(), value)
679    }
680
681    /// The font weight used to draw the control's text.
682    pub fn font_weight(&self) -> FontWeight {
683        self.get_value(Self::font_weight_property())
684    }
685
686    pub fn set_font_weight(&self, value: FontWeight) {
687        self.set_value(Self::font_weight_property(), value)
688    }
689
690    /// The font stretch used to draw the control's text.
691    pub fn font_stretch(&self) -> FontStretch {
692        self.get_value(Self::font_stretch_property())
693    }
694
695    pub fn set_font_stretch(&self, value: FontStretch) {
696        self.set_value(Self::font_stretch_property(), value)
697    }
698
699    /// The brush used to draw the control's text and other foreground
700    /// elements.
701    pub fn foreground(&self) -> Option<Rc<dyn IBrush>> {
702        self.get_value(Self::foreground_property())
703    }
704
705    pub fn set_foreground(&self, value: Option<Rc<dyn IBrush>>) {
706        self.set_value(Self::foreground_property(), value)
707    }
708
709    /// The height of each line of content.
710    pub fn line_height(&self) -> f64 {
711        self.get_value(Self::line_height_property())
712    }
713
714    pub fn set_line_height(&self, value: f64) {
715        self.set_value(Self::line_height_property(), value)
716    }
717
718    /// The extra distance between each line of content.
719    pub fn line_spacing(&self) -> f64 {
720        self.get_value(Self::line_spacing_property())
721    }
722
723    pub fn set_line_spacing(&self, value: f64) {
724        self.set_value(Self::line_spacing_property(), value)
725    }
726
727    /// The letter spacing.
728    pub fn letter_spacing(&self) -> f64 {
729        self.get_value(Self::letter_spacing_property())
730    }
731
732    pub fn set_letter_spacing(&self, value: f64) {
733        self.set_value(Self::letter_spacing_property(), value)
734    }
735
736    /// The maximum number of text lines.
737    pub fn max_lines(&self) -> i32 {
738        self.get_value(Self::max_lines_property())
739    }
740
741    pub fn set_max_lines(&self, value: i32) {
742        self.set_value(Self::max_lines_property(), value)
743    }
744
745    /// The control's text wrapping mode.
746    pub fn text_wrapping(&self) -> TextWrapping {
747        self.get_value(Self::text_wrapping_property())
748    }
749
750    pub fn set_text_wrapping(&self, value: TextWrapping) {
751        self.set_value(Self::text_wrapping_property(), value)
752    }
753
754    /// The control's text trimming mode.
755    pub fn text_trimming(&self) -> Rc<dyn TextTrimming> {
756        self.get_value(Self::text_trimming_property())
757    }
758
759    pub fn set_text_trimming(&self, value: Rc<dyn TextTrimming>) {
760        self.set_value(Self::text_trimming_property(), value)
761    }
762
763    /// The text alignment.
764    pub fn text_alignment(&self) -> TextAlignment {
765        self.get_value(Self::text_alignment_property())
766    }
767
768    pub fn set_text_alignment(&self, value: TextAlignment) {
769        self.set_value(Self::text_alignment_property(), value)
770    }
771
772    /// The text decorations.
773    pub fn text_decorations(&self) -> Option<TextDecorationCollection> {
774        self.get_value(Self::text_decorations_property())
775    }
776
777    pub fn set_text_decorations(&self, value: Option<TextDecorationCollection>) {
778        self.set_value(Self::text_decorations_property(), value)
779    }
780
781    /// The font features.
782    pub fn font_features(&self) -> Option<FontFeatureCollection> {
783        self.get_value(Self::font_features_property())
784    }
785
786    pub fn set_font_features(&self, value: Option<FontFeatureCollection>) {
787        self.set_value(Self::font_features_property(), value)
788    }
789
790    /// The inlines.
791    pub fn inlines(&self) -> Option<InlineCollection> {
792        self.inlines.borrow().clone()
793    }
794
795    pub fn set_inlines(&self, value: Option<InlineCollection>) {
796        self.set_and_raise(Self::inlines_property(), &self.inlines, value);
797    }
798
799    /// Whether the text block displays inlines rather than its `Text`.
800    pub fn has_complex_content(&self) -> bool {
801        self.inlines.borrow().as_ref().is_some_and(|inlines| inlines.count() > 0)
802    }
803
804    /// The constraint of the text layout as a maximum size: an unset
805    /// constraint is zero.
806    pub fn get_max_size_from_constraint(&self) -> Size {
807        let constraint = self.constraint.get();
808        let max_width = if constraint.width.is_nan() { 0.0 } else { constraint.width };
809        let max_height = if constraint.height.is_nan() { 0.0 } else { constraint.height };
810        Size::new(max_width, max_height)
811    }
812
813    /// The constraint the text layout is created for.
814    pub fn constraint(&self) -> Size {
815        self.constraint.get()
816    }
817
818    /// The text runs of the inlines, when they have been built.
819    pub fn text_runs(&self) -> Option<Rc<Vec<Rc<dyn TextRun>>>> {
820        self.text_runs.borrow().clone()
821    }
822
823    /// The baseline offset.
824    pub fn baseline_offset(&self) -> f64 {
825        self.get_value(Self::baseline_offset_property())
826    }
827
828    pub fn set_baseline_offset(&self, value: f64) {
829        self.set_value(Self::baseline_offset_property(), value)
830    }
831
832    /// Reads the attached `BaselineOffset` property from the given control.
833    pub fn get_baseline_offset(control: &Control) -> f64 {
834        control.get_value(Self::baseline_offset_property())
835    }
836
837    /// Writes the attached `BaselineOffset` property to the given control.
838    pub fn set_baseline_offset_on(control: &Control, value: f64) {
839        control.set_value(Self::baseline_offset_property(), value)
840    }
841
842    /// Where the text block puts the height layout rounding adds to it: below its text (the
843    /// default, as the reference) or above it (as WinUI). A FerroUI addition.
844    pub fn layout_rounding_placement(&self) -> TextLayoutRoundingPlacement {
845        self.get_value(Self::layout_rounding_placement_property())
846    }
847
848    pub fn set_layout_rounding_placement(&self, value: TextLayoutRoundingPlacement) {
849        self.set_value(Self::layout_rounding_placement_property(), value)
850    }
851
852    /// Reads the attached `LayoutRoundingPlacement` property from the given control.
853    pub fn get_layout_rounding_placement(control: &Control) -> TextLayoutRoundingPlacement {
854        control.get_value(Self::layout_rounding_placement_property())
855    }
856
857    /// Writes the attached `LayoutRoundingPlacement` property to the given control.
858    pub fn set_layout_rounding_placement_on(control: &Control, value: TextLayoutRoundingPlacement) {
859        control.set_value(Self::layout_rounding_placement_property(), value)
860    }
861
862    /// How far down the text is drawn from the top of the content (the top padding) because
863    /// layout rounding added height to the block: the height of the text and the vertical
864    /// padding rounded up to whole device pixels, minus that height, when
865    /// [`layout_rounding_placement`](Self::layout_rounding_placement) is
866    /// [`Above`](TextLayoutRoundingPlacement::Above) and the block uses layout rounding; zero
867    /// otherwise. Rendering, hit testing and the access key underline add it to the top padding
868    /// (WinUI's `CTextBlock::GetContentRenderingOffset`). A FerroUI addition.
869    pub fn layout_rounding_offset(&self) -> f64 {
870        if !self.use_layout_rounding() {
871            return 0.0;
872        }
873        let scale = LayoutHelper::get_layout_scale(self);
874        let padding = LayoutHelper::round_layout_thickness(self.padding(), scale);
875        self.layout_rounding_offset_of(&self.text_layout(), padding, Some(scale))
876    }
877
878    /// The offset of [`layout_rounding_offset`](Self::layout_rounding_offset) for a text layout,
879    /// the rounded padding and the layout scale (`None` without layout rounding).
880    fn layout_rounding_offset_of(&self, text_layout: &TextLayout, padding: Thickness, layout_scale: Option<f64>) -> f64 {
881        let Some(scale) = layout_scale else {
882            return 0.0;
883        };
884        let placement = self.layout_rounding_placement();
885        if placement == TextLayoutRoundingPlacement::Below {
886            return 0.0;
887        }
888        placement.offset(text_layout.height() + padding.top + padding.bottom, scale)
889    }
890
891    /// Reads the attached `TextAlignment` property from the given control.
892    pub fn get_text_alignment(control: &Control) -> TextAlignment {
893        control.get_value(Self::text_alignment_property())
894    }
895
896    /// Writes the attached `TextAlignment` property to the given control.
897    pub fn set_text_alignment_on(control: &Control, alignment: TextAlignment) {
898        control.set_value(Self::text_alignment_property(), alignment)
899    }
900
901    /// Reads the attached `TextWrapping` property from the given control.
902    pub fn get_text_wrapping(control: &Control) -> TextWrapping {
903        control.get_value(Self::text_wrapping_property())
904    }
905
906    /// Writes the attached `TextWrapping` property to the given control.
907    pub fn set_text_wrapping_on(control: &Control, wrapping: TextWrapping) {
908        control.set_value(Self::text_wrapping_property(), wrapping)
909    }
910
911    /// Reads the attached `TextTrimming` property from the given control.
912    pub fn get_text_trimming(control: &Control) -> Rc<dyn TextTrimming> {
913        control.get_value(Self::text_trimming_property())
914    }
915
916    /// Writes the attached `TextTrimming` property to the given control.
917    pub fn set_text_trimming_on(control: &Control, trimming: Rc<dyn TextTrimming>) {
918        control.set_value(Self::text_trimming_property(), trimming)
919    }
920
921    /// Reads the attached `LineHeight` property from the given control.
922    pub fn get_line_height(control: &Control) -> f64 {
923        control.get_value(Self::line_height_property())
924    }
925
926    /// Writes the attached `LineHeight` property to the given control.
927    pub fn set_line_height_on(control: &Control, height: f64) {
928        control.set_value(Self::line_height_property(), height)
929    }
930
931    /// Reads the attached `LetterSpacing` property from the given control.
932    pub fn get_letter_spacing(control: &Control) -> f64 {
933        control.get_value(Self::letter_spacing_property())
934    }
935
936    /// Writes the attached `LetterSpacing` property to the given control.
937    pub fn set_letter_spacing_on(control: &Control, letter_spacing: f64) {
938        control.set_value(Self::letter_spacing_property(), letter_spacing)
939    }
940
941    /// Reads the attached `MaxLines` property from the given control.
942    pub fn get_max_lines(control: &Control) -> i32 {
943        control.get_value(Self::max_lines_property())
944    }
945
946    /// Writes the attached `MaxLines` property to the given control.
947    pub fn set_max_lines_on(control: &Control, max_lines: i32) {
948        control.set_value(Self::max_lines_property(), max_lines)
949    }
950
951    /// Clears the text without clearing the inlines.
952    pub(crate) fn clear_text_internal(&self) {
953        struct Reset<'a>(&'a Cell<bool>);
954
955        impl Drop for Reset<'_> {
956            fn drop(&mut self) {
957                self.0.set(false);
958            }
959        }
960
961        self.clear_text_internal.set(true);
962        let _reset = Reset(&self.clear_text_internal);
963        self.set_current_value(Self::text_property(), None);
964    }
965
966    /// Invalidates the text layout and the shaped runs it was built from.
967    pub fn invalidate_text_layout(&self) {
968        if let Some(text_run_cache) = self.text_run_cache.borrow().as_ref() {
969            text_run_cache.invalidate();
970        }
971        *self.text_runs.borrow_mut() = None;
972        self.dispose_text_layout();
973        self.invalidate_visual();
974        self.invalidate_measure();
975    }
976
977    fn invalidate_text_layout_keep_cache(&self) {
978        self.dispose_text_layout();
979        self.invalidate_visual();
980        self.invalidate_measure();
981    }
982
983    #[allow(dead_code)] // read by the tests
984    pub(crate) fn has_text_layout(&self) -> bool {
985        self.text_layout.borrow().is_some()
986    }
987
988    fn dispose_text_layout(&self) {
989        let text_layout = self.text_layout.borrow_mut().take();
990        if let Some(text_layout) = text_layout {
991            text_layout.dispose();
992        }
993    }
994
995    /// Builds the text runs of the inlines, unless they are up to date.
996    pub fn ensure_text_runs(&self) {
997        if self.text_runs.borrow().is_some() || !self.has_complex_content() {
998            return;
999        }
1000
1001        let mut text_runs: Vec<Rc<dyn TextRun>> = Vec::new();
1002
1003        if let Some(inlines) = self.inlines() {
1004            for inline in inlines.snapshot().iter() {
1005                inline.build_text_run(&mut text_runs);
1006            }
1007        }
1008
1009        *self.text_runs.borrow_mut() = Some(Rc::new(text_runs));
1010    }
1011
1012    fn measure_embedded_controls(&self, constraint: Size) -> bool {
1013        if !self.has_complex_content() {
1014            return false;
1015        }
1016
1017        let mut resized = false;
1018
1019        if let Some(inlines) = self.inlines() {
1020            for inline in inlines.snapshot().iter() {
1021                resized |= inline.measure_embedded_controls(constraint);
1022            }
1023        }
1024
1025        resized
1026    }
1027
1028    fn is_valid_max_lines(max_lines: &i32) -> bool {
1029        *max_lines >= 0
1030    }
1031
1032    fn is_valid_line_height(line_height: &f64) -> bool {
1033        line_height.is_nan() || *line_height > 0.0
1034    }
1035
1036    fn is_valid_line_spacing(line_spacing: &f64) -> bool {
1037        !line_spacing.is_nan() && !line_spacing.is_infinite()
1038    }
1039
1040    fn on_inlines_changed(&self, old_value: Option<InlineCollection>, new_value: Option<InlineCollection>) {
1041        self.visual_children().clear();
1042
1043        if let Some(old_value) = old_value {
1044            old_value.set_logical_children(None);
1045            old_value.set_inline_host(None);
1046            if let Some(subscription) = self.inlines_invalidated.borrow_mut().take() {
1047                subscription.dispose();
1048            }
1049        }
1050
1051        if let Some(new_value) = new_value {
1052            new_value.set_logical_children(Some(self));
1053            new_value.set_inline_host(Some(self.as_inline_host()));
1054            let weak = self.to_ref().downgrade();
1055            *self.inlines_invalidated.borrow_mut() = Some(new_value.invalidated(move || {
1056                if let Some(this) = weak.upgrade() {
1057                    this.invalidate_text_layout();
1058                }
1059            }));
1060        }
1061    }
1062
1063    /// The handle through which inlines reach this text block.
1064    pub(crate) fn as_inline_host(&self) -> Rc<dyn IInlineHost> {
1065        self.inline_host.get_or_init(|| Rc::new(TextBlockInlineHost(self.to_ref().downgrade()))).clone()
1066    }
1067
1068    /// The UTF-16 form of `text`, transcoded only when it differs from the
1069    /// text of the previous layout.
1070    fn utf16_text(&self, text: &str) -> ReadOnlyMemory<u16> {
1071        if let Some((cached_text, utf16)) = self.utf16_text.borrow().as_ref() {
1072            if cached_text == text {
1073                return utf16.clone();
1074            }
1075        }
1076
1077        let utf16 = ReadOnlyMemory::<u16>::from_str(text);
1078        *self.utf16_text.borrow_mut() = Some((text.to_owned(), utf16.clone()));
1079        utf16
1080    }
1081}
1082
1083/// A text source of one string with one set of run properties.
1084#[derive(Clone)]
1085pub struct SimpleTextSource {
1086    text: ReadOnlyMemory<u16>,
1087    default_properties: Rc<dyn TextRunProperties>,
1088}
1089
1090impl SimpleTextSource {
1091    pub fn new(text: ReadOnlyMemory<u16>, default_properties: Rc<dyn TextRunProperties>) -> Self {
1092        Self { text, default_properties }
1093    }
1094}
1095
1096impl ITextSource for SimpleTextSource {
1097    fn get_text_run(&self, text_source_index: i32) -> Option<Rc<dyn TextRun>> {
1098        if text_source_index > self.text.len() as i32 {
1099            return Some(Rc::new(TextEndOfParagraph::new()));
1100        }
1101
1102        let run_text = self.text.slice_from(text_source_index.max(0) as usize);
1103
1104        if run_text.is_empty() {
1105            return Some(Rc::new(TextEndOfParagraph::new()));
1106        }
1107
1108        Some(Rc::new(TextCharacters::new(run_text, self.default_properties.clone())))
1109    }
1110}
1111
1112/// A text source of the text runs of inlines, with optional run property
1113/// overrides for ranges of the text.
1114#[derive(Clone)]
1115pub struct InlinesTextSource {
1116    text_runs: Rc<Vec<Rc<dyn TextRun>>>,
1117    text_modifier: Option<Rc<[ValueSpan<Rc<dyn TextRunProperties>>]>>,
1118}
1119
1120impl InlinesTextSource {
1121    pub fn new(
1122        text_runs: Rc<Vec<Rc<dyn TextRun>>>,
1123        text_modifier: Option<Rc<[ValueSpan<Rc<dyn TextRunProperties>>]>>,
1124    ) -> Self {
1125        Self { text_runs, text_modifier }
1126    }
1127
1128    /// The text runs of the source.
1129    pub fn text_runs(&self) -> &Rc<Vec<Rc<dyn TextRun>>> {
1130        &self.text_runs
1131    }
1132}
1133
1134impl ITextSource for InlinesTextSource {
1135    fn get_text_run(&self, text_source_index: i32) -> Option<Rc<dyn TextRun>> {
1136        let mut current_position = 0;
1137
1138        for text_run in self.text_runs.iter() {
1139            if text_run.length() == 0 {
1140                continue;
1141            }
1142
1143            if text_source_index >= current_position + text_run.length() {
1144                current_position += text_run.length();
1145
1146                continue;
1147            }
1148
1149            if let Some(text_characters) = text_run.downcast_ref::<TextCharacters>() {
1150                let skip = (text_source_index - current_position).max(0) as usize;
1151
1152                let text = text_run.text().slice_from(skip);
1153
1154                let text_style_run = FormattedTextSource::create_text_style_run(
1155                    text.span(),
1156                    text_source_index,
1157                    text_characters.run_properties(),
1158                    self.text_modifier.as_deref(),
1159                );
1160
1161                return Some(Rc::new(TextCharacters::new(
1162                    text.slice(0, text_style_run.length() as usize),
1163                    text_style_run.value().clone(),
1164                )));
1165            }
1166
1167            return Some(text_run.clone());
1168        }
1169
1170        Some(Rc::new(TextEndOfParagraph::new()))
1171    }
1172}
1173
1174impl TextBlock {
1175    /// Appends the text that describes the text block in diagnostics: the base description
1176    /// and, with the content, its text or the text of its inlines.
1177    pub(crate) fn build_debug_display(&self, builder: &mut String, include_content: bool) {
1178        build_base_debug_display(self, builder);
1179
1180        if include_content {
1181            let text = self.text().or_else(|| self.inlines().and_then(|inlines| inlines.text()));
1182            append_optional_value(builder, "Text", text.as_deref());
1183        }
1184    }
1185}