Skip to main content

fission_core/ui/widgets/
text.rs

1use crate::internal::InternalLower;
2use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
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, Role, Semantics, WidgetId,
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: crate::ui::Widget,
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::Widget>, width: f32, height: f32) -> Self {
329        Self {
330            widget: 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 children<I, T>(mut self, children: I) -> Self
462    where
463        I: IntoIterator<Item = T>,
464        T: Into<RichTextChild>,
465    {
466        self.children.extend(children.into_iter().map(Into::into));
467        self
468    }
469
470    fn push_runs(
471        &self,
472        inherited: &TextRunStyle,
473        runs: &mut Vec<RichTextRun>,
474        inline_widgets: &mut Vec<InlineWidgetSpan>,
475        annotations: &mut Vec<IrRichTextAnnotation>,
476        byte_cursor: &mut usize,
477    ) {
478        let style = self.style.cascade(inherited);
479        let span_start = *byte_cursor;
480        push_rich_text_run(runs, &self.text, &style);
481        *byte_cursor += self.text.len();
482        for child in &self.children {
483            match child {
484                RichTextChild::Span(child) => {
485                    child.push_runs(&style, runs, inline_widgets, annotations, byte_cursor)
486                }
487                RichTextChild::Widget(widget) => {
488                    let inline_id = inline_widgets.len() as u64;
489                    inline_widgets.push(widget.clone());
490                    runs.push(RichTextRun {
491                        text: String::new(),
492                        style: TextRunStyle {
493                            font_size: style.font_size,
494                            color: Some(IrColor {
495                                r: 0,
496                                g: 0,
497                                b: 0,
498                                a: 0,
499                            }),
500                            underline: false,
501                            font_family: Some(encode_inline_widget_marker(
502                                inline_id,
503                                widget.width,
504                                widget.height,
505                            )),
506                            locale: style.locale.clone(),
507                            font_weight: style.font_weight,
508                            font_style: style.font_style,
509                            line_height: style.line_height,
510                            letter_spacing: style.letter_spacing,
511                            text_scale: style.text_scale,
512                            background_color: None,
513                        },
514                        semantics_label: None,
515                        semantics_identifier: None,
516                        spell_out: None,
517                    });
518                }
519            }
520        }
521        let span_end = *byte_cursor;
522        if let Some(annotation) = self.annotation(span_start..span_end) {
523            annotations.push(annotation);
524        }
525    }
526
527    fn collect_semantics_text(&self, out: &mut String) -> bool {
528        let mut has_override = false;
529        if let Some(label) = &self.semantics_label {
530            out.push_str(label);
531            has_override = true;
532        } else {
533            out.push_str(&self.text);
534        }
535        for child in &self.children {
536            match child {
537                RichTextChild::Span(child) => {
538                    has_override |= child.collect_semantics_text(out);
539                }
540                RichTextChild::Widget(widget) => {
541                    if let Some(label) = &widget.semantics_label {
542                        out.push_str(label);
543                        has_override = true;
544                    }
545                }
546            }
547        }
548        has_override
549    }
550
551    fn collect_semantics_identifier(&self) -> Option<String> {
552        if let Some(identifier) = &self.semantics_identifier {
553            return Some(identifier.clone());
554        }
555        for child in &self.children {
556            if let RichTextChild::Span(child) = child {
557                if let Some(identifier) = child.collect_semantics_identifier() {
558                    return Some(identifier);
559                }
560            }
561        }
562        None
563    }
564
565    fn annotation(&self, range: std::ops::Range<usize>) -> Option<IrRichTextAnnotation> {
566        if range.start >= range.end
567            || (self.semantics_label.is_none()
568                && self.semantics_identifier.is_none()
569                && self.spell_out.is_none()
570                && self.mouse_cursor.is_none()
571                && self.actions.is_empty())
572        {
573            return None;
574        }
575
576        Some(IrRichTextAnnotation {
577            range,
578            semantics_label: self.semantics_label.clone(),
579            semantics_identifier: self.semantics_identifier.clone(),
580            spell_out: self.spell_out,
581            mouse_cursor: self.mouse_cursor,
582            actions: self.actions.clone(),
583        })
584    }
585}
586
587impl From<RichTextRun> for RichTextSpan {
588    fn from(value: RichTextRun) -> Self {
589        Self {
590            text: value.text,
591            style: RichTextSpanStyle {
592                font_size: value.style.font_size,
593                color: value.style.color,
594                underline: Some(value.style.underline),
595                font_family: value.style.font_family,
596                locale: value.style.locale,
597                font_weight: value.style.font_weight,
598                font_style: Some(value.style.font_style),
599                line_height: value.style.line_height,
600                letter_spacing: value.style.letter_spacing,
601                text_scale: value.style.text_scale,
602                background_color: value.style.background_color,
603            },
604            children: Vec::new(),
605            semantics_label: value.semantics_label,
606            semantics_identifier: value.semantics_identifier,
607            spell_out: value.spell_out,
608            mouse_cursor: None,
609            actions: Vec::new(),
610        }
611    }
612}
613
614impl From<RichTextRun> for RichTextChild {
615    fn from(value: RichTextRun) -> Self {
616        Self::Span(value.into())
617    }
618}
619
620impl From<RichTextSpan> for RichTextChild {
621    fn from(value: RichTextSpan) -> Self {
622        Self::Span(value)
623    }
624}
625
626impl From<InlineWidgetSpan> for RichTextChild {
627    fn from(value: InlineWidgetSpan) -> Self {
628        Self::Widget(value)
629    }
630}
631
632#[derive(Debug, Default, Clone, Serialize, Deserialize)]
633pub struct Text {
634    pub id: Option<WidgetId>,
635    pub content: TextContent,
636    pub semantics: Option<Semantics>,
637    pub width: Option<f32>,
638    pub height: Option<f32>,
639    pub min_width: Option<f32>,
640    pub max_width: Option<f32>,
641    pub min_height: Option<f32>,
642    pub max_height: Option<f32>,
643    pub font_size: Option<f32>,
644    pub color: Option<IrColor>,
645    pub underline: bool,
646    pub font_family: Option<String>,
647    pub font_weight: Option<u16>,
648    pub font_style: TextFontStyle,
649    pub line_height: Option<f32>,
650    pub letter_spacing: Option<f32>,
651    pub locale: Option<String>,
652    pub text_scale: Option<f32>,
653    pub wrap: bool,
654    pub text_align: IrTextAlign,
655    pub text_direction: IrTextDirection,
656    pub text_width_basis: IrTextWidthBasis,
657    pub max_lines: Option<usize>,
658    pub overflow: IrTextOverflow,
659    pub strut_line_height: Option<f32>,
660    pub text_height_behavior: IrTextHeightBehavior,
661    pub selection_range: Option<(usize, usize)>,
662    pub selection_color: Option<IrColor>,
663    pub selection_text_color: Option<IrColor>,
664    pub flex_grow: f32,
665    pub flex_shrink: f32,
666}
667
668impl Text {
669    pub fn new(content: impl Into<TextContent>) -> Self {
670        Self {
671            content: content.into(),
672            wrap: true,
673            ..Default::default()
674        }
675    }
676
677    pub fn width(mut self, w: f32) -> Self {
678        self.width = Some(w);
679        self
680    }
681
682    pub fn height(mut self, h: f32) -> Self {
683        self.height = Some(h);
684        self
685    }
686
687    pub fn min_width(mut self, w: f32) -> Self {
688        self.min_width = Some(w);
689        self
690    }
691
692    pub fn max_width(mut self, w: f32) -> Self {
693        self.max_width = Some(w);
694        self
695    }
696
697    pub fn min_height(mut self, h: f32) -> Self {
698        self.min_height = Some(h);
699        self
700    }
701
702    pub fn max_height(mut self, h: f32) -> Self {
703        self.max_height = Some(h);
704        self
705    }
706
707    pub fn flex_grow(mut self, grow: f32) -> Self {
708        self.flex_grow = grow;
709        self
710    }
711
712    pub fn flex_shrink(mut self, shrink: f32) -> Self {
713        self.flex_shrink = shrink;
714        self
715    }
716
717    pub fn color(mut self, color: IrColor) -> Self {
718        self.color = Some(color);
719        self
720    }
721
722    pub fn underline(mut self, u: bool) -> Self {
723        self.underline = u;
724        self
725    }
726
727    pub fn size(mut self, size: f32) -> Self {
728        self.font_size = Some(size);
729        self
730    }
731
732    pub fn family(mut self, family: impl Into<String>) -> Self {
733        self.font_family = Some(family.into());
734        self
735    }
736
737    pub fn weight(mut self, weight: u16) -> Self {
738        self.font_weight = Some(weight);
739        self
740    }
741
742    pub fn locale(mut self, locale: impl Into<String>) -> Self {
743        self.locale = Some(locale.into());
744        self
745    }
746
747    pub fn italic(mut self, italic: bool) -> Self {
748        self.font_style = if italic {
749            TextFontStyle::Italic
750        } else {
751            TextFontStyle::Normal
752        };
753        self
754    }
755
756    pub fn line_height(mut self, line_height: f32) -> Self {
757        self.line_height = Some(line_height);
758        self
759    }
760
761    pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
762        self.letter_spacing = Some(letter_spacing);
763        self
764    }
765
766    pub fn text_scale(mut self, text_scale: f32) -> Self {
767        self.text_scale = Some(text_scale);
768        self
769    }
770
771    pub fn text_scaler(mut self, text_scaler: impl Into<TextScaler>) -> Self {
772        self.text_scale = Some(text_scaler.into().scale_factor());
773        self
774    }
775
776    pub fn wrap(mut self, wrap: bool) -> Self {
777        self.wrap = wrap;
778        self
779    }
780
781    pub fn text_align(mut self, text_align: IrTextAlign) -> Self {
782        self.text_align = text_align;
783        self
784    }
785
786    pub fn text_direction(mut self, text_direction: IrTextDirection) -> Self {
787        self.text_direction = text_direction;
788        self
789    }
790
791    pub fn text_width_basis(mut self, text_width_basis: IrTextWidthBasis) -> Self {
792        self.text_width_basis = text_width_basis;
793        self
794    }
795
796    pub fn max_lines(mut self, max_lines: usize) -> Self {
797        self.max_lines = Some(max_lines);
798        self
799    }
800
801    pub fn overflow(mut self, overflow: IrTextOverflow) -> Self {
802        self.overflow = overflow;
803        self
804    }
805
806    pub fn strut_line_height(mut self, line_height: f32) -> Self {
807        self.strut_line_height = Some(line_height);
808        self
809    }
810
811    pub fn text_height_behavior(mut self, behavior: IrTextHeightBehavior) -> Self {
812        self.text_height_behavior = behavior;
813        self
814    }
815
816    pub fn selection_range(mut self, range: (usize, usize)) -> Self {
817        self.selection_range = Some(range);
818        self
819    }
820
821    pub fn selection_color(mut self, color: IrColor) -> Self {
822        self.selection_color = Some(color);
823        self
824    }
825
826    pub fn selection_text_color(mut self, color: IrColor) -> Self {
827        self.selection_text_color = Some(color);
828        self
829    }
830
831    pub fn semantics_identifier(mut self, identifier: impl Into<String>) -> Self {
832        let mut semantics = self.semantics.take().unwrap_or_default();
833        semantics.identifier = Some(identifier.into());
834        self.semantics = Some(semantics);
835        self
836    }
837
838    pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
839        self.semantics = Some(merge_semantics_label(self.semantics.take(), label));
840        self
841    }
842
843    pub fn on_tap(mut self, action: ActionEnvelope) -> Self {
844        self.semantics = Some(merge_semantics_action(
845            self.semantics.take(),
846            ActionTrigger::Default,
847            action,
848        ));
849        self
850    }
851
852    pub fn on_hover_enter(mut self, action: ActionEnvelope) -> Self {
853        self.semantics = Some(merge_semantics_action(
854            self.semantics.take(),
855            ActionTrigger::HoverEnter,
856            action,
857        ));
858        self
859    }
860
861    pub fn on_hover_exit(mut self, action: ActionEnvelope) -> Self {
862        self.semantics = Some(merge_semantics_action(
863            self.semantics.take(),
864            ActionTrigger::HoverExit,
865            action,
866        ));
867        self
868    }
869
870    pub fn on_secondary_click(mut self, action: ActionEnvelope) -> Self {
871        self.semantics = Some(merge_semantics_action(
872            self.semantics.take(),
873            ActionTrigger::SecondaryClick,
874            action,
875        ));
876        self
877    }
878
879    fn resolve_text(&self, cx: &InternalLoweringCx<'_>) -> String {
880        match &self.content {
881            TextContent::Literal(s) => s.clone(),
882            TextContent::Key(key) => cx
883                .env
884                .i18n
885                .get(&cx.env.locale, key)
886                .map(|s| s.to_string())
887                .unwrap_or_else(|| format!("MISSING:{}", key)),
888        }
889    }
890
891    fn resolved_style(&self, cx: &InternalLoweringCx<'_>) -> fission_ir::op::TextStyle {
892        let scale = self.text_scale.unwrap_or(1.0).max(0.0);
893        let base_font_size = self
894            .font_size
895            .unwrap_or(cx.env.theme.tokens.typography.body_medium_size);
896        fission_ir::op::TextStyle {
897            font_size: base_font_size * scale,
898            color: self
899                .color
900                .unwrap_or(cx.env.theme.tokens.colors.text_primary),
901            underline: self.underline,
902            font_family: self.font_family.clone(),
903            locale: self.locale.clone(),
904            font_weight: self.font_weight.unwrap_or(400),
905            font_style: self.font_style.into(),
906            line_height: Some(self.line_height.unwrap_or(base_font_size * 1.2) * scale),
907            letter_spacing: self.letter_spacing.unwrap_or(0.0) * scale,
908            background_color: None,
909        }
910    }
911
912    fn needs_rich_text(&self) -> bool {
913        self.font_family.is_some()
914            || self.locale.is_some()
915            || self.font_weight.is_some()
916            || self.font_style != TextFontStyle::Normal
917            || self.line_height.is_some()
918            || self.letter_spacing.unwrap_or(0.0) != 0.0
919            || self.text_scale.unwrap_or(1.0) != 1.0
920            || self.selection_range.is_some()
921    }
922}
923
924#[derive(Debug, Default, Clone, Serialize, Deserialize)]
925pub struct RichText {
926    pub id: Option<WidgetId>,
927    pub runs: Vec<RichTextRun>,
928    pub inline_widgets: Vec<InlineWidgetSpan>,
929    #[serde(default)]
930    pub annotations: Vec<IrRichTextAnnotation>,
931    pub semantics: Option<Semantics>,
932    pub width: Option<f32>,
933    pub height: Option<f32>,
934    pub min_width: Option<f32>,
935    pub max_width: Option<f32>,
936    pub min_height: Option<f32>,
937    pub max_height: Option<f32>,
938    pub wrap: bool,
939    pub text_align: IrTextAlign,
940    pub text_direction: IrTextDirection,
941    pub text_width_basis: IrTextWidthBasis,
942    pub max_lines: Option<usize>,
943    pub overflow: IrTextOverflow,
944    pub strut_line_height: Option<f32>,
945    pub text_height_behavior: IrTextHeightBehavior,
946    pub selection_range: Option<(usize, usize)>,
947    pub selection_color: Option<IrColor>,
948    pub selection_text_color: Option<IrColor>,
949    pub flex_grow: f32,
950    pub flex_shrink: f32,
951}
952
953impl RichText {
954    pub fn new(runs: Vec<RichTextRun>) -> Self {
955        if runs.iter().any(|run| {
956            run.semantics_label.is_some()
957                || run.semantics_identifier.is_some()
958                || run.spell_out.is_some()
959        }) {
960            return Self::from_spans(runs);
961        }
962
963        Self {
964            runs,
965            inline_widgets: Vec::new(),
966            wrap: true,
967            ..Default::default()
968        }
969    }
970
971    pub fn from_span<T>(span: T) -> Self
972    where
973        T: Into<RichTextChild>,
974    {
975        Self::from_spans(std::iter::once(span))
976    }
977
978    pub fn from_spans<I, T>(spans: I) -> Self
979    where
980        I: IntoIterator<Item = T>,
981        T: Into<RichTextChild>,
982    {
983        let spans: Vec<_> = spans.into_iter().map(Into::into).collect();
984        let mut runs = Vec::new();
985        let mut inline_widgets = Vec::new();
986        let mut annotations = Vec::new();
987        let mut semantics_text = String::new();
988        let mut has_semantics_override = false;
989        let mut semantics_identifier = None;
990        let mut byte_cursor = 0usize;
991
992        for span in &spans {
993            match span {
994                RichTextChild::Span(span) => {
995                    span.push_runs(
996                        &TextRunStyle::default(),
997                        &mut runs,
998                        &mut inline_widgets,
999                        &mut annotations,
1000                        &mut byte_cursor,
1001                    );
1002                    has_semantics_override |= span.collect_semantics_text(&mut semantics_text);
1003                    if semantics_identifier.is_none() {
1004                        semantics_identifier = span.collect_semantics_identifier();
1005                    }
1006                }
1007                RichTextChild::Widget(widget) => {
1008                    let inline_id = inline_widgets.len() as u64;
1009                    inline_widgets.push(widget.clone());
1010                    runs.push(RichTextRun {
1011                        text: String::new(),
1012                        style: TextRunStyle {
1013                            font_size: None,
1014                            color: Some(IrColor {
1015                                r: 0,
1016                                g: 0,
1017                                b: 0,
1018                                a: 0,
1019                            }),
1020                            underline: false,
1021                            font_family: Some(encode_inline_widget_marker(
1022                                inline_id,
1023                                widget.width,
1024                                widget.height,
1025                            )),
1026                            locale: None,
1027                            font_weight: None,
1028                            font_style: TextFontStyle::Normal,
1029                            line_height: None,
1030                            letter_spacing: None,
1031                            text_scale: None,
1032                            background_color: None,
1033                        },
1034                        semantics_label: None,
1035                        semantics_identifier: None,
1036                        spell_out: None,
1037                    });
1038                    if let Some(label) = &widget.semantics_label {
1039                        semantics_text.push_str(label);
1040                        has_semantics_override = true;
1041                    }
1042                }
1043            }
1044        }
1045
1046        let mut rich_text = Self {
1047            runs,
1048            inline_widgets,
1049            annotations,
1050            wrap: true,
1051            ..Default::default()
1052        };
1053        if let Some(identifier) = semantics_identifier {
1054            rich_text = rich_text.semantics_identifier(identifier);
1055        }
1056        if has_semantics_override {
1057            rich_text.semantics = Some(merge_semantics_label(
1058                rich_text.semantics.take(),
1059                semantics_text,
1060            ));
1061        }
1062        rich_text
1063    }
1064
1065    pub fn width(mut self, w: f32) -> Self {
1066        self.width = Some(w);
1067        self
1068    }
1069
1070    pub fn height(mut self, h: f32) -> Self {
1071        self.height = Some(h);
1072        self
1073    }
1074
1075    pub fn min_width(mut self, w: f32) -> Self {
1076        self.min_width = Some(w);
1077        self
1078    }
1079
1080    pub fn max_width(mut self, w: f32) -> Self {
1081        self.max_width = Some(w);
1082        self
1083    }
1084
1085    pub fn min_height(mut self, h: f32) -> Self {
1086        self.min_height = Some(h);
1087        self
1088    }
1089
1090    pub fn max_height(mut self, h: f32) -> Self {
1091        self.max_height = Some(h);
1092        self
1093    }
1094
1095    pub fn flex_grow(mut self, grow: f32) -> Self {
1096        self.flex_grow = grow;
1097        self
1098    }
1099
1100    pub fn flex_shrink(mut self, shrink: f32) -> Self {
1101        self.flex_shrink = shrink;
1102        self
1103    }
1104
1105    pub fn wrap(mut self, wrap: bool) -> Self {
1106        self.wrap = wrap;
1107        self
1108    }
1109
1110    pub fn text_align(mut self, text_align: IrTextAlign) -> Self {
1111        self.text_align = text_align;
1112        self
1113    }
1114
1115    pub fn text_direction(mut self, text_direction: IrTextDirection) -> Self {
1116        self.text_direction = text_direction;
1117        self
1118    }
1119
1120    pub fn text_width_basis(mut self, text_width_basis: IrTextWidthBasis) -> Self {
1121        self.text_width_basis = text_width_basis;
1122        self
1123    }
1124
1125    pub fn max_lines(mut self, max_lines: usize) -> Self {
1126        self.max_lines = Some(max_lines);
1127        self
1128    }
1129
1130    pub fn overflow(mut self, overflow: IrTextOverflow) -> Self {
1131        self.overflow = overflow;
1132        self
1133    }
1134
1135    pub fn strut_line_height(mut self, line_height: f32) -> Self {
1136        self.strut_line_height = Some(line_height);
1137        self
1138    }
1139
1140    pub fn text_height_behavior(mut self, behavior: IrTextHeightBehavior) -> Self {
1141        self.text_height_behavior = behavior;
1142        self
1143    }
1144
1145    pub fn selection_range(mut self, range: (usize, usize)) -> Self {
1146        self.selection_range = Some(range);
1147        self
1148    }
1149
1150    pub fn selection_color(mut self, color: IrColor) -> Self {
1151        self.selection_color = Some(color);
1152        self
1153    }
1154
1155    pub fn selection_text_color(mut self, color: IrColor) -> Self {
1156        self.selection_text_color = Some(color);
1157        self
1158    }
1159
1160    pub fn semantics_identifier(mut self, identifier: impl Into<String>) -> Self {
1161        let mut semantics = self.semantics.take().unwrap_or_default();
1162        semantics.identifier = Some(identifier.into());
1163        self.semantics = Some(semantics);
1164        self
1165    }
1166
1167    pub fn semantics_label(mut self, label: impl Into<String>) -> Self {
1168        self.semantics = Some(merge_semantics_label(self.semantics.take(), label));
1169        self
1170    }
1171
1172    pub fn on_tap(mut self, action: ActionEnvelope) -> Self {
1173        self.semantics = Some(merge_semantics_action(
1174            self.semantics.take(),
1175            ActionTrigger::Default,
1176            action,
1177        ));
1178        self
1179    }
1180
1181    pub fn on_hover_enter(mut self, action: ActionEnvelope) -> Self {
1182        self.semantics = Some(merge_semantics_action(
1183            self.semantics.take(),
1184            ActionTrigger::HoverEnter,
1185            action,
1186        ));
1187        self
1188    }
1189
1190    pub fn on_hover_exit(mut self, action: ActionEnvelope) -> Self {
1191        self.semantics = Some(merge_semantics_action(
1192            self.semantics.take(),
1193            ActionTrigger::HoverExit,
1194            action,
1195        ));
1196        self
1197    }
1198
1199    pub fn on_secondary_click(mut self, action: ActionEnvelope) -> Self {
1200        self.semantics = Some(merge_semantics_action(
1201            self.semantics.take(),
1202            ActionTrigger::SecondaryClick,
1203            action,
1204        ));
1205        self
1206    }
1207
1208    fn lower_runs(&self, cx: &InternalLoweringCx<'_>) -> Vec<IrTextRun> {
1209        self.runs
1210            .iter()
1211            .map(|run| run.lower_with_theme(&cx.env.theme, None, None))
1212            .collect()
1213    }
1214}
1215
1216fn push_rich_text_run(runs: &mut Vec<RichTextRun>, text: &str, style: &TextRunStyle) {
1217    if text.is_empty() {
1218        return;
1219    }
1220
1221    if let Some(last) = runs.last_mut() {
1222        if last.style == *style {
1223            last.text.push_str(text);
1224            return;
1225        }
1226    }
1227
1228    runs.push(RichTextRun {
1229        text: text.to_string(),
1230        style: style.clone(),
1231        semantics_label: None,
1232        semantics_identifier: None,
1233        spell_out: None,
1234    });
1235}
1236
1237fn apply_selection_to_runs(
1238    runs: Vec<IrTextRun>,
1239    selection_range: Option<(usize, usize)>,
1240    selection_color: Option<IrColor>,
1241    selection_text_color: Option<IrColor>,
1242) -> Vec<IrTextRun> {
1243    let Some((start, end)) = selection_range.map(|(start, end)| (start.min(end), start.max(end)))
1244    else {
1245        return runs;
1246    };
1247    if start == end {
1248        return runs;
1249    }
1250
1251    let selection_fill = selection_color.unwrap_or(IrColor {
1252        r: 38,
1253        g: 132,
1254        b: 255,
1255        a: 64,
1256    });
1257
1258    let mut out = Vec::new();
1259    let mut byte_cursor = 0usize;
1260
1261    for run in runs {
1262        let run_start = byte_cursor;
1263        let run_end = run_start + run.text.len();
1264        byte_cursor = run_end;
1265
1266        if end <= run_start || start >= run_end {
1267            out.push(run);
1268            continue;
1269        }
1270
1271        let local_start = start.saturating_sub(run_start).min(run.text.len());
1272        let local_end = end.saturating_sub(run_start).min(run.text.len());
1273
1274        if local_start > 0 {
1275            out.push(IrTextRun {
1276                text: run.text[..local_start].to_string(),
1277                style: run.style.clone(),
1278            });
1279        }
1280
1281        if local_end > local_start {
1282            let mut style = run.style.clone();
1283            style.background_color = Some(selection_fill);
1284            if let Some(color) = selection_text_color {
1285                style.color = color;
1286            }
1287            out.push(IrTextRun {
1288                text: run.text[local_start..local_end].to_string(),
1289                style,
1290            });
1291        }
1292
1293        if local_end < run.text.len() {
1294            out.push(IrTextRun {
1295                text: run.text[local_end..].to_string(),
1296                style: run.style,
1297            });
1298        }
1299    }
1300
1301    out
1302}
1303
1304fn merge_semantics_label(semantics: Option<Semantics>, label: impl Into<String>) -> Semantics {
1305    let mut semantics = semantics.unwrap_or_default();
1306    semantics.label = Some(label.into());
1307    semantics
1308}
1309
1310fn merge_semantics_action(
1311    semantics: Option<Semantics>,
1312    trigger: ActionTrigger,
1313    action: ActionEnvelope,
1314) -> Semantics {
1315    let mut semantics = semantics.unwrap_or_default();
1316    upsert_semantics_action(&mut semantics, trigger, &action);
1317    semantics
1318}
1319
1320fn upsert_semantics_action(
1321    semantics: &mut Semantics,
1322    trigger: ActionTrigger,
1323    action: &ActionEnvelope,
1324) {
1325    upsert_action_entry(&mut semantics.actions.entries, trigger, action);
1326}
1327
1328fn upsert_action_entry(
1329    entries: &mut Vec<ActionEntry>,
1330    trigger: ActionTrigger,
1331    action: &ActionEnvelope,
1332) {
1333    entries.retain(|entry| entry.trigger != trigger);
1334    entries.push(ActionEntry {
1335        trigger,
1336        action_id: action.id.as_u128(),
1337        payload_data: Some(action.payload.clone()),
1338    });
1339}
1340
1341fn wrap_paint_in_layout(
1342    cx: &mut InternalLoweringCx<'_>,
1343    layout_node_id: WidgetId,
1344    paint_node_id: WidgetId,
1345    width: Option<f32>,
1346    height: Option<f32>,
1347    min_width: Option<f32>,
1348    max_width: Option<f32>,
1349    min_height: Option<f32>,
1350    max_height: Option<f32>,
1351    clip_to_bounds: bool,
1352    flex_grow: f32,
1353    flex_shrink: f32,
1354) -> WidgetId {
1355    let mut layout_builder = InternalIrBuilder::new(
1356        layout_node_id,
1357        Op::Layout(LayoutOp::Box {
1358            width,
1359            height,
1360            min_width,
1361            max_width,
1362            min_height,
1363            max_height,
1364            padding: [0.0; 4],
1365            flex_grow,
1366            flex_shrink,
1367            aspect_ratio: None,
1368        }),
1369    )
1370    .composite(CompositeStyle {
1371        clip_to_bounds,
1372        ..Default::default()
1373    });
1374    layout_builder.add_child(paint_node_id);
1375    layout_builder.build(cx)
1376}
1377
1378fn resolve_line_height(font_size: f32, line_height: Option<f32>) -> f32 {
1379    line_height.unwrap_or(font_size * 1.2)
1380}
1381
1382fn cap_max_height(
1383    max_height: Option<f32>,
1384    max_lines: Option<usize>,
1385    line_height: f32,
1386) -> Option<f32> {
1387    match max_lines {
1388        Some(lines) => {
1389            let line_cap = line_height * lines as f32;
1390            Some(max_height.map_or(line_cap, |existing| existing.min(line_cap)))
1391        }
1392        None => max_height,
1393    }
1394}
1395
1396fn paragraph_line_height(line_height: f32, strut_line_height: Option<f32>) -> f32 {
1397    strut_line_height.map_or(line_height, |strut| line_height.max(strut))
1398}
1399
1400fn paragraph_style_metadata(
1401    text_align: IrTextAlign,
1402    text_direction: IrTextDirection,
1403    text_width_basis: IrTextWidthBasis,
1404    max_lines: Option<usize>,
1405    overflow: IrTextOverflow,
1406    strut_line_height: Option<f32>,
1407    text_height_behavior: IrTextHeightBehavior,
1408) -> Option<IrTextParagraphStyle> {
1409    let style = IrTextParagraphStyle {
1410        text_align,
1411        text_direction,
1412        text_width_basis,
1413        max_lines,
1414        overflow,
1415        strut_line_height,
1416        text_height_behavior,
1417    };
1418    if style == IrTextParagraphStyle::default() {
1419        None
1420    } else {
1421        Some(style)
1422    }
1423}
1424
1425fn should_clip_paragraph(max_lines: Option<usize>, overflow: IrTextOverflow) -> bool {
1426    max_lines.is_some() || overflow != IrTextOverflow::Visible
1427}
1428
1429fn rich_text_line_height(
1430    runs: &[IrTextRun],
1431    fallback_size: f32,
1432    strut_line_height: Option<f32>,
1433) -> f32 {
1434    runs.iter()
1435        .map(|run| {
1436            if let Some(marker) = decode_inline_widget_marker(run.style.font_family.as_deref()) {
1437                marker.height
1438            } else {
1439                paragraph_line_height(
1440                    resolve_line_height(run.style.font_size, run.style.line_height),
1441                    strut_line_height,
1442                )
1443            }
1444        })
1445        .fold(
1446            paragraph_line_height(resolve_line_height(fallback_size, None), strut_line_height),
1447            f32::max,
1448        )
1449}
1450
1451fn maybe_wrap_semantics(
1452    cx: &mut InternalLoweringCx<'_>,
1453    layout_node_id: WidgetId,
1454    semantics: Option<Semantics>,
1455    multiline: bool,
1456) -> WidgetId {
1457    if let Some(mut s) = semantics {
1458        if s.role == Role::Generic {
1459            s.role = Role::Text;
1460        }
1461        s.multiline = multiline;
1462        s.focusable |= s
1463            .actions
1464            .entries
1465            .iter()
1466            .any(|entry| entry.trigger == ActionTrigger::Default);
1467        let mut semantics_builder = InternalIrBuilder::new(cx.next_node_id(), Op::Semantics(s));
1468        semantics_builder.add_child(layout_node_id);
1469        semantics_builder.build(cx)
1470    } else {
1471        layout_node_id
1472    }
1473}
1474
1475impl InternalLower for Text {
1476    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
1477        let layout_node_id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
1478        let resolved_text = self.resolve_text(cx);
1479        let style = self.resolved_style(cx);
1480        let paragraph_style = paragraph_style_metadata(
1481            self.text_align,
1482            self.text_direction,
1483            self.text_width_basis,
1484            self.max_lines,
1485            self.overflow,
1486            self.strut_line_height,
1487            self.text_height_behavior,
1488        );
1489        let max_height = cap_max_height(
1490            self.max_height,
1491            self.max_lines,
1492            paragraph_line_height(
1493                resolve_line_height(style.font_size, style.line_height),
1494                self.strut_line_height,
1495            ),
1496        );
1497        let clip_to_bounds = should_clip_paragraph(self.max_lines, self.overflow);
1498
1499        let paint_node_id = if self.needs_rich_text() {
1500            let runs = apply_selection_to_runs(
1501                vec![IrTextRun {
1502                    text: resolved_text,
1503                    style: style.clone(),
1504                }],
1505                self.selection_range,
1506                self.selection_color,
1507                self.selection_text_color,
1508            );
1509            InternalIrBuilder::new(
1510                cx.next_node_id(),
1511                Op::Paint(PaintOp::DrawRichText {
1512                    runs,
1513                    wrap: self.wrap,
1514                    caret_index: None,
1515                    caret_color: None,
1516                    caret_width: None,
1517                    caret_height: None,
1518                    caret_radius: None,
1519                    paragraph_style,
1520                }),
1521            )
1522            .build(cx)
1523        } else {
1524            InternalIrBuilder::new(
1525                cx.next_node_id(),
1526                Op::Paint(PaintOp::DrawText {
1527                    text: resolved_text,
1528                    size: style.font_size,
1529                    color: style.color,
1530                    underline: style.underline,
1531                    wrap: self.wrap,
1532                    caret_index: None,
1533                    caret_color: None,
1534                    caret_width: None,
1535                    caret_height: None,
1536                    caret_radius: None,
1537                    paragraph_style,
1538                }),
1539            )
1540            .build(cx)
1541        };
1542
1543        let layout_node_id = wrap_paint_in_layout(
1544            cx,
1545            layout_node_id,
1546            paint_node_id,
1547            self.width,
1548            self.height,
1549            self.min_width,
1550            self.max_width,
1551            self.min_height,
1552            max_height,
1553            clip_to_bounds,
1554            self.flex_grow,
1555            self.flex_shrink,
1556        );
1557
1558        maybe_wrap_semantics(cx, layout_node_id, self.semantics.clone(), false)
1559    }
1560}
1561
1562impl InternalLower for RichText {
1563    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
1564        let layout_node_id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
1565        let runs = self.lower_runs(cx);
1566        let runs = apply_selection_to_runs(
1567            runs,
1568            self.selection_range,
1569            self.selection_color,
1570            self.selection_text_color,
1571        );
1572        let paragraph_style = paragraph_style_metadata(
1573            self.text_align,
1574            self.text_direction,
1575            self.text_width_basis,
1576            self.max_lines,
1577            self.overflow,
1578            self.strut_line_height,
1579            self.text_height_behavior,
1580        );
1581        let max_height = cap_max_height(
1582            self.max_height,
1583            self.max_lines,
1584            rich_text_line_height(
1585                &runs,
1586                cx.env.theme.tokens.typography.body_medium_size,
1587                self.strut_line_height,
1588            ),
1589        );
1590        let clip_to_bounds = should_clip_paragraph(self.max_lines, self.overflow);
1591        let mut paint_builder = InternalIrBuilder::new(
1592            cx.next_node_id(),
1593            Op::Paint(PaintOp::DrawRichText {
1594                runs,
1595                wrap: self.wrap,
1596                caret_index: None,
1597                caret_color: None,
1598                caret_width: None,
1599                caret_height: None,
1600                caret_radius: None,
1601                paragraph_style,
1602            }),
1603        );
1604        for inline_widget in &self.inline_widgets {
1605            let child_id = inline_widget.widget.lower(cx);
1606            paint_builder.add_child(child_id);
1607        }
1608        let paint_node_id = paint_builder.build(cx);
1609        if !self.annotations.is_empty() {
1610            cx.ir
1611                .custom_render_objects
1612                .insert(paint_node_id, Arc::new(self.annotations.clone()));
1613        }
1614
1615        let layout_node_id = wrap_paint_in_layout(
1616            cx,
1617            layout_node_id,
1618            paint_node_id,
1619            self.width,
1620            self.height,
1621            self.min_width,
1622            self.max_width,
1623            self.min_height,
1624            max_height,
1625            clip_to_bounds,
1626            self.flex_grow,
1627            self.flex_shrink,
1628        );
1629
1630        maybe_wrap_semantics(cx, layout_node_id, self.semantics.clone(), true)
1631    }
1632}