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