Skip to main content

freya_core/elements/
paragraph.rs

1//! [paragraph()] makes it possible to render rich text with different styles. Its a more customizable API than [crate::elements::label].
2
3use std::{
4    any::Any,
5    borrow::Cow,
6    cell::RefCell,
7    fmt::{
8        Debug,
9        Display,
10    },
11    rc::Rc,
12};
13
14use freya_engine::prelude::{
15    BlendMode,
16    Canvas,
17    FontCollection,
18    FontStyle,
19    Paint,
20    PaintStyle,
21    ParagraphBuilder,
22    ParagraphStyle,
23    PlaceholderAlignment,
24    PlaceholderStyle,
25    RectHeightStyle,
26    RectWidthStyle,
27    SaveLayerRec,
28    SkParagraph,
29    SkRect,
30    TextBaseline,
31    TextStyle,
32};
33use rustc_hash::FxHashMap;
34use torin::prelude::{
35    Area,
36    Length,
37    Point2D,
38    Position,
39    PostMeasure,
40    Size2D,
41};
42
43use crate::{
44    data::{
45        AccessibilityData,
46        CursorStyleData,
47        EffectData,
48        LayoutData,
49        StyleState,
50        TextStyleData,
51        TextStyleState,
52    },
53    diff_key::DiffKey,
54    element::{
55        Element,
56        ElementExt,
57        EventHandlerType,
58        IntoElement,
59        LayoutContext,
60        PostMeasureContext,
61        RenderContext,
62    },
63    elements::rect::rect,
64    events::name::EventName,
65    layers::Layer,
66    node_id::NodeId,
67    prelude::{
68        AccessibilityExt,
69        ChildrenExt,
70        Color,
71        ContainerExt,
72        ContainerPositionExt,
73        EventHandlersExt,
74        Fill,
75        KeyExt,
76        LayerExt,
77        LayoutExt,
78        MaybeExt,
79        TextAlign,
80        TextStyleExt,
81        VerticalAlign,
82    },
83    style::cursor::{
84        CursorMode,
85        CursorStyle,
86    },
87    text_cache::CachedParagraph,
88    tree::DiffModifies,
89};
90
91/// [paragraph()] makes it possible to render rich text with different styles. Its a more customizable API than [crate::elements::label].
92///
93/// See the available methods in [Paragraph].
94///
95/// ```rust
96/// # use freya::prelude::*;
97/// fn app() -> impl IntoElement {
98///     paragraph()
99///         .span(Span::new("Hello").font_size(24.0))
100///         .span(Span::new("World").font_size(16.0))
101/// }
102/// ```
103pub fn paragraph() -> Paragraph {
104    Paragraph {
105        key: DiffKey::None,
106        element: ParagraphElement::default(),
107        children: Vec::new(),
108    }
109}
110
111pub struct ParagraphHolderInner {
112    pub paragraph: Rc<SkParagraph>,
113    pub scale_factor: f64,
114}
115
116/// A shared slot that receives the laid-out paragraph, so callers can hit-test and measure
117/// text after layout. Pass it to a [`paragraph()`] with [`Paragraph::holder`].
118#[derive(Clone)]
119pub struct ParagraphHolder(pub Rc<RefCell<Option<ParagraphHolderInner>>>);
120
121impl PartialEq for ParagraphHolder {
122    fn eq(&self, other: &Self) -> bool {
123        Rc::ptr_eq(&self.0, &other.0)
124    }
125}
126
127impl Debug for ParagraphHolder {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        f.write_str("ParagraphHolder")
130    }
131}
132
133impl Default for ParagraphHolder {
134    fn default() -> Self {
135        Self(Rc::new(RefCell::new(None)))
136    }
137}
138
139/// Marks the order of a [Paragraph]'s content.
140#[derive(PartialEq, Clone)]
141pub enum ParagraphContent {
142    Span,
143    Element,
144}
145
146#[derive(PartialEq, Clone)]
147pub struct ParagraphElement {
148    pub layout: LayoutData,
149    pub spans: Vec<Span<'static>>,
150    pub contents: Vec<ParagraphContent>,
151    pub accessibility: AccessibilityData,
152    pub text_style_data: TextStyleData,
153    pub cursor_style_data: CursorStyleData,
154    pub event_handlers: FxHashMap<EventName, EventHandlerType>,
155    pub sk_paragraph: ParagraphHolder,
156    pub cursor_index: Option<usize>,
157    pub highlights: Vec<(usize, usize)>,
158    pub max_lines: Option<usize>,
159    pub line_height: Option<f32>,
160    pub relative_layer: Layer,
161    pub cursor_style: CursorStyle,
162    pub cursor_mode: CursorMode,
163    pub vertical_align: VerticalAlign,
164}
165
166impl Default for ParagraphElement {
167    fn default() -> Self {
168        let mut accessibility = AccessibilityData::default();
169        accessibility.builder.set_role(accesskit::Role::Paragraph);
170        Self {
171            layout: Default::default(),
172            spans: Default::default(),
173            contents: Default::default(),
174            accessibility,
175            text_style_data: Default::default(),
176            cursor_style_data: Default::default(),
177            event_handlers: Default::default(),
178            sk_paragraph: Default::default(),
179            cursor_index: Default::default(),
180            highlights: Default::default(),
181            max_lines: Default::default(),
182            line_height: Default::default(),
183            relative_layer: Default::default(),
184            cursor_style: CursorStyle::default(),
185            cursor_mode: CursorMode::default(),
186            vertical_align: VerticalAlign::default(),
187        }
188    }
189}
190
191impl Display for ParagraphElement {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.write_str(
194            &self
195                .spans
196                .iter()
197                .map(|s| s.text.clone())
198                .collect::<Vec<_>>()
199                .join("\n"),
200        )
201    }
202}
203
204impl ElementExt for ParagraphElement {
205    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
206        let Some(paragraph) = (other.as_ref() as &dyn Any).downcast_ref::<ParagraphElement>()
207        else {
208            return false;
209        };
210        self != paragraph
211    }
212
213    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
214        let Some(paragraph) = (other.as_ref() as &dyn Any).downcast_ref::<ParagraphElement>()
215        else {
216            return DiffModifies::all();
217        };
218
219        let mut diff = DiffModifies::empty();
220
221        if self.spans != paragraph.spans || self.contents != paragraph.contents {
222            diff.insert(DiffModifies::STYLE);
223            diff.insert(DiffModifies::LAYOUT);
224        }
225
226        if self.accessibility != paragraph.accessibility {
227            diff.insert(DiffModifies::ACCESSIBILITY);
228        }
229
230        if self.relative_layer != paragraph.relative_layer {
231            diff.insert(DiffModifies::LAYER);
232        }
233
234        if self.text_style_data != paragraph.text_style_data {
235            diff.insert(DiffModifies::STYLE);
236        }
237
238        if self.event_handlers != paragraph.event_handlers {
239            diff.insert(DiffModifies::EVENT_HANDLERS);
240        }
241
242        if self.cursor_index != paragraph.cursor_index
243            || self.highlights != paragraph.highlights
244            || self.cursor_mode != paragraph.cursor_mode
245            || self.cursor_style != paragraph.cursor_style
246            || self.cursor_style_data != paragraph.cursor_style_data
247            || self.vertical_align != paragraph.vertical_align
248        {
249            diff.insert(DiffModifies::STYLE);
250        }
251
252        if self.text_style_data != paragraph.text_style_data
253            || self.line_height != paragraph.line_height
254            || self.max_lines != paragraph.max_lines
255        {
256            diff.insert(DiffModifies::TEXT_STYLE);
257            diff.insert(DiffModifies::LAYOUT);
258        }
259
260        if self.layout != paragraph.layout {
261            diff.insert(DiffModifies::STYLE);
262            diff.insert(DiffModifies::LAYOUT);
263        }
264
265        diff
266    }
267
268    fn layout(&'_ self) -> Cow<'_, LayoutData> {
269        Cow::Borrowed(&self.layout)
270    }
271    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
272        None
273    }
274
275    fn style(&'_ self) -> Cow<'_, StyleState> {
276        Cow::Owned(StyleState::default())
277    }
278
279    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
280        Cow::Borrowed(&self.text_style_data)
281    }
282
283    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
284        Cow::Borrowed(&self.accessibility)
285    }
286
287    fn layer(&self) -> Layer {
288        self.relative_layer
289    }
290
291    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
292        let cached_paragraph = CachedParagraph {
293            text_style_state: context.text_style_state,
294            spans: &self.spans,
295            max_lines: self.max_lines,
296            line_height: self.line_height,
297            width: context.area_size.width,
298        };
299        let paragraph = context
300            .text_cache
301            .utilize(context.node_id, &cached_paragraph)
302            .unwrap_or_else(|| {
303                let width = if self.max_lines == Some(1)
304                    && context.text_style_state.text_align == TextAlign::default()
305                    && context
306                        .text_style_state
307                        .text_overflow
308                        .get_ellipsis()
309                        .is_none()
310                {
311                    f32::MAX
312                } else {
313                    context.area_size.width + 1.0
314                };
315
316                let paragraph = self.build_paragraph(
317                    context.text_style_state,
318                    context.fallback_fonts,
319                    context.scale_factor,
320                    context.font_collection,
321                    width,
322                    &[],
323                );
324                context
325                    .text_cache
326                    .insert(context.node_id, &cached_paragraph, paragraph)
327            });
328
329        let size = Size2D::new(paragraph.longest_line(), paragraph.height()).max(Size2D::zero());
330
331        self.sk_paragraph
332            .0
333            .borrow_mut()
334            .replace(ParagraphHolderInner {
335                paragraph,
336                scale_factor: context.scale_factor,
337            });
338
339        Some((size, Rc::new(())))
340    }
341
342    fn should_hook_measurement(&self) -> bool {
343        true
344    }
345
346    fn should_measure_inner_children(&self) -> bool {
347        self.has_inline_content()
348    }
349
350    fn needs_post_measure(&self) -> bool {
351        self.has_inline_content()
352    }
353
354    fn post_measure(&self, context: PostMeasureContext) -> PostMeasure<NodeId> {
355        if context.children.is_empty() {
356            return PostMeasure::default();
357        }
358
359        let placeholders: Vec<Size2D> = context
360            .children
361            .iter()
362            .map(|child| {
363                context
364                    .layout
365                    .get(child)
366                    .map(|node| node.area.size)
367                    .unwrap()
368            })
369            .collect();
370
371        let width = self
372            .sk_paragraph
373            .0
374            .borrow()
375            .as_ref()
376            .map(|holder| holder.paragraph.max_width())
377            .unwrap();
378
379        let paragraph = self.build_paragraph(
380            context.text_style_state,
381            context.fallback_fonts,
382            context.scale_factor,
383            context.font_collection,
384            width,
385            &placeholders,
386        );
387        let rects = paragraph.get_rects_for_placeholders();
388        let paragraph_height = paragraph.height();
389        // The size with the placeholders in place, so the node is sized around the inline children.
390        let content_size = Size2D::new(paragraph.longest_line(), paragraph_height);
391
392        self.sk_paragraph
393            .0
394            .borrow_mut()
395            .replace(ParagraphHolderInner {
396                paragraph: Rc::new(paragraph),
397                scale_factor: context.scale_factor,
398            });
399
400        let visible_area = context.node_layout.visible_area();
401        let vertical_offset = match self.vertical_align {
402            VerticalAlign::Start => 0.0,
403            VerticalAlign::Center => (visible_area.height() - paragraph_height).max(0.0) / 2.0,
404        };
405        let origin = visible_area.origin;
406
407        let mut offsets = Vec::new();
408        let mut hidden_children = Vec::new();
409        for (index, child_id) in context.children.iter().enumerate() {
410            let Some(current) = context.layout.get(child_id).map(|node| node.area.origin) else {
411                continue;
412            };
413            match rects.get(index) {
414                Some(rect) => {
415                    let offset_x = origin.x + rect.rect.left - current.x;
416                    let offset_y = origin.y + vertical_offset + rect.rect.top - current.y;
417                    offsets.push((*child_id, Length::new(offset_x), Length::new(offset_y)));
418                }
419                // Children whose placeholder got cut off by max_lines or ellipsis are hidden.
420                None => hidden_children.push(*child_id),
421            }
422        }
423
424        PostMeasure {
425            content_size: Some(content_size),
426            offsets,
427            hidden_children,
428        }
429    }
430
431    fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
432        Some(Cow::Borrowed(&self.event_handlers))
433    }
434
435    fn render(&self, context: RenderContext) {
436        let paragraph = self.sk_paragraph.0.borrow();
437        let ParagraphHolderInner { paragraph, .. } = paragraph.as_ref().unwrap();
438        let visible_area = context.layout_node.visible_area();
439
440        let cursor_area = match self.cursor_mode {
441            CursorMode::Fit => visible_area,
442            CursorMode::Expanded => context.layout_node.area,
443        };
444
445        let paragraph_height = paragraph.height();
446        let area_height = visible_area.height();
447        let vertical_offset = match self.vertical_align {
448            VerticalAlign::Start => 0.0,
449            VerticalAlign::Center => (area_height - paragraph_height).max(0.0) / 2.0,
450        };
451
452        let cursor_vertical_offset = match self.cursor_mode {
453            CursorMode::Fit => vertical_offset,
454            CursorMode::Expanded => 0.0,
455        };
456        let cursor_vertical_size_offset = match self.cursor_mode {
457            CursorMode::Fit => 0.,
458            CursorMode::Expanded => vertical_offset * 2.,
459        };
460
461        // Draw highlights
462        for (from, to) in self.highlights.iter() {
463            if from == to {
464                continue;
465            }
466            let (from, to) = { if from < to { (from, to) } else { (to, from) } };
467            let rects = paragraph.get_rects_for_range(
468                *from..*to,
469                RectHeightStyle::Tight,
470                RectWidthStyle::Tight,
471            );
472
473            let mut highlights_paint = Paint::default();
474            highlights_paint.set_anti_alias(true);
475            highlights_paint.set_style(PaintStyle::Fill);
476            highlights_paint.set_color(self.cursor_style_data.highlight_color);
477
478            if rects.is_empty() && *from == 0 {
479                let avg_line_height =
480                    paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
481                let rect = SkRect::new(
482                    cursor_area.min_x(),
483                    cursor_area.min_y() + cursor_vertical_offset,
484                    cursor_area.min_x() + 6.,
485                    cursor_area.min_y() + avg_line_height + cursor_vertical_size_offset,
486                );
487
488                context.canvas.draw_rect(rect, &highlights_paint);
489            }
490
491            for rect in rects {
492                let rect = SkRect::new(
493                    cursor_area.min_x() + rect.rect.left,
494                    cursor_area.min_y() + rect.rect.top + cursor_vertical_offset,
495                    cursor_area.min_x() + rect.rect.right.max(6.),
496                    cursor_area.min_y() + rect.rect.bottom + cursor_vertical_size_offset,
497                );
498                context.canvas.draw_rect(rect, &highlights_paint);
499            }
500        }
501
502        // We exclude those highlights that on the same start and end (e.g the user just started dragging)
503        let visible_highlights = self
504            .highlights
505            .iter()
506            .filter(|highlight| highlight.0 != highlight.1)
507            .count()
508            > 0;
509
510        // Draw block cursor behind text if needed
511        if let Some(cursor_index) = self.cursor_index
512            && self.cursor_style == CursorStyle::Block
513            && let Some(cursor_rect) = paragraph
514                .get_rects_for_range(
515                    cursor_index..cursor_index + 1,
516                    RectHeightStyle::Tight,
517                    RectWidthStyle::Tight,
518                )
519                .first()
520                .map(|text| text.rect)
521                .or_else(|| {
522                    // Show the cursor at the end of the text if possible
523                    let text_len = paragraph
524                        .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
525                        .position as usize;
526                    let last_rects = paragraph.get_rects_for_range(
527                        text_len.saturating_sub(1)..text_len,
528                        RectHeightStyle::Tight,
529                        RectWidthStyle::Tight,
530                    );
531
532                    if let Some(last_rect) = last_rects.first() {
533                        let mut caret = last_rect.rect;
534                        caret.left = caret.right;
535                        Some(caret)
536                    } else {
537                        let avg_line_height =
538                            paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
539                        Some(SkRect::new(0., 0., 6., avg_line_height))
540                    }
541                })
542        {
543            let width = (cursor_rect.right - cursor_rect.left).max(6.0);
544            let cursor_rect = SkRect::new(
545                cursor_area.min_x() + cursor_rect.left,
546                cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
547                cursor_area.min_x() + cursor_rect.left + width,
548                cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
549            );
550
551            let mut paint = Paint::default();
552            paint.set_anti_alias(true);
553            paint.set_style(PaintStyle::Fill);
554            paint.set_color(self.cursor_style_data.color);
555
556            context.canvas.draw_rect(cursor_rect, &paint);
557        }
558
559        // Draw text (always uses visible_area with vertical_offset)
560        paint_paragraph_with_fill(
561            paragraph,
562            context.canvas,
563            Point2D::new(visible_area.min_x(), visible_area.min_y() + vertical_offset),
564            &context.text_style_state.color,
565        );
566
567        // Draw cursor
568        if let Some(cursor_index) = self.cursor_index
569            && !visible_highlights
570        {
571            let cursor_rects = paragraph.get_rects_for_range(
572                cursor_index..cursor_index + 1,
573                RectHeightStyle::Tight,
574                RectWidthStyle::Tight,
575            );
576            if let Some(cursor_rect) = cursor_rects.first().map(|text| text.rect).or_else(|| {
577                // Show the cursor at the end of the text if possible
578                let text_len = paragraph
579                    .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
580                    .position as usize;
581                let last_rects = paragraph.get_rects_for_range(
582                    text_len.saturating_sub(1)..text_len,
583                    RectHeightStyle::Tight,
584                    RectWidthStyle::Tight,
585                );
586
587                if let Some(last_rect) = last_rects.first() {
588                    let mut caret = last_rect.rect;
589                    caret.left = caret.right;
590                    Some(caret)
591                } else {
592                    None
593                }
594            }) {
595                let paint_color = self.cursor_style_data.color;
596                match self.cursor_style {
597                    CursorStyle::Underline => {
598                        let thickness = 2.0;
599                        let underline_rect = SkRect::new(
600                            cursor_area.min_x() + cursor_rect.left,
601                            cursor_area.min_y() + cursor_rect.bottom - thickness
602                                + cursor_vertical_offset,
603                            cursor_area.min_x() + cursor_rect.right,
604                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
605                        );
606
607                        let mut paint = Paint::default();
608                        paint.set_anti_alias(true);
609                        paint.set_style(PaintStyle::Fill);
610                        paint.set_color(paint_color);
611
612                        context.canvas.draw_rect(underline_rect, &paint);
613                    }
614                    CursorStyle::Line => {
615                        let cursor_rect = SkRect::new(
616                            cursor_area.min_x() + cursor_rect.left,
617                            cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
618                            cursor_area.min_x() + cursor_rect.left + 2.,
619                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
620                        );
621
622                        let mut paint = Paint::default();
623                        paint.set_anti_alias(true);
624                        paint.set_style(PaintStyle::Fill);
625                        paint.set_color(paint_color);
626
627                        context.canvas.draw_rect(cursor_rect, &paint);
628                    }
629                    _ => {}
630                }
631            }
632        }
633    }
634}
635
636impl ParagraphElement {
637    fn has_inline_content(&self) -> bool {
638        self.contents
639            .iter()
640            .any(|content| matches!(content, ParagraphContent::Element))
641    }
642
643    /// Builds the Skia paragraph from the content, reserving a placeholder (sized from
644    /// `placeholders`, in order) for each inline child, laid out against `width`.
645    fn build_paragraph(
646        &self,
647        text_style_state: &TextStyleState,
648        fallback_fonts: &[Cow<'static, str>],
649        scale_factor: f64,
650        font_collection: &FontCollection,
651        width: f32,
652        placeholders: &[Size2D],
653    ) -> SkParagraph {
654        let mut paragraph_style = ParagraphStyle::default();
655
656        if let Some(ellipsis) = text_style_state.text_overflow.get_ellipsis() {
657            paragraph_style.set_ellipsis(ellipsis);
658        }
659
660        paragraph_style.set_text_style(&base_text_style(
661            text_style_state,
662            fallback_fonts,
663            scale_factor,
664            self.line_height,
665        ));
666        paragraph_style.set_max_lines(self.max_lines);
667        paragraph_style.set_text_align(text_style_state.text_align.into());
668
669        let mut paragraph_builder = ParagraphBuilder::new(&paragraph_style, font_collection);
670
671        let mut spans = self.spans.iter();
672        let mut placeholders = placeholders.iter();
673        for content in &self.contents {
674            match content {
675                ParagraphContent::Span => {
676                    let Some(span) = spans.next() else { continue };
677                    paragraph_builder.push_style(&span_text_style(
678                        text_style_state,
679                        fallback_fonts,
680                        scale_factor,
681                        span,
682                        self.line_height,
683                    ));
684                    paragraph_builder.add_text(&span.text);
685                }
686                ParagraphContent::Element => {
687                    let Some(size) = placeholders.next() else {
688                        continue;
689                    };
690                    paragraph_builder.add_placeholder(&PlaceholderStyle::new(
691                        size.width,
692                        size.height,
693                        PlaceholderAlignment::Middle,
694                        TextBaseline::Alphabetic,
695                        0.0,
696                    ));
697                }
698            }
699        }
700
701        let mut paragraph = paragraph_builder.build();
702        paragraph.layout(width);
703        paragraph
704    }
705}
706
707impl From<Paragraph> for Element {
708    fn from(value: Paragraph) -> Self {
709        let elements = value
710            .children
711            .into_iter()
712            .map(|child| {
713                rect()
714                    .position(Position::new_absolute())
715                    .child(child)
716                    .into_element()
717            })
718            .collect();
719
720        Element::Element {
721            key: value.key,
722            element: Rc::new(value.element),
723            elements,
724        }
725    }
726}
727
728/// Builds the paragraph-level base [TextStyle] from the inherited text style state.
729fn base_text_style(
730    text_style_state: &TextStyleState,
731    fallback_fonts: &[Cow<'static, str>],
732    scale_factor: f64,
733    line_height: Option<f32>,
734) -> TextStyle {
735    let mut text_style = TextStyle::default();
736
737    let mut font_families = text_style_state.font_families.clone();
738    font_families.extend_from_slice(fallback_fonts);
739
740    text_style.set_color(text_style_state.color.as_color().unwrap_or(Color::WHITE));
741    text_style.set_font_size(f32::from(text_style_state.font_size) * scale_factor as f32);
742    text_style.set_font_families(&font_families);
743    text_style.set_font_style(FontStyle::new(
744        text_style_state.font_weight.into(),
745        text_style_state.font_width.into(),
746        text_style_state.font_slant.into(),
747    ));
748
749    if text_style_state.text_height.needs_custom_height() {
750        text_style.set_height_override(true);
751        text_style.set_half_leading(true);
752    }
753
754    if let Some(line_height) = line_height {
755        text_style.set_height_override(true);
756        text_style.set_height(line_height);
757    }
758
759    for text_shadow in text_style_state.text_shadows.iter() {
760        text_style.add_shadow((*text_shadow).into());
761    }
762
763    text_style
764}
765
766/// Builds the [TextStyle] for a single [Span], layering its overrides over the inherited state.
767fn span_text_style(
768    text_style_state: &TextStyleState,
769    fallback_fonts: &[Cow<'static, str>],
770    scale_factor: f64,
771    span: &Span,
772    line_height: Option<f32>,
773) -> TextStyle {
774    let span_style = TextStyleState::from_data(text_style_state, &span.text_style_data);
775    let mut text_style = TextStyle::new();
776    let mut font_families = text_style_state.font_families.clone();
777    font_families.extend_from_slice(fallback_fonts);
778
779    for text_shadow in span_style.text_shadows.iter() {
780        text_style.add_shadow((*text_shadow).into());
781    }
782
783    text_style.set_color(span_style.color.as_color().unwrap_or(Color::WHITE));
784    text_style.set_font_size(f32::from(span_style.font_size) * scale_factor as f32);
785    text_style.set_font_families(&font_families);
786    text_style.set_font_style(FontStyle::new(
787        span_style.font_weight.into(),
788        span_style.font_width.into(),
789        span_style.font_slant.into(),
790    ));
791    text_style.set_decoration_type(span_style.text_decoration.into());
792    if let Some(line_height) = line_height {
793        text_style.set_height_override(true);
794        text_style.set_height(line_height);
795    }
796    text_style
797}
798
799/// Paints a paragraph with a [Fill] as the text color. Non-color fills are masked
800/// onto the rendered glyph alpha via an offscreen layer + [BlendMode::SrcIn].
801pub(crate) fn paint_paragraph_with_fill(
802    paragraph: &SkParagraph,
803    canvas: &Canvas,
804    origin: Point2D,
805    fill: &Fill,
806) {
807    if matches!(fill, Fill::Color(_)) {
808        paragraph.paint(canvas, origin.to_tuple());
809        return;
810    }
811
812    let width = paragraph.longest_line();
813    let height = paragraph.height();
814    let area = Area::new(origin, Size2D::new(width, height));
815    let bounds_rect = SkRect::from_xywh(area.min_x(), area.min_y(), width, height);
816
817    let layer = canvas.save_layer(&SaveLayerRec::default().bounds(&bounds_rect));
818
819    paragraph.paint(canvas, origin.to_tuple());
820
821    let mut paint = Paint::default();
822    paint.set_anti_alias(true);
823    paint.set_style(PaintStyle::Fill);
824    paint.set_blend_mode(BlendMode::SrcIn);
825    fill.apply_to_paint(&mut paint, area);
826
827    canvas.draw_rect(bounds_rect, &paint);
828
829    canvas.restore_to_count(layer);
830}
831
832impl KeyExt for Paragraph {
833    fn write_key(&mut self) -> &mut DiffKey {
834        &mut self.key
835    }
836}
837
838impl EventHandlersExt for Paragraph {
839    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
840        &mut self.element.event_handlers
841    }
842}
843
844impl MaybeExt for Paragraph {}
845
846impl LayerExt for Paragraph {
847    fn get_layer(&mut self) -> &mut Layer {
848        &mut self.element.relative_layer
849    }
850}
851
852pub struct Paragraph {
853    key: DiffKey,
854    element: ParagraphElement,
855    children: Vec<Element>,
856}
857
858impl LayoutExt for Paragraph {
859    fn get_layout(&mut self) -> &mut LayoutData {
860        &mut self.element.layout
861    }
862}
863
864impl ContainerExt for Paragraph {}
865
866/// Children added to a [Paragraph] flow inline at the point they were added, each laid out at
867/// its own measured size, so give them an explicit `width` and `height`.
868impl ChildrenExt for Paragraph {
869    fn get_children(&mut self) -> &mut Vec<Element> {
870        &mut self.children
871    }
872
873    fn child<C: IntoElement>(mut self, child: C) -> Self {
874        self.element.contents.push(ParagraphContent::Element);
875        self.children.push(child.into_element());
876        self
877    }
878
879    fn children(self, children: impl IntoIterator<Item = Element>) -> Self {
880        children
881            .into_iter()
882            .fold(self, |paragraph, child| paragraph.child(child))
883    }
884
885    fn maybe_child<C: IntoElement>(self, child: Option<C>) -> Self {
886        match child {
887            Some(child) => self.child(child),
888            None => self,
889        }
890    }
891}
892
893impl AccessibilityExt for Paragraph {
894    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
895        &mut self.element.accessibility
896    }
897}
898
899impl TextStyleExt for Paragraph {
900    fn get_text_style_data(&mut self) -> &mut TextStyleData {
901        &mut self.element.text_style_data
902    }
903}
904
905impl Paragraph {
906    pub fn try_downcast(element: &dyn ElementExt) -> Option<ParagraphElement> {
907        (element as &dyn Any)
908            .downcast_ref::<ParagraphElement>()
909            .cloned()
910    }
911
912    /// Append every [`Span`] yielded by the iterator to the paragraph.
913    pub fn spans_iter(mut self, spans: impl Iterator<Item = Span<'static>>) -> Self {
914        // TODO: Accessible paragraphs
915        // self.element.accessibility.builder.set_value(text.clone());
916        for span in spans {
917            self.push_span(span);
918        }
919        self
920    }
921
922    /// Append a single [`Span`] of styled text to the paragraph.
923    pub fn span(mut self, span: impl Into<Span<'static>>) -> Self {
924        // TODO: Accessible paragraphs
925        // self.element.accessibility.builder.set_value(text.clone());
926        self.push_span(span.into());
927        self
928    }
929
930    fn push_span(&mut self, span: Span<'static>) {
931        self.element.contents.push(ParagraphContent::Span);
932        self.element.spans.push(span);
933    }
934
935    /// Set the color of the text cursor. See [`Color`].
936    pub fn cursor_color(mut self, cursor_color: impl Into<Color>) -> Self {
937        self.element.cursor_style_data.color = cursor_color.into();
938        self
939    }
940
941    /// Set the color used to highlight selected text. See [`Color`].
942    pub fn highlight_color(mut self, highlight_color: impl Into<Color>) -> Self {
943        self.element.cursor_style_data.highlight_color = highlight_color.into();
944        self
945    }
946
947    /// Set the shape of the text cursor. See [`CursorStyle`].
948    pub fn cursor_style(mut self, cursor_style: impl Into<CursorStyle>) -> Self {
949        self.element.cursor_style = cursor_style.into();
950        self
951    }
952
953    /// Attach a [`ParagraphHolder`] that receives the laid-out paragraph for hit-testing and measurement.
954    pub fn holder(mut self, holder: ParagraphHolder) -> Self {
955        self.element.sk_paragraph = holder;
956        self
957    }
958
959    /// Place the text cursor at the given character index. Pass `None` to hide it.
960    pub fn cursor_index(mut self, cursor_index: impl Into<Option<usize>>) -> Self {
961        self.element.cursor_index = cursor_index.into();
962        self
963    }
964
965    /// Highlight the given `(start, end)` character ranges, used for text selection.
966    pub fn highlights(mut self, highlights: impl Into<Option<Vec<(usize, usize)>>>) -> Self {
967        if let Some(highlights) = highlights.into() {
968            self.element.highlights = highlights;
969        }
970        self
971    }
972
973    /// Limit the paragraph to at most this many lines, truncating the rest. Pass `None` for no limit.
974    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
975        self.element.max_lines = max_lines.into();
976        self
977    }
978
979    /// Override the height of each line as a multiple of the font size. Pass `None` for the default.
980    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
981        self.element.line_height = line_height.into();
982        self
983    }
984
985    /// Set the cursor mode for the paragraph.
986    /// - `CursorMode::Fit`: cursor/highlights use the paragraph's visible_area. VerticalAlign affects cursor positions.
987    /// - `CursorMode::Expanded`: cursor/highlights use the paragraph's inner_area. VerticalAlign does NOT affect cursor positions.
988    pub fn cursor_mode(mut self, cursor_mode: impl Into<CursorMode>) -> Self {
989        self.element.cursor_mode = cursor_mode.into();
990        self
991    }
992
993    /// Set the vertical alignment for the paragraph text.
994    /// This affects how the text is rendered within the paragraph area, but cursor/highlight behavior
995    /// depends on the `cursor_mode` setting.
996    pub fn vertical_align(mut self, vertical_align: impl Into<VerticalAlign>) -> Self {
997        self.element.vertical_align = vertical_align.into();
998        self
999    }
1000}
1001
1002/// A run of text with its own style, used to build a [`paragraph()`].
1003///
1004/// Create it with [`Span::new`] (or from a `&str`/`String`) and style it with the
1005/// [`TextStyleExt`] methods such as [`font_size`](TextStyleExt::font_size) and
1006/// [`color`](TextStyleExt::color):
1007///
1008/// ```
1009/// # use freya_core::prelude::*;
1010/// let span = Span::new("Hello").font_size(24.0).color(Color::RED);
1011/// ```
1012#[derive(Clone, PartialEq, Hash)]
1013pub struct Span<'a> {
1014    pub text_style_data: TextStyleData,
1015    pub text: Cow<'a, str>,
1016}
1017
1018impl From<&'static str> for Span<'static> {
1019    fn from(text: &'static str) -> Self {
1020        Span {
1021            text_style_data: TextStyleData::default(),
1022            text: text.into(),
1023        }
1024    }
1025}
1026
1027impl From<String> for Span<'static> {
1028    fn from(text: String) -> Self {
1029        Span {
1030            text_style_data: TextStyleData::default(),
1031            text: text.into(),
1032        }
1033    }
1034}
1035
1036impl<'a> Span<'a> {
1037    /// Create a [`Span`] from the given text, with the default text style.
1038    pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
1039        Self {
1040            text: text.into(),
1041            text_style_data: TextStyleData::default(),
1042        }
1043    }
1044}
1045
1046impl<'a> TextStyleExt for Span<'a> {
1047    fn get_text_style_data(&mut self) -> &mut TextStyleData {
1048        &mut self.text_style_data
1049    }
1050}