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