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