Skip to main content

fission_core/ui/widgets/
text.rs

1use crate::lowering::{LoweringContext, NodeBuilder};
2use crate::ui::traits::Lower;
3use crate::ActionEnvelope;
4use fission_ir::{
5    op::{
6        decode_inline_widget_marker, encode_inline_widget_marker, Color as IrColor,
7        FontStyle as IrFontStyle, LayoutOp, MouseCursor as IrMouseCursor, Op, PaintOp,
8        RichTextAnnotation as IrRichTextAnnotation, TextAlign as IrTextAlign,
9        TextDirection as IrTextDirection, TextHeightBehavior as IrTextHeightBehavior,
10        TextOverflow as IrTextOverflow, TextParagraphStyle as IrTextParagraphStyle,
11        TextRun as IrTextRun, TextWidthBasis as IrTextWidthBasis,
12    },
13    semantics::ActionTrigger,
14    ActionEntry, CompositeStyle, NodeId, Role, Semantics,
15};
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18
19/// The content source for a [`Text`] widget.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub enum TextContent {
22    Literal(String),
23    Key(String),
24}
25
26impl From<&str> for TextContent {
27    fn from(value: &str) -> Self {
28        TextContent::Literal(value.to_string())
29    }
30}
31
32impl From<String> for TextContent {
33    fn from(value: String) -> Self {
34        TextContent::Literal(value)
35    }
36}
37
38impl Default for TextContent {
39    fn default() -> Self {
40        TextContent::Literal(String::new())
41    }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
45pub enum TextFontStyle {
46    #[default]
47    Normal,
48    Italic,
49}
50
51impl From<TextFontStyle> for IrFontStyle {
52    fn from(value: TextFontStyle) -> Self {
53        match value {
54            TextFontStyle::Normal => IrFontStyle::Normal,
55            TextFontStyle::Italic => IrFontStyle::Italic,
56        }
57    }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
61#[serde(transparent)]
62pub struct TextScaler(f32);
63
64impl TextScaler {
65    pub fn linear(scale_factor: f32) -> Self {
66        Self(scale_factor)
67    }
68
69    pub fn scale_factor(self) -> f32 {
70        self.0
71    }
72}
73
74impl Default for TextScaler {
75    fn default() -> Self {
76        Self::linear(1.0)
77    }
78}
79
80impl From<f32> for TextScaler {
81    fn from(value: f32) -> Self {
82        Self::linear(value)
83    }
84}
85
86impl From<TextScaler> for f32 {
87    fn from(value: TextScaler) -> Self {
88        value.scale_factor()
89    }
90}
91
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
93pub struct TextRunStyle {
94    pub font_size: Option<f32>,
95    pub color: Option<IrColor>,
96    pub underline: bool,
97    pub font_family: Option<String>,
98    pub locale: Option<String>,
99    pub font_weight: Option<u16>,
100    pub font_style: TextFontStyle,
101    pub line_height: Option<f32>,
102    pub letter_spacing: Option<f32>,
103    pub text_scale: Option<f32>,
104    pub background_color: Option<IrColor>,
105}
106
107impl TextRunStyle {
108    fn resolve(
109        &self,
110        theme: &fission_theme::Theme,
111        fallback_size: Option<f32>,
112        fallback_color: Option<IrColor>,
113    ) -> fission_ir::op::TextStyle {
114        let scale = self.text_scale.unwrap_or(1.0).max(0.0);
115        let base_font_size = self
116            .font_size
117            .or(fallback_size)
118            .unwrap_or(theme.tokens.typography.body_medium_size);
119        let base_line_height = self.line_height.or(Some(base_font_size * 1.2));
120        let base_letter_spacing = self.letter_spacing.unwrap_or(0.0);
121        fission_ir::op::TextStyle {
122            font_size: base_font_size * scale,
123            color: self
124                .color
125                .or(fallback_color)
126                .unwrap_or(theme.tokens.colors.text_primary),
127            underline: self.underline,
128            font_family: self.font_family.clone(),
129            locale: self.locale.clone(),
130            font_weight: self.font_weight.unwrap_or(400),
131            font_style: self.font_style.into(),
132            line_height: base_line_height.map(|value| value * scale),
133            letter_spacing: base_letter_spacing * scale,
134            background_color: self.background_color,
135        }
136    }
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct RichTextRun {
141    pub text: String,
142    pub style: TextRunStyle,
143    pub semantics_label: Option<String>,
144    pub semantics_identifier: Option<String>,
145    #[serde(default)]
146    pub spell_out: Option<bool>,
147}
148
149impl RichTextRun {
150    pub fn new(text: impl Into<String>) -> Self {
151        Self {
152            text: text.into(),
153            style: TextRunStyle::default(),
154            semantics_label: None,
155            semantics_identifier: None,
156            spell_out: None,
157        }
158    }
159
160    pub fn size(mut self, size: f32) -> Self {
161        self.style.font_size = Some(size);
162        self
163    }
164
165    pub fn color(mut self, color: IrColor) -> Self {
166        self.style.color = Some(color);
167        self
168    }
169
170    pub fn underline(mut self, underline: bool) -> Self {
171        self.style.underline = underline;
172        self
173    }
174
175    pub fn family(mut self, family: impl Into<String>) -> Self {
176        self.style.font_family = Some(family.into());
177        self
178    }
179
180    pub fn locale(mut self, locale: impl Into<String>) -> Self {
181        self.style.locale = Some(locale.into());
182        self
183    }
184
185    pub fn weight(mut self, weight: u16) -> Self {
186        self.style.font_weight = Some(weight);
187        self
188    }
189
190    pub fn italic(mut self, italic: bool) -> Self {
191        self.style.font_style = if italic {
192            TextFontStyle::Italic
193        } else {
194            TextFontStyle::Normal
195        };
196        self
197    }
198
199    pub fn line_height(mut self, line_height: f32) -> Self {
200        self.style.line_height = Some(line_height);
201        self
202    }
203
204    pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
205        self.style.letter_spacing = Some(letter_spacing);
206        self
207    }
208
209    pub fn text_scale(mut self, text_scale: f32) -> Self {
210        self.style.text_scale = Some(text_scale);
211        self
212    }
213
214    pub fn text_scaler(mut self, text_scaler: impl Into<TextScaler>) -> Self {
215        self.style.text_scale = Some(text_scaler.into().scale_factor());
216        self
217    }
218
219    pub fn background_color(mut self, color: IrColor) -> Self {
220        self.style.background_color = Some(color);
221        self
222    }
223
224    pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
225        self.semantics_label = Some(label.into());
226        self
227    }
228
229    pub fn semantics_identifier(mut self, identifier: impl Into<String>) -> Self {
230        self.semantics_identifier = Some(identifier.into());
231        self
232    }
233
234    pub fn spell_out(mut self, spell_out: bool) -> Self {
235        self.spell_out = Some(spell_out);
236        self
237    }
238
239    pub fn into_span(self) -> RichTextSpan {
240        RichTextSpan::from(self)
241    }
242
243    fn lower_with_theme(
244        &self,
245        theme: &fission_theme::Theme,
246        fallback_size: Option<f32>,
247        fallback_color: Option<IrColor>,
248    ) -> IrTextRun {
249        IrTextRun {
250            text: self.text.clone(),
251            style: self.style.resolve(theme, fallback_size, fallback_color),
252        }
253    }
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
257pub struct RichTextSpanStyle {
258    pub font_size: Option<f32>,
259    pub color: Option<IrColor>,
260    pub underline: Option<bool>,
261    pub font_family: Option<String>,
262    pub locale: Option<String>,
263    pub font_weight: Option<u16>,
264    pub font_style: Option<TextFontStyle>,
265    pub line_height: Option<f32>,
266    pub letter_spacing: Option<f32>,
267    pub text_scale: Option<f32>,
268    pub background_color: Option<IrColor>,
269}
270
271impl RichTextSpanStyle {
272    fn cascade(&self, inherited: &TextRunStyle) -> TextRunStyle {
273        TextRunStyle {
274            font_size: self.font_size.or(inherited.font_size),
275            color: self.color.or(inherited.color),
276            underline: self.underline.unwrap_or(inherited.underline),
277            font_family: self
278                .font_family
279                .clone()
280                .or_else(|| inherited.font_family.clone()),
281            locale: self.locale.clone().or_else(|| inherited.locale.clone()),
282            font_weight: self.font_weight.or(inherited.font_weight),
283            font_style: self.font_style.unwrap_or(inherited.font_style),
284            line_height: self.line_height.or(inherited.line_height),
285            letter_spacing: self.letter_spacing.or(inherited.letter_spacing),
286            text_scale: self.text_scale.or(inherited.text_scale),
287            background_color: self.background_color.or(inherited.background_color),
288        }
289    }
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
293pub struct RichTextSpan {
294    pub text: String,
295    pub style: RichTextSpanStyle,
296    pub children: Vec<RichTextChild>,
297    pub semantics_label: Option<String>,
298    pub semantics_identifier: Option<String>,
299    #[serde(default)]
300    pub spell_out: Option<bool>,
301    #[serde(default)]
302    pub mouse_cursor: Option<IrMouseCursor>,
303    #[serde(default)]
304    pub actions: Vec<ActionEntry>,
305}
306
307pub type TextSpan = RichTextSpan;
308pub type WidgetSpan = InlineWidgetSpan;
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct InlineWidgetSpan {
312    pub widget: Box<crate::ui::Node>,
313    pub width: f32,
314    pub height: f32,
315    pub semantics_label: Option<String>,
316}
317
318impl PartialEq for InlineWidgetSpan {
319    fn eq(&self, other: &Self) -> bool {
320        self.width == other.width
321            && self.height == other.height
322            && self.semantics_label == other.semantics_label
323            && serde_json::to_vec(&self.widget).ok() == serde_json::to_vec(&other.widget).ok()
324    }
325}
326
327impl InlineWidgetSpan {
328    pub fn new(widget: impl Into<crate::ui::Node>, width: f32, height: f32) -> Self {
329        Self {
330            widget: Box::new(widget.into()),
331            width,
332            height,
333            semantics_label: None,
334        }
335    }
336
337    pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
338        self.semantics_label = Some(label.into());
339        self
340    }
341}
342
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub enum RichTextChild {
345    Span(RichTextSpan),
346    Widget(InlineWidgetSpan),
347}
348
349impl RichTextSpan {
350    pub fn new(text: impl Into<String>) -> Self {
351        Self {
352            text: text.into(),
353            ..Default::default()
354        }
355    }
356
357    pub fn size(mut self, size: f32) -> Self {
358        self.style.font_size = Some(size);
359        self
360    }
361
362    pub fn color(mut self, color: IrColor) -> Self {
363        self.style.color = Some(color);
364        self
365    }
366
367    pub fn underline(mut self, underline: bool) -> Self {
368        self.style.underline = Some(underline);
369        self
370    }
371
372    pub fn family(mut self, family: impl Into<String>) -> Self {
373        self.style.font_family = Some(family.into());
374        self
375    }
376
377    pub fn weight(mut self, weight: u16) -> Self {
378        self.style.font_weight = Some(weight);
379        self
380    }
381
382    pub fn locale(mut self, locale: impl Into<String>) -> Self {
383        self.style.locale = Some(locale.into());
384        self
385    }
386
387    pub fn italic(mut self, italic: bool) -> Self {
388        self.style.font_style = Some(if italic {
389            TextFontStyle::Italic
390        } else {
391            TextFontStyle::Normal
392        });
393        self
394    }
395
396    pub fn line_height(mut self, line_height: f32) -> Self {
397        self.style.line_height = Some(line_height);
398        self
399    }
400
401    pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
402        self.style.letter_spacing = Some(letter_spacing);
403        self
404    }
405
406    pub fn text_scale(mut self, text_scale: f32) -> Self {
407        self.style.text_scale = Some(text_scale);
408        self
409    }
410
411    pub fn text_scaler(mut self, text_scaler: impl Into<TextScaler>) -> Self {
412        self.style.text_scale = Some(text_scaler.into().scale_factor());
413        self
414    }
415
416    pub fn background_color(mut self, color: IrColor) -> Self {
417        self.style.background_color = Some(color);
418        self
419    }
420
421    pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
422        self.semantics_label = Some(label.into());
423        self
424    }
425
426    pub fn semantics_identifier(mut self, identifier: impl Into<String>) -> Self {
427        self.semantics_identifier = Some(identifier.into());
428        self
429    }
430
431    pub fn spell_out(mut self, spell_out: bool) -> Self {
432        self.spell_out = Some(spell_out);
433        self
434    }
435
436    pub fn mouse_cursor(mut self, mouse_cursor: IrMouseCursor) -> Self {
437        self.mouse_cursor = Some(mouse_cursor);
438        self
439    }
440
441    pub fn on_tap(mut self, action: ActionEnvelope) -> Self {
442        upsert_action_entry(&mut self.actions, ActionTrigger::Default, &action);
443        self
444    }
445
446    pub fn on_hover_enter(mut self, action: ActionEnvelope) -> Self {
447        upsert_action_entry(&mut self.actions, ActionTrigger::HoverEnter, &action);
448        self
449    }
450
451    pub fn on_hover_exit(mut self, action: ActionEnvelope) -> Self {
452        upsert_action_entry(&mut self.actions, ActionTrigger::HoverExit, &action);
453        self
454    }
455
456    pub fn on_secondary_click(mut self, action: ActionEnvelope) -> Self {
457        upsert_action_entry(&mut self.actions, ActionTrigger::SecondaryClick, &action);
458        self
459    }
460
461    pub fn child<T>(mut self, child: T) -> Self
462    where
463        T: Into<RichTextChild>,
464    {
465        self.children.push(child.into());
466        self
467    }
468
469    pub fn children<I, T>(mut self, children: I) -> Self
470    where
471        I: IntoIterator<Item = T>,
472        T: Into<RichTextChild>,
473    {
474        self.children.extend(children.into_iter().map(Into::into));
475        self
476    }
477
478    fn push_runs(
479        &self,
480        inherited: &TextRunStyle,
481        runs: &mut Vec<RichTextRun>,
482        inline_widgets: &mut Vec<InlineWidgetSpan>,
483        annotations: &mut Vec<IrRichTextAnnotation>,
484        byte_cursor: &mut usize,
485    ) {
486        let style = self.style.cascade(inherited);
487        let span_start = *byte_cursor;
488        push_rich_text_run(runs, &self.text, &style);
489        *byte_cursor += self.text.len();
490        for child in &self.children {
491            match child {
492                RichTextChild::Span(child) => {
493                    child.push_runs(&style, runs, inline_widgets, annotations, byte_cursor)
494                }
495                RichTextChild::Widget(widget) => {
496                    let inline_id = inline_widgets.len() as u64;
497                    inline_widgets.push(widget.clone());
498                    runs.push(RichTextRun {
499                        text: String::new(),
500                        style: TextRunStyle {
501                            font_size: style.font_size,
502                            color: Some(IrColor {
503                                r: 0,
504                                g: 0,
505                                b: 0,
506                                a: 0,
507                            }),
508                            underline: false,
509                            font_family: Some(encode_inline_widget_marker(
510                                inline_id,
511                                widget.width,
512                                widget.height,
513                            )),
514                            locale: style.locale.clone(),
515                            font_weight: style.font_weight,
516                            font_style: style.font_style,
517                            line_height: style.line_height,
518                            letter_spacing: style.letter_spacing,
519                            text_scale: style.text_scale,
520                            background_color: None,
521                        },
522                        semantics_label: None,
523                        semantics_identifier: None,
524                        spell_out: None,
525                    });
526                }
527            }
528        }
529        let span_end = *byte_cursor;
530        if let Some(annotation) = self.annotation(span_start..span_end) {
531            annotations.push(annotation);
532        }
533    }
534
535    fn collect_semantics_text(&self, out: &mut String) -> bool {
536        let mut has_override = false;
537        if let Some(label) = &self.semantics_label {
538            out.push_str(label);
539            has_override = true;
540        } else {
541            out.push_str(&self.text);
542        }
543        for child in &self.children {
544            match child {
545                RichTextChild::Span(child) => {
546                    has_override |= child.collect_semantics_text(out);
547                }
548                RichTextChild::Widget(widget) => {
549                    if let Some(label) = &widget.semantics_label {
550                        out.push_str(label);
551                        has_override = true;
552                    }
553                }
554            }
555        }
556        has_override
557    }
558
559    fn collect_semantics_identifier(&self) -> Option<String> {
560        if let Some(identifier) = &self.semantics_identifier {
561            return Some(identifier.clone());
562        }
563        for child in &self.children {
564            if let RichTextChild::Span(child) = child {
565                if let Some(identifier) = child.collect_semantics_identifier() {
566                    return Some(identifier);
567                }
568            }
569        }
570        None
571    }
572
573    fn annotation(&self, range: std::ops::Range<usize>) -> Option<IrRichTextAnnotation> {
574        if range.start >= range.end
575            || (self.semantics_label.is_none()
576                && self.semantics_identifier.is_none()
577                && self.spell_out.is_none()
578                && self.mouse_cursor.is_none()
579                && self.actions.is_empty())
580        {
581            return None;
582        }
583
584        Some(IrRichTextAnnotation {
585            range,
586            semantics_label: self.semantics_label.clone(),
587            semantics_identifier: self.semantics_identifier.clone(),
588            spell_out: self.spell_out,
589            mouse_cursor: self.mouse_cursor,
590            actions: self.actions.clone(),
591        })
592    }
593}
594
595impl From<RichTextRun> for RichTextSpan {
596    fn from(value: RichTextRun) -> Self {
597        Self {
598            text: value.text,
599            style: RichTextSpanStyle {
600                font_size: value.style.font_size,
601                color: value.style.color,
602                underline: Some(value.style.underline),
603                font_family: value.style.font_family,
604                locale: value.style.locale,
605                font_weight: value.style.font_weight,
606                font_style: Some(value.style.font_style),
607                line_height: value.style.line_height,
608                letter_spacing: value.style.letter_spacing,
609                text_scale: value.style.text_scale,
610                background_color: value.style.background_color,
611            },
612            children: Vec::new(),
613            semantics_label: value.semantics_label,
614            semantics_identifier: value.semantics_identifier,
615            spell_out: value.spell_out,
616            mouse_cursor: None,
617            actions: Vec::new(),
618        }
619    }
620}
621
622impl From<RichTextRun> for RichTextChild {
623    fn from(value: RichTextRun) -> Self {
624        Self::Span(value.into())
625    }
626}
627
628impl From<RichTextSpan> for RichTextChild {
629    fn from(value: RichTextSpan) -> Self {
630        Self::Span(value)
631    }
632}
633
634impl From<InlineWidgetSpan> for RichTextChild {
635    fn from(value: InlineWidgetSpan) -> Self {
636        Self::Widget(value)
637    }
638}
639
640#[derive(Debug, Default, Clone, Serialize, Deserialize)]
641pub struct Text {
642    pub id: Option<NodeId>,
643    pub content: TextContent,
644    pub semantics: Option<Semantics>,
645    pub width: Option<f32>,
646    pub height: Option<f32>,
647    pub min_width: Option<f32>,
648    pub max_width: Option<f32>,
649    pub min_height: Option<f32>,
650    pub max_height: Option<f32>,
651    pub font_size: Option<f32>,
652    pub color: Option<IrColor>,
653    pub underline: bool,
654    pub font_family: Option<String>,
655    pub font_weight: Option<u16>,
656    pub font_style: TextFontStyle,
657    pub line_height: Option<f32>,
658    pub letter_spacing: Option<f32>,
659    pub locale: Option<String>,
660    pub text_scale: Option<f32>,
661    pub wrap: bool,
662    pub text_align: IrTextAlign,
663    pub text_direction: IrTextDirection,
664    pub text_width_basis: IrTextWidthBasis,
665    pub max_lines: Option<usize>,
666    pub overflow: IrTextOverflow,
667    pub strut_line_height: Option<f32>,
668    pub text_height_behavior: IrTextHeightBehavior,
669    pub selection_range: Option<(usize, usize)>,
670    pub selection_color: Option<IrColor>,
671    pub selection_text_color: Option<IrColor>,
672    pub flex_grow: f32,
673    pub flex_shrink: f32,
674}
675
676impl Text {
677    pub fn new(content: impl Into<TextContent>) -> Self {
678        Self {
679            content: content.into(),
680            wrap: true,
681            ..Default::default()
682        }
683    }
684
685    pub fn width(mut self, w: f32) -> Self {
686        self.width = Some(w);
687        self
688    }
689
690    pub fn height(mut self, h: f32) -> Self {
691        self.height = Some(h);
692        self
693    }
694
695    pub fn min_width(mut self, w: f32) -> Self {
696        self.min_width = Some(w);
697        self
698    }
699
700    pub fn max_width(mut self, w: f32) -> Self {
701        self.max_width = Some(w);
702        self
703    }
704
705    pub fn min_height(mut self, h: f32) -> Self {
706        self.min_height = Some(h);
707        self
708    }
709
710    pub fn max_height(mut self, h: f32) -> Self {
711        self.max_height = Some(h);
712        self
713    }
714
715    pub fn flex_grow(mut self, grow: f32) -> Self {
716        self.flex_grow = grow;
717        self
718    }
719
720    pub fn flex_shrink(mut self, shrink: f32) -> Self {
721        self.flex_shrink = shrink;
722        self
723    }
724
725    pub fn color(mut self, color: IrColor) -> Self {
726        self.color = Some(color);
727        self
728    }
729
730    pub fn underline(mut self, u: bool) -> Self {
731        self.underline = u;
732        self
733    }
734
735    pub fn size(mut self, size: f32) -> Self {
736        self.font_size = Some(size);
737        self
738    }
739
740    pub fn family(mut self, family: impl Into<String>) -> Self {
741        self.font_family = Some(family.into());
742        self
743    }
744
745    pub fn weight(mut self, weight: u16) -> Self {
746        self.font_weight = Some(weight);
747        self
748    }
749
750    pub fn locale(mut self, locale: impl Into<String>) -> Self {
751        self.locale = Some(locale.into());
752        self
753    }
754
755    pub fn italic(mut self, italic: bool) -> Self {
756        self.font_style = if italic {
757            TextFontStyle::Italic
758        } else {
759            TextFontStyle::Normal
760        };
761        self
762    }
763
764    pub fn line_height(mut self, line_height: f32) -> Self {
765        self.line_height = Some(line_height);
766        self
767    }
768
769    pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
770        self.letter_spacing = Some(letter_spacing);
771        self
772    }
773
774    pub fn text_scale(mut self, text_scale: f32) -> Self {
775        self.text_scale = Some(text_scale);
776        self
777    }
778
779    pub fn text_scaler(mut self, text_scaler: impl Into<TextScaler>) -> Self {
780        self.text_scale = Some(text_scaler.into().scale_factor());
781        self
782    }
783
784    pub fn wrap(mut self, wrap: bool) -> Self {
785        self.wrap = wrap;
786        self
787    }
788
789    pub fn text_align(mut self, text_align: IrTextAlign) -> Self {
790        self.text_align = text_align;
791        self
792    }
793
794    pub fn text_direction(mut self, text_direction: IrTextDirection) -> Self {
795        self.text_direction = text_direction;
796        self
797    }
798
799    pub fn text_width_basis(mut self, text_width_basis: IrTextWidthBasis) -> Self {
800        self.text_width_basis = text_width_basis;
801        self
802    }
803
804    pub fn max_lines(mut self, max_lines: usize) -> Self {
805        self.max_lines = Some(max_lines);
806        self
807    }
808
809    pub fn overflow(mut self, overflow: IrTextOverflow) -> Self {
810        self.overflow = overflow;
811        self
812    }
813
814    pub fn strut_line_height(mut self, line_height: f32) -> Self {
815        self.strut_line_height = Some(line_height);
816        self
817    }
818
819    pub fn text_height_behavior(mut self, behavior: IrTextHeightBehavior) -> Self {
820        self.text_height_behavior = behavior;
821        self
822    }
823
824    pub fn selection_range(mut self, range: (usize, usize)) -> Self {
825        self.selection_range = Some(range);
826        self
827    }
828
829    pub fn selection_color(mut self, color: IrColor) -> Self {
830        self.selection_color = Some(color);
831        self
832    }
833
834    pub fn selection_text_color(mut self, color: IrColor) -> Self {
835        self.selection_text_color = Some(color);
836        self
837    }
838
839    pub fn semantics_identifier(mut self, identifier: impl Into<String>) -> Self {
840        let mut semantics = self.semantics.take().unwrap_or_default();
841        semantics.identifier = Some(identifier.into());
842        self.semantics = Some(semantics);
843        self
844    }
845
846    pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
847        self.semantics = Some(merge_semantics_label(self.semantics.take(), label));
848        self
849    }
850
851    pub fn on_tap(mut self, action: ActionEnvelope) -> Self {
852        self.semantics = Some(merge_semantics_action(
853            self.semantics.take(),
854            ActionTrigger::Default,
855            action,
856        ));
857        self
858    }
859
860    pub fn on_hover_enter(mut self, action: ActionEnvelope) -> Self {
861        self.semantics = Some(merge_semantics_action(
862            self.semantics.take(),
863            ActionTrigger::HoverEnter,
864            action,
865        ));
866        self
867    }
868
869    pub fn on_hover_exit(mut self, action: ActionEnvelope) -> Self {
870        self.semantics = Some(merge_semantics_action(
871            self.semantics.take(),
872            ActionTrigger::HoverExit,
873            action,
874        ));
875        self
876    }
877
878    pub fn on_secondary_click(mut self, action: ActionEnvelope) -> Self {
879        self.semantics = Some(merge_semantics_action(
880            self.semantics.take(),
881            ActionTrigger::SecondaryClick,
882            action,
883        ));
884        self
885    }
886
887    pub fn into_node(self) -> crate::ui::Node {
888        crate::ui::Node::Text(self)
889    }
890
891    fn resolve_text(&self, cx: &LoweringContext<'_>) -> String {
892        match &self.content {
893            TextContent::Literal(s) => s.clone(),
894            TextContent::Key(key) => cx
895                .env
896                .i18n
897                .get(&cx.env.locale, key)
898                .map(|s| s.to_string())
899                .unwrap_or_else(|| format!("MISSING:{}", key)),
900        }
901    }
902
903    fn resolved_style(&self, cx: &LoweringContext<'_>) -> fission_ir::op::TextStyle {
904        let scale = self.text_scale.unwrap_or(1.0).max(0.0);
905        let base_font_size = self
906            .font_size
907            .unwrap_or(cx.env.theme.tokens.typography.body_medium_size);
908        fission_ir::op::TextStyle {
909            font_size: base_font_size * scale,
910            color: self
911                .color
912                .unwrap_or(cx.env.theme.tokens.colors.text_primary),
913            underline: self.underline,
914            font_family: self.font_family.clone(),
915            locale: self.locale.clone(),
916            font_weight: self.font_weight.unwrap_or(400),
917            font_style: self.font_style.into(),
918            line_height: Some(self.line_height.unwrap_or(base_font_size * 1.2) * scale),
919            letter_spacing: self.letter_spacing.unwrap_or(0.0) * scale,
920            background_color: None,
921        }
922    }
923
924    fn needs_rich_text(&self) -> bool {
925        self.font_family.is_some()
926            || self.locale.is_some()
927            || self.font_weight.is_some()
928            || self.font_style != TextFontStyle::Normal
929            || self.line_height.is_some()
930            || self.letter_spacing.unwrap_or(0.0) != 0.0
931            || self.text_scale.unwrap_or(1.0) != 1.0
932            || self.selection_range.is_some()
933    }
934}
935
936#[derive(Debug, Default, Clone, Serialize, Deserialize)]
937pub struct RichText {
938    pub id: Option<NodeId>,
939    pub runs: Vec<RichTextRun>,
940    pub inline_widgets: Vec<InlineWidgetSpan>,
941    #[serde(default)]
942    pub annotations: Vec<IrRichTextAnnotation>,
943    pub semantics: Option<Semantics>,
944    pub width: Option<f32>,
945    pub height: Option<f32>,
946    pub min_width: Option<f32>,
947    pub max_width: Option<f32>,
948    pub min_height: Option<f32>,
949    pub max_height: Option<f32>,
950    pub wrap: bool,
951    pub text_align: IrTextAlign,
952    pub text_direction: IrTextDirection,
953    pub text_width_basis: IrTextWidthBasis,
954    pub max_lines: Option<usize>,
955    pub overflow: IrTextOverflow,
956    pub strut_line_height: Option<f32>,
957    pub text_height_behavior: IrTextHeightBehavior,
958    pub selection_range: Option<(usize, usize)>,
959    pub selection_color: Option<IrColor>,
960    pub selection_text_color: Option<IrColor>,
961    pub flex_grow: f32,
962    pub flex_shrink: f32,
963}
964
965impl RichText {
966    pub fn new(runs: Vec<RichTextRun>) -> Self {
967        if runs.iter().any(|run| {
968            run.semantics_label.is_some()
969                || run.semantics_identifier.is_some()
970                || run.spell_out.is_some()
971        }) {
972            return Self::from_spans(runs);
973        }
974
975        Self {
976            runs,
977            inline_widgets: Vec::new(),
978            wrap: true,
979            ..Default::default()
980        }
981    }
982
983    pub fn from_span<T>(span: T) -> Self
984    where
985        T: Into<RichTextChild>,
986    {
987        Self::from_spans(std::iter::once(span))
988    }
989
990    pub fn from_spans<I, T>(spans: I) -> Self
991    where
992        I: IntoIterator<Item = T>,
993        T: Into<RichTextChild>,
994    {
995        let spans: Vec<_> = spans.into_iter().map(Into::into).collect();
996        let mut runs = Vec::new();
997        let mut inline_widgets = Vec::new();
998        let mut annotations = Vec::new();
999        let mut semantics_text = String::new();
1000        let mut has_semantics_override = false;
1001        let mut semantics_identifier = None;
1002        let mut byte_cursor = 0usize;
1003
1004        for span in &spans {
1005            match span {
1006                RichTextChild::Span(span) => {
1007                    span.push_runs(
1008                        &TextRunStyle::default(),
1009                        &mut runs,
1010                        &mut inline_widgets,
1011                        &mut annotations,
1012                        &mut byte_cursor,
1013                    );
1014                    has_semantics_override |= span.collect_semantics_text(&mut semantics_text);
1015                    if semantics_identifier.is_none() {
1016                        semantics_identifier = span.collect_semantics_identifier();
1017                    }
1018                }
1019                RichTextChild::Widget(widget) => {
1020                    let inline_id = inline_widgets.len() as u64;
1021                    inline_widgets.push(widget.clone());
1022                    runs.push(RichTextRun {
1023                        text: String::new(),
1024                        style: TextRunStyle {
1025                            font_size: None,
1026                            color: Some(IrColor {
1027                                r: 0,
1028                                g: 0,
1029                                b: 0,
1030                                a: 0,
1031                            }),
1032                            underline: false,
1033                            font_family: Some(encode_inline_widget_marker(
1034                                inline_id,
1035                                widget.width,
1036                                widget.height,
1037                            )),
1038                            locale: None,
1039                            font_weight: None,
1040                            font_style: TextFontStyle::Normal,
1041                            line_height: None,
1042                            letter_spacing: None,
1043                            text_scale: None,
1044                            background_color: None,
1045                        },
1046                        semantics_label: None,
1047                        semantics_identifier: None,
1048                        spell_out: None,
1049                    });
1050                    if let Some(label) = &widget.semantics_label {
1051                        semantics_text.push_str(label);
1052                        has_semantics_override = true;
1053                    }
1054                }
1055            }
1056        }
1057
1058        let mut rich_text = Self {
1059            runs,
1060            inline_widgets,
1061            annotations,
1062            wrap: true,
1063            ..Default::default()
1064        };
1065        if let Some(identifier) = semantics_identifier {
1066            rich_text = rich_text.semantics_identifier(identifier);
1067        }
1068        if has_semantics_override {
1069            rich_text.semantics = Some(merge_semantics_label(
1070                rich_text.semantics.take(),
1071                semantics_text,
1072            ));
1073        }
1074        rich_text
1075    }
1076
1077    pub fn width(mut self, w: f32) -> Self {
1078        self.width = Some(w);
1079        self
1080    }
1081
1082    pub fn height(mut self, h: f32) -> Self {
1083        self.height = Some(h);
1084        self
1085    }
1086
1087    pub fn min_width(mut self, w: f32) -> Self {
1088        self.min_width = Some(w);
1089        self
1090    }
1091
1092    pub fn max_width(mut self, w: f32) -> Self {
1093        self.max_width = Some(w);
1094        self
1095    }
1096
1097    pub fn min_height(mut self, h: f32) -> Self {
1098        self.min_height = Some(h);
1099        self
1100    }
1101
1102    pub fn max_height(mut self, h: f32) -> Self {
1103        self.max_height = Some(h);
1104        self
1105    }
1106
1107    pub fn flex_grow(mut self, grow: f32) -> Self {
1108        self.flex_grow = grow;
1109        self
1110    }
1111
1112    pub fn flex_shrink(mut self, shrink: f32) -> Self {
1113        self.flex_shrink = shrink;
1114        self
1115    }
1116
1117    pub fn wrap(mut self, wrap: bool) -> Self {
1118        self.wrap = wrap;
1119        self
1120    }
1121
1122    pub fn text_align(mut self, text_align: IrTextAlign) -> Self {
1123        self.text_align = text_align;
1124        self
1125    }
1126
1127    pub fn text_direction(mut self, text_direction: IrTextDirection) -> Self {
1128        self.text_direction = text_direction;
1129        self
1130    }
1131
1132    pub fn text_width_basis(mut self, text_width_basis: IrTextWidthBasis) -> Self {
1133        self.text_width_basis = text_width_basis;
1134        self
1135    }
1136
1137    pub fn max_lines(mut self, max_lines: usize) -> Self {
1138        self.max_lines = Some(max_lines);
1139        self
1140    }
1141
1142    pub fn overflow(mut self, overflow: IrTextOverflow) -> Self {
1143        self.overflow = overflow;
1144        self
1145    }
1146
1147    pub fn strut_line_height(mut self, line_height: f32) -> Self {
1148        self.strut_line_height = Some(line_height);
1149        self
1150    }
1151
1152    pub fn text_height_behavior(mut self, behavior: IrTextHeightBehavior) -> Self {
1153        self.text_height_behavior = behavior;
1154        self
1155    }
1156
1157    pub fn selection_range(mut self, range: (usize, usize)) -> Self {
1158        self.selection_range = Some(range);
1159        self
1160    }
1161
1162    pub fn selection_color(mut self, color: IrColor) -> Self {
1163        self.selection_color = Some(color);
1164        self
1165    }
1166
1167    pub fn selection_text_color(mut self, color: IrColor) -> Self {
1168        self.selection_text_color = Some(color);
1169        self
1170    }
1171
1172    pub fn semantics_identifier(mut self, identifier: impl Into<String>) -> Self {
1173        let mut semantics = self.semantics.take().unwrap_or_default();
1174        semantics.identifier = Some(identifier.into());
1175        self.semantics = Some(semantics);
1176        self
1177    }
1178
1179    pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
1180        self.semantics = Some(merge_semantics_label(self.semantics.take(), label));
1181        self
1182    }
1183
1184    pub fn on_tap(mut self, action: ActionEnvelope) -> Self {
1185        self.semantics = Some(merge_semantics_action(
1186            self.semantics.take(),
1187            ActionTrigger::Default,
1188            action,
1189        ));
1190        self
1191    }
1192
1193    pub fn on_hover_enter(mut self, action: ActionEnvelope) -> Self {
1194        self.semantics = Some(merge_semantics_action(
1195            self.semantics.take(),
1196            ActionTrigger::HoverEnter,
1197            action,
1198        ));
1199        self
1200    }
1201
1202    pub fn on_hover_exit(mut self, action: ActionEnvelope) -> Self {
1203        self.semantics = Some(merge_semantics_action(
1204            self.semantics.take(),
1205            ActionTrigger::HoverExit,
1206            action,
1207        ));
1208        self
1209    }
1210
1211    pub fn on_secondary_click(mut self, action: ActionEnvelope) -> Self {
1212        self.semantics = Some(merge_semantics_action(
1213            self.semantics.take(),
1214            ActionTrigger::SecondaryClick,
1215            action,
1216        ));
1217        self
1218    }
1219
1220    pub fn into_node(self) -> crate::ui::Node {
1221        crate::ui::Node::RichText(self)
1222    }
1223
1224    fn lower_runs(&self, cx: &LoweringContext<'_>) -> Vec<IrTextRun> {
1225        self.runs
1226            .iter()
1227            .map(|run| run.lower_with_theme(&cx.env.theme, None, None))
1228            .collect()
1229    }
1230}
1231
1232fn push_rich_text_run(runs: &mut Vec<RichTextRun>, text: &str, style: &TextRunStyle) {
1233    if text.is_empty() {
1234        return;
1235    }
1236
1237    if let Some(last) = runs.last_mut() {
1238        if last.style == *style {
1239            last.text.push_str(text);
1240            return;
1241        }
1242    }
1243
1244    runs.push(RichTextRun {
1245        text: text.to_string(),
1246        style: style.clone(),
1247        semantics_label: None,
1248        semantics_identifier: None,
1249        spell_out: None,
1250    });
1251}
1252
1253fn apply_selection_to_runs(
1254    runs: Vec<IrTextRun>,
1255    selection_range: Option<(usize, usize)>,
1256    selection_color: Option<IrColor>,
1257    selection_text_color: Option<IrColor>,
1258) -> Vec<IrTextRun> {
1259    let Some((start, end)) = selection_range.map(|(start, end)| (start.min(end), start.max(end)))
1260    else {
1261        return runs;
1262    };
1263    if start == end {
1264        return runs;
1265    }
1266
1267    let selection_fill = selection_color.unwrap_or(IrColor {
1268        r: 38,
1269        g: 132,
1270        b: 255,
1271        a: 64,
1272    });
1273
1274    let mut out = Vec::new();
1275    let mut byte_cursor = 0usize;
1276
1277    for run in runs {
1278        let run_start = byte_cursor;
1279        let run_end = run_start + run.text.len();
1280        byte_cursor = run_end;
1281
1282        if end <= run_start || start >= run_end {
1283            out.push(run);
1284            continue;
1285        }
1286
1287        let local_start = start.saturating_sub(run_start).min(run.text.len());
1288        let local_end = end.saturating_sub(run_start).min(run.text.len());
1289
1290        if local_start > 0 {
1291            out.push(IrTextRun {
1292                text: run.text[..local_start].to_string(),
1293                style: run.style.clone(),
1294            });
1295        }
1296
1297        if local_end > local_start {
1298            let mut style = run.style.clone();
1299            style.background_color = Some(selection_fill);
1300            if let Some(color) = selection_text_color {
1301                style.color = color;
1302            }
1303            out.push(IrTextRun {
1304                text: run.text[local_start..local_end].to_string(),
1305                style,
1306            });
1307        }
1308
1309        if local_end < run.text.len() {
1310            out.push(IrTextRun {
1311                text: run.text[local_end..].to_string(),
1312                style: run.style,
1313            });
1314        }
1315    }
1316
1317    out
1318}
1319
1320fn merge_semantics_label(semantics: Option<Semantics>, label: impl Into<String>) -> Semantics {
1321    let mut semantics = semantics.unwrap_or_default();
1322    semantics.label = Some(label.into());
1323    semantics
1324}
1325
1326fn merge_semantics_action(
1327    semantics: Option<Semantics>,
1328    trigger: ActionTrigger,
1329    action: ActionEnvelope,
1330) -> Semantics {
1331    let mut semantics = semantics.unwrap_or_default();
1332    upsert_semantics_action(&mut semantics, trigger, &action);
1333    semantics
1334}
1335
1336fn upsert_semantics_action(
1337    semantics: &mut Semantics,
1338    trigger: ActionTrigger,
1339    action: &ActionEnvelope,
1340) {
1341    upsert_action_entry(&mut semantics.actions.entries, trigger, action);
1342}
1343
1344fn upsert_action_entry(
1345    entries: &mut Vec<ActionEntry>,
1346    trigger: ActionTrigger,
1347    action: &ActionEnvelope,
1348) {
1349    entries.retain(|entry| entry.trigger != trigger);
1350    entries.push(ActionEntry {
1351        trigger,
1352        action_id: action.id.as_u128(),
1353        payload_data: Some(action.payload.clone()),
1354    });
1355}
1356
1357fn wrap_paint_in_layout(
1358    cx: &mut LoweringContext<'_>,
1359    layout_node_id: NodeId,
1360    paint_node_id: NodeId,
1361    width: Option<f32>,
1362    height: Option<f32>,
1363    min_width: Option<f32>,
1364    max_width: Option<f32>,
1365    min_height: Option<f32>,
1366    max_height: Option<f32>,
1367    clip_to_bounds: bool,
1368    flex_grow: f32,
1369    flex_shrink: f32,
1370) -> NodeId {
1371    let mut layout_builder = NodeBuilder::new(
1372        layout_node_id,
1373        Op::Layout(LayoutOp::Box {
1374            width,
1375            height,
1376            min_width,
1377            max_width,
1378            min_height,
1379            max_height,
1380            padding: [0.0; 4],
1381            flex_grow,
1382            flex_shrink,
1383            aspect_ratio: None,
1384        }),
1385    )
1386    .composite(CompositeStyle {
1387        clip_to_bounds,
1388        ..Default::default()
1389    });
1390    layout_builder.add_child(paint_node_id);
1391    layout_builder.build(cx)
1392}
1393
1394fn resolve_line_height(font_size: f32, line_height: Option<f32>) -> f32 {
1395    line_height.unwrap_or(font_size * 1.2)
1396}
1397
1398fn cap_max_height(
1399    max_height: Option<f32>,
1400    max_lines: Option<usize>,
1401    line_height: f32,
1402) -> Option<f32> {
1403    match max_lines {
1404        Some(lines) => {
1405            let line_cap = line_height * lines as f32;
1406            Some(max_height.map_or(line_cap, |existing| existing.min(line_cap)))
1407        }
1408        None => max_height,
1409    }
1410}
1411
1412fn paragraph_line_height(line_height: f32, strut_line_height: Option<f32>) -> f32 {
1413    strut_line_height.map_or(line_height, |strut| line_height.max(strut))
1414}
1415
1416fn paragraph_style_metadata(
1417    text_align: IrTextAlign,
1418    text_direction: IrTextDirection,
1419    text_width_basis: IrTextWidthBasis,
1420    max_lines: Option<usize>,
1421    overflow: IrTextOverflow,
1422    strut_line_height: Option<f32>,
1423    text_height_behavior: IrTextHeightBehavior,
1424) -> Option<IrTextParagraphStyle> {
1425    let style = IrTextParagraphStyle {
1426        text_align,
1427        text_direction,
1428        text_width_basis,
1429        max_lines,
1430        overflow,
1431        strut_line_height,
1432        text_height_behavior,
1433    };
1434    if style == IrTextParagraphStyle::default() {
1435        None
1436    } else {
1437        Some(style)
1438    }
1439}
1440
1441fn should_clip_paragraph(max_lines: Option<usize>, overflow: IrTextOverflow) -> bool {
1442    max_lines.is_some() || overflow != IrTextOverflow::Visible
1443}
1444
1445fn rich_text_line_height(
1446    runs: &[IrTextRun],
1447    fallback_size: f32,
1448    strut_line_height: Option<f32>,
1449) -> f32 {
1450    runs.iter()
1451        .map(|run| {
1452            if let Some(marker) = decode_inline_widget_marker(run.style.font_family.as_deref()) {
1453                marker.height
1454            } else {
1455                paragraph_line_height(
1456                    resolve_line_height(run.style.font_size, run.style.line_height),
1457                    strut_line_height,
1458                )
1459            }
1460        })
1461        .fold(
1462            paragraph_line_height(resolve_line_height(fallback_size, None), strut_line_height),
1463            f32::max,
1464        )
1465}
1466
1467fn maybe_wrap_semantics(
1468    cx: &mut LoweringContext<'_>,
1469    layout_node_id: NodeId,
1470    semantics: Option<Semantics>,
1471    multiline: bool,
1472) -> NodeId {
1473    if let Some(mut s) = semantics {
1474        if s.role == Role::Generic {
1475            s.role = Role::Text;
1476        }
1477        s.multiline = multiline;
1478        s.focusable |= s
1479            .actions
1480            .entries
1481            .iter()
1482            .any(|entry| entry.trigger == ActionTrigger::Default);
1483        let mut semantics_builder = NodeBuilder::new(cx.next_node_id(), Op::Semantics(s));
1484        semantics_builder.add_child(layout_node_id);
1485        semantics_builder.build(cx)
1486    } else {
1487        layout_node_id
1488    }
1489}
1490
1491impl Lower for Text {
1492    fn lower(&self, cx: &mut LoweringContext) -> NodeId {
1493        let layout_node_id = self.id.unwrap_or_else(|| cx.next_node_id());
1494        let resolved_text = self.resolve_text(cx);
1495        let style = self.resolved_style(cx);
1496        let paragraph_style = paragraph_style_metadata(
1497            self.text_align,
1498            self.text_direction,
1499            self.text_width_basis,
1500            self.max_lines,
1501            self.overflow,
1502            self.strut_line_height,
1503            self.text_height_behavior,
1504        );
1505        let max_height = cap_max_height(
1506            self.max_height,
1507            self.max_lines,
1508            paragraph_line_height(
1509                resolve_line_height(style.font_size, style.line_height),
1510                self.strut_line_height,
1511            ),
1512        );
1513        let clip_to_bounds = should_clip_paragraph(self.max_lines, self.overflow);
1514
1515        let paint_node_id = if self.needs_rich_text() {
1516            let runs = apply_selection_to_runs(
1517                vec![IrTextRun {
1518                    text: resolved_text,
1519                    style: style.clone(),
1520                }],
1521                self.selection_range,
1522                self.selection_color,
1523                self.selection_text_color,
1524            );
1525            NodeBuilder::new(
1526                cx.next_node_id(),
1527                Op::Paint(PaintOp::DrawRichText {
1528                    runs,
1529                    wrap: self.wrap,
1530                    caret_index: None,
1531                    caret_color: None,
1532                    caret_width: None,
1533                    caret_height: None,
1534                    caret_radius: None,
1535                    paragraph_style,
1536                }),
1537            )
1538            .build(cx)
1539        } else {
1540            NodeBuilder::new(
1541                cx.next_node_id(),
1542                Op::Paint(PaintOp::DrawText {
1543                    text: resolved_text,
1544                    size: style.font_size,
1545                    color: style.color,
1546                    underline: style.underline,
1547                    wrap: self.wrap,
1548                    caret_index: None,
1549                    caret_color: None,
1550                    caret_width: None,
1551                    caret_height: None,
1552                    caret_radius: None,
1553                    paragraph_style,
1554                }),
1555            )
1556            .build(cx)
1557        };
1558
1559        let layout_node_id = wrap_paint_in_layout(
1560            cx,
1561            layout_node_id,
1562            paint_node_id,
1563            self.width,
1564            self.height,
1565            self.min_width,
1566            self.max_width,
1567            self.min_height,
1568            max_height,
1569            clip_to_bounds,
1570            self.flex_grow,
1571            self.flex_shrink,
1572        );
1573
1574        maybe_wrap_semantics(cx, layout_node_id, self.semantics.clone(), false)
1575    }
1576}
1577
1578impl Lower for RichText {
1579    fn lower(&self, cx: &mut LoweringContext) -> NodeId {
1580        let layout_node_id = self.id.unwrap_or_else(|| cx.next_node_id());
1581        let runs = self.lower_runs(cx);
1582        let runs = apply_selection_to_runs(
1583            runs,
1584            self.selection_range,
1585            self.selection_color,
1586            self.selection_text_color,
1587        );
1588        let paragraph_style = paragraph_style_metadata(
1589            self.text_align,
1590            self.text_direction,
1591            self.text_width_basis,
1592            self.max_lines,
1593            self.overflow,
1594            self.strut_line_height,
1595            self.text_height_behavior,
1596        );
1597        let max_height = cap_max_height(
1598            self.max_height,
1599            self.max_lines,
1600            rich_text_line_height(
1601                &runs,
1602                cx.env.theme.tokens.typography.body_medium_size,
1603                self.strut_line_height,
1604            ),
1605        );
1606        let clip_to_bounds = should_clip_paragraph(self.max_lines, self.overflow);
1607        let mut paint_builder = NodeBuilder::new(
1608            cx.next_node_id(),
1609            Op::Paint(PaintOp::DrawRichText {
1610                runs,
1611                wrap: self.wrap,
1612                caret_index: None,
1613                caret_color: None,
1614                caret_width: None,
1615                caret_height: None,
1616                caret_radius: None,
1617                paragraph_style,
1618            }),
1619        );
1620        for inline_widget in &self.inline_widgets {
1621            let child_id = inline_widget.widget.lower(cx);
1622            paint_builder.add_child(child_id);
1623        }
1624        let paint_node_id = paint_builder.build(cx);
1625        if !self.annotations.is_empty() {
1626            cx.ir
1627                .custom_render_objects
1628                .insert(paint_node_id, Arc::new(self.annotations.clone()));
1629        }
1630
1631        let layout_node_id = wrap_paint_in_layout(
1632            cx,
1633            layout_node_id,
1634            paint_node_id,
1635            self.width,
1636            self.height,
1637            self.min_width,
1638            self.max_width,
1639            self.min_height,
1640            max_height,
1641            clip_to_bounds,
1642            self.flex_grow,
1643            self.flex_shrink,
1644        );
1645
1646        maybe_wrap_semantics(cx, layout_node_id, self.semantics.clone(), true)
1647    }
1648}