Skip to main content

fission_core/ui/widgets/
text.rs

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