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#[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 pub selectable: bool,
670 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 pub selectable: bool,
975 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}