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