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