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        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 {
104        key: DiffKey::None,
105        element: ParagraphElement::default(),
106        children: Vec::new(),
107    }
108}
109
110pub struct ParagraphHolderInner {
111    pub paragraph: Rc<SkParagraph>,
112    pub scale_factor: f64,
113}
114
115/// A shared slot that receives the laid-out paragraph, so callers can hit-test and measure
116/// text after layout. Pass it to a [`paragraph()`] with [`Paragraph::holder`].
117#[derive(Clone)]
118pub struct ParagraphHolder(pub Rc<RefCell<Option<ParagraphHolderInner>>>);
119
120impl PartialEq for ParagraphHolder {
121    fn eq(&self, other: &Self) -> bool {
122        Rc::ptr_eq(&self.0, &other.0)
123    }
124}
125
126impl Debug for ParagraphHolder {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        f.write_str("ParagraphHolder")
129    }
130}
131
132impl Default for ParagraphHolder {
133    fn default() -> Self {
134        Self(Rc::new(RefCell::new(None)))
135    }
136}
137
138/// Marks the order of a [Paragraph]'s content.
139#[derive(PartialEq, Clone)]
140pub enum ParagraphContent {
141    Span,
142    Element,
143}
144
145#[derive(PartialEq, Clone)]
146pub struct ParagraphElement {
147    pub layout: LayoutData,
148    pub spans: Vec<Span<'static>>,
149    pub contents: Vec<ParagraphContent>,
150    pub accessibility: AccessibilityData,
151    pub text_style_data: TextStyleData,
152    pub cursor_style_data: CursorStyleData,
153    pub event_handlers: FxHashMap<EventName, EventHandlerType>,
154    pub sk_paragraph: ParagraphHolder,
155    pub cursor_index: Option<usize>,
156    pub highlights: Vec<(usize, usize)>,
157    pub max_lines: Option<usize>,
158    pub line_height: Option<f32>,
159    pub relative_layer: Layer,
160    pub cursor_style: CursorStyle,
161    pub cursor_mode: CursorMode,
162    pub vertical_align: VerticalAlign,
163}
164
165impl Default for ParagraphElement {
166    fn default() -> Self {
167        let mut accessibility = AccessibilityData::default();
168        accessibility.builder.set_role(accesskit::Role::Paragraph);
169        Self {
170            layout: Default::default(),
171            spans: Default::default(),
172            contents: Default::default(),
173            accessibility,
174            text_style_data: Default::default(),
175            cursor_style_data: Default::default(),
176            event_handlers: Default::default(),
177            sk_paragraph: Default::default(),
178            cursor_index: Default::default(),
179            highlights: Default::default(),
180            max_lines: Default::default(),
181            line_height: Default::default(),
182            relative_layer: Default::default(),
183            cursor_style: CursorStyle::default(),
184            cursor_mode: CursorMode::default(),
185            vertical_align: VerticalAlign::default(),
186        }
187    }
188}
189
190impl Display for ParagraphElement {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        f.write_str(
193            &self
194                .spans
195                .iter()
196                .map(|s| s.text.clone())
197                .collect::<Vec<_>>()
198                .join("\n"),
199        )
200    }
201}
202
203impl ElementExt for ParagraphElement {
204    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
205        let Some(paragraph) = (other.as_ref() as &dyn Any).downcast_ref::<ParagraphElement>()
206        else {
207            return false;
208        };
209        self != paragraph
210    }
211
212    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
213        let Some(paragraph) = (other.as_ref() as &dyn Any).downcast_ref::<ParagraphElement>()
214        else {
215            return DiffModifies::all();
216        };
217
218        let mut diff = DiffModifies::empty();
219
220        if self.spans != paragraph.spans || self.contents != paragraph.contents {
221            diff.insert(DiffModifies::STYLE);
222            diff.insert(DiffModifies::LAYOUT);
223        }
224
225        if self.accessibility != paragraph.accessibility {
226            diff.insert(DiffModifies::ACCESSIBILITY);
227        }
228
229        if self.relative_layer != paragraph.relative_layer {
230            diff.insert(DiffModifies::LAYER);
231        }
232
233        if self.text_style_data != paragraph.text_style_data {
234            diff.insert(DiffModifies::STYLE);
235        }
236
237        if self.event_handlers != paragraph.event_handlers {
238            diff.insert(DiffModifies::EVENT_HANDLERS);
239        }
240
241        if self.cursor_index != paragraph.cursor_index
242            || self.highlights != paragraph.highlights
243            || self.cursor_mode != paragraph.cursor_mode
244            || self.cursor_style != paragraph.cursor_style
245            || self.cursor_style_data != paragraph.cursor_style_data
246            || self.vertical_align != paragraph.vertical_align
247        {
248            diff.insert(DiffModifies::STYLE);
249        }
250
251        if self.text_style_data != paragraph.text_style_data
252            || self.line_height != paragraph.line_height
253            || self.max_lines != paragraph.max_lines
254        {
255            diff.insert(DiffModifies::TEXT_STYLE);
256            diff.insert(DiffModifies::LAYOUT);
257        }
258
259        if self.layout != paragraph.layout {
260            diff.insert(DiffModifies::STYLE);
261            diff.insert(DiffModifies::LAYOUT);
262        }
263
264        diff
265    }
266
267    fn layout(&'_ self) -> Cow<'_, LayoutData> {
268        Cow::Borrowed(&self.layout)
269    }
270    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
271        None
272    }
273
274    fn style(&'_ self) -> Cow<'_, StyleState> {
275        Cow::Owned(StyleState::default())
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());
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<'_, FxHashMap<EventName, EventHandlerType>>> {
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        // Draw highlights
461        for (from, to) in self.highlights.iter() {
462            if from == to {
463                continue;
464            }
465            let (from, to) = { if from < to { (from, to) } else { (to, from) } };
466            let rects = paragraph.get_rects_for_range(
467                *from..*to,
468                RectHeightStyle::Tight,
469                RectWidthStyle::Tight,
470            );
471
472            let mut highlights_paint = Paint::default();
473            highlights_paint.set_anti_alias(true);
474            highlights_paint.set_style(PaintStyle::Fill);
475            highlights_paint.set_color(self.cursor_style_data.highlight_color);
476
477            if rects.is_empty() && *from == 0 {
478                let avg_line_height =
479                    paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
480                let rect = SkRect::new(
481                    cursor_area.min_x(),
482                    cursor_area.min_y() + cursor_vertical_offset,
483                    cursor_area.min_x() + 6.,
484                    cursor_area.min_y() + avg_line_height + cursor_vertical_size_offset,
485                );
486
487                context.canvas.draw_rect(rect, &highlights_paint);
488            }
489
490            for rect in rects {
491                let rect = SkRect::new(
492                    cursor_area.min_x() + rect.rect.left,
493                    cursor_area.min_y() + rect.rect.top + cursor_vertical_offset,
494                    cursor_area.min_x() + rect.rect.right.max(6.),
495                    cursor_area.min_y() + rect.rect.bottom + cursor_vertical_size_offset,
496                );
497                context.canvas.draw_rect(rect, &highlights_paint);
498            }
499        }
500
501        // We exclude those highlights that on the same start and end (e.g the user just started dragging)
502        let visible_highlights = self
503            .highlights
504            .iter()
505            .filter(|highlight| highlight.0 != highlight.1)
506            .count()
507            > 0;
508
509        // Draw block cursor behind text if needed
510        if let Some(cursor_index) = self.cursor_index
511            && self.cursor_style == CursorStyle::Block
512            && let Some(cursor_rect) = paragraph
513                .get_rects_for_range(
514                    cursor_index..cursor_index + 1,
515                    RectHeightStyle::Tight,
516                    RectWidthStyle::Tight,
517                )
518                .first()
519                .map(|text| text.rect)
520                .or_else(|| {
521                    // Show the cursor at the end of the text if possible
522                    let text_len = paragraph
523                        .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
524                        .position as usize;
525                    let last_rects = paragraph.get_rects_for_range(
526                        text_len.saturating_sub(1)..text_len,
527                        RectHeightStyle::Tight,
528                        RectWidthStyle::Tight,
529                    );
530
531                    if let Some(last_rect) = last_rects.first() {
532                        let mut caret = last_rect.rect;
533                        caret.left = caret.right;
534                        Some(caret)
535                    } else {
536                        let avg_line_height =
537                            paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
538                        Some(SkRect::new(0., 0., 6., avg_line_height))
539                    }
540                })
541        {
542            let width = (cursor_rect.right - cursor_rect.left).max(6.0);
543            let cursor_rect = SkRect::new(
544                cursor_area.min_x() + cursor_rect.left,
545                cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
546                cursor_area.min_x() + cursor_rect.left + width,
547                cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
548            );
549
550            let mut paint = Paint::default();
551            paint.set_anti_alias(true);
552            paint.set_style(PaintStyle::Fill);
553            paint.set_color(self.cursor_style_data.color);
554
555            context.canvas.draw_rect(cursor_rect, &paint);
556        }
557
558        // Draw text (always uses visible_area with vertical_offset)
559        paint_paragraph_with_fill(
560            paragraph,
561            context.canvas,
562            Point2D::new(visible_area.min_x(), visible_area.min_y() + vertical_offset),
563            &context.text_style_state.color,
564        );
565
566        // Draw cursor
567        if let Some(cursor_index) = self.cursor_index
568            && !visible_highlights
569        {
570            let cursor_rects = paragraph.get_rects_for_range(
571                cursor_index..cursor_index + 1,
572                RectHeightStyle::Tight,
573                RectWidthStyle::Tight,
574            );
575            if let Some(cursor_rect) = cursor_rects.first().map(|text| text.rect).or_else(|| {
576                // Show the cursor at the end of the text if possible
577                let text_len = paragraph
578                    .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
579                    .position as usize;
580                let last_rects = paragraph.get_rects_for_range(
581                    text_len.saturating_sub(1)..text_len,
582                    RectHeightStyle::Tight,
583                    RectWidthStyle::Tight,
584                );
585
586                if let Some(last_rect) = last_rects.first() {
587                    let mut caret = last_rect.rect;
588                    caret.left = caret.right;
589                    Some(caret)
590                } else {
591                    None
592                }
593            }) {
594                let paint_color = self.cursor_style_data.color;
595                match self.cursor_style {
596                    CursorStyle::Underline => {
597                        let thickness = 2.0;
598                        let underline_rect = SkRect::new(
599                            cursor_area.min_x() + cursor_rect.left,
600                            cursor_area.min_y() + cursor_rect.bottom - thickness
601                                + cursor_vertical_offset,
602                            cursor_area.min_x() + cursor_rect.right,
603                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
604                        );
605
606                        let mut paint = Paint::default();
607                        paint.set_anti_alias(true);
608                        paint.set_style(PaintStyle::Fill);
609                        paint.set_color(paint_color);
610
611                        context.canvas.draw_rect(underline_rect, &paint);
612                    }
613                    CursorStyle::Line => {
614                        let cursor_rect = SkRect::new(
615                            cursor_area.min_x() + cursor_rect.left,
616                            cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
617                            cursor_area.min_x() + cursor_rect.left + 2.,
618                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
619                        );
620
621                        let mut paint = Paint::default();
622                        paint.set_anti_alias(true);
623                        paint.set_style(PaintStyle::Fill);
624                        paint.set_color(paint_color);
625
626                        context.canvas.draw_rect(cursor_rect, &paint);
627                    }
628                    _ => {}
629                }
630            }
631        }
632    }
633}
634
635impl ParagraphElement {
636    fn has_inline_content(&self) -> bool {
637        self.contents
638            .iter()
639            .any(|content| matches!(content, ParagraphContent::Element))
640    }
641
642    /// Builds the Skia paragraph from the content, reserving a placeholder (sized from
643    /// `placeholders`, in order) for each inline child, laid out against `width`.
644    fn build_paragraph(
645        &self,
646        text_style_state: &TextStyleState,
647        fallback_fonts: &[Cow<'static, str>],
648        scale_factor: f64,
649        font_collection: &FontCollection,
650        width: f32,
651        placeholders: &[Size2D],
652    ) -> SkParagraph {
653        let mut paragraph_style = ParagraphStyle::default();
654
655        if let Some(ellipsis) = text_style_state.text_overflow.get_ellipsis() {
656            paragraph_style.set_ellipsis(ellipsis);
657        }
658
659        paragraph_style.set_text_style(&base_text_style(
660            text_style_state,
661            fallback_fonts,
662            scale_factor,
663            self.line_height,
664        ));
665        paragraph_style.set_max_lines(self.max_lines);
666        paragraph_style.set_text_align(text_style_state.text_align.into());
667
668        let mut paragraph_builder = ParagraphBuilder::new(&paragraph_style, font_collection);
669
670        let mut spans = self.spans.iter();
671        let mut placeholders = placeholders.iter();
672        for content in &self.contents {
673            match content {
674                ParagraphContent::Span => {
675                    let Some(span) = spans.next() else { continue };
676                    paragraph_builder.push_style(&span_text_style(
677                        text_style_state,
678                        fallback_fonts,
679                        scale_factor,
680                        span,
681                        self.line_height,
682                    ));
683                    paragraph_builder.add_text(&span.text);
684                }
685                ParagraphContent::Element => {
686                    let Some(size) = placeholders.next() else {
687                        continue;
688                    };
689                    paragraph_builder.add_placeholder(&PlaceholderStyle::new(
690                        size.width,
691                        size.height,
692                        PlaceholderAlignment::Middle,
693                        TextBaseline::Alphabetic,
694                        0.0,
695                    ));
696                }
697            }
698        }
699
700        let mut paragraph = paragraph_builder.build();
701        paragraph.layout(width);
702        paragraph
703    }
704}
705
706impl From<Paragraph> for Element {
707    fn from(value: Paragraph) -> Self {
708        let elements = value
709            .children
710            .into_iter()
711            .map(|child| {
712                rect()
713                    .position(Position::new_absolute())
714                    .child(child)
715                    .into_element()
716            })
717            .collect();
718
719        Element::Element {
720            key: value.key,
721            element: Rc::new(value.element),
722            elements,
723        }
724    }
725}
726
727/// Builds the paragraph-level base [TextStyle] from the inherited text style state.
728fn base_text_style(
729    text_style_state: &TextStyleState,
730    fallback_fonts: &[Cow<'static, str>],
731    scale_factor: f64,
732    line_height: Option<f32>,
733) -> TextStyle {
734    let mut text_style = TextStyle::default();
735
736    let mut font_families = text_style_state.font_families.clone();
737    font_families.extend_from_slice(fallback_fonts);
738
739    text_style.set_color(text_style_state.color.as_color().unwrap_or(Color::WHITE));
740    text_style.set_font_size(f32::from(text_style_state.font_size) * scale_factor as f32);
741    text_style.set_font_families(&font_families);
742    text_style.set_font_style(FontStyle::new(
743        text_style_state.font_weight.into(),
744        text_style_state.font_width.into(),
745        text_style_state.font_slant.into(),
746    ));
747
748    if text_style_state.text_height.needs_custom_height() {
749        text_style.set_height_override(true);
750        text_style.set_half_leading(true);
751    }
752
753    if let Some(line_height) = line_height {
754        text_style.set_height_override(true);
755        text_style.set_height(line_height);
756    }
757
758    for text_shadow in text_style_state.text_shadows.iter() {
759        text_style.add_shadow((*text_shadow).into());
760    }
761
762    text_style
763}
764
765/// Builds the [TextStyle] for a single [Span], layering its overrides over the inherited state.
766fn span_text_style(
767    text_style_state: &TextStyleState,
768    fallback_fonts: &[Cow<'static, str>],
769    scale_factor: f64,
770    span: &Span,
771    line_height: Option<f32>,
772) -> TextStyle {
773    let span_style = TextStyleState::from_data(text_style_state, &span.text_style_data);
774    let mut text_style = TextStyle::new();
775    let mut font_families = text_style_state.font_families.clone();
776    font_families.extend_from_slice(fallback_fonts);
777
778    for text_shadow in span_style.text_shadows.iter() {
779        text_style.add_shadow((*text_shadow).into());
780    }
781
782    text_style.set_color(span_style.color.as_color().unwrap_or(Color::WHITE));
783    text_style.set_font_size(f32::from(span_style.font_size) * scale_factor as f32);
784    text_style.set_font_families(&font_families);
785    text_style.set_font_style(FontStyle::new(
786        span_style.font_weight.into(),
787        span_style.font_width.into(),
788        span_style.font_slant.into(),
789    ));
790    text_style.set_decoration_type(span_style.text_decoration.into());
791    if let Some(line_height) = line_height {
792        text_style.set_height_override(true);
793        text_style.set_height(line_height);
794    }
795    text_style
796}
797
798/// Paints a paragraph with a [Fill] as the text color. Non-color fills are masked
799/// onto the rendered glyph alpha via an offscreen layer + [BlendMode::SrcIn].
800pub(crate) fn paint_paragraph_with_fill(
801    paragraph: &SkParagraph,
802    canvas: &Canvas,
803    origin: Point2D,
804    fill: &Fill,
805) {
806    if matches!(fill, Fill::Color(_)) {
807        paragraph.paint(canvas, origin.to_tuple());
808        return;
809    }
810
811    let width = paragraph.longest_line();
812    let height = paragraph.height();
813    let area = Area::new(origin, Size2D::new(width, height));
814    let bounds_rect = SkRect::from_xywh(area.min_x(), area.min_y(), width, height);
815
816    let layer = canvas.save_layer(&SaveLayerRec::default().bounds(&bounds_rect));
817
818    paragraph.paint(canvas, origin.to_tuple());
819
820    let mut paint = Paint::default();
821    paint.set_anti_alias(true);
822    paint.set_style(PaintStyle::Fill);
823    paint.set_blend_mode(BlendMode::SrcIn);
824    fill.apply_to_paint(&mut paint, area);
825
826    canvas.draw_rect(bounds_rect, &paint);
827
828    canvas.restore_to_count(layer);
829}
830
831impl KeyExt for Paragraph {
832    fn write_key(&mut self) -> &mut DiffKey {
833        &mut self.key
834    }
835}
836
837impl EventHandlersExt for Paragraph {
838    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
839        &mut self.element.event_handlers
840    }
841}
842
843impl MaybeExt for Paragraph {}
844
845impl LayerExt for Paragraph {
846    fn get_layer(&mut self) -> &mut Layer {
847        &mut self.element.relative_layer
848    }
849}
850
851pub struct Paragraph {
852    key: DiffKey,
853    element: ParagraphElement,
854    children: Vec<Element>,
855}
856
857impl LayoutExt for Paragraph {
858    fn get_layout(&mut self) -> &mut LayoutData {
859        &mut self.element.layout
860    }
861}
862
863impl ContainerExt for Paragraph {}
864
865/// Children added to a [Paragraph] flow inline at the point they were added, each laid out at
866/// its own measured size, so give them an explicit `width` and `height`.
867impl ChildrenExt for Paragraph {
868    fn get_children(&mut self) -> &mut Vec<Element> {
869        &mut self.children
870    }
871
872    fn child<C: IntoElement>(mut self, child: C) -> Self {
873        self.element.contents.push(ParagraphContent::Element);
874        self.children.push(child.into_element());
875        self
876    }
877
878    fn children(self, children: impl IntoIterator<Item = Element>) -> Self {
879        children
880            .into_iter()
881            .fold(self, |paragraph, child| paragraph.child(child))
882    }
883
884    fn maybe_child<C: IntoElement>(self, child: Option<C>) -> Self {
885        match child {
886            Some(child) => self.child(child),
887            None => self,
888        }
889    }
890}
891
892impl AccessibilityExt for Paragraph {
893    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
894        &mut self.element.accessibility
895    }
896}
897
898impl TextStyleExt for Paragraph {
899    fn get_text_style_data(&mut self) -> &mut TextStyleData {
900        &mut self.element.text_style_data
901    }
902}
903
904impl Paragraph {
905    pub fn try_downcast(element: &dyn ElementExt) -> Option<ParagraphElement> {
906        (element as &dyn Any)
907            .downcast_ref::<ParagraphElement>()
908            .cloned()
909    }
910
911    /// Append every [`Span`] yielded by the iterator to the paragraph.
912    pub fn spans_iter(mut self, spans: impl Iterator<Item = Span<'static>>) -> Self {
913        // TODO: Accessible paragraphs
914        // self.element.accessibility.builder.set_value(text.clone());
915        for span in spans {
916            self.push_span(span);
917        }
918        self
919    }
920
921    /// Append a single [`Span`] of styled text to the paragraph.
922    pub fn span(mut self, span: impl Into<Span<'static>>) -> Self {
923        // TODO: Accessible paragraphs
924        // self.element.accessibility.builder.set_value(text.clone());
925        self.push_span(span.into());
926        self
927    }
928
929    fn push_span(&mut self, span: Span<'static>) {
930        self.element.contents.push(ParagraphContent::Span);
931        self.element.spans.push(span);
932    }
933
934    /// Set the color of the text cursor. See [`Color`].
935    pub fn cursor_color(mut self, cursor_color: impl Into<Color>) -> Self {
936        self.element.cursor_style_data.color = cursor_color.into();
937        self
938    }
939
940    /// Set the color used to highlight selected text. See [`Color`].
941    pub fn highlight_color(mut self, highlight_color: impl Into<Color>) -> Self {
942        self.element.cursor_style_data.highlight_color = highlight_color.into();
943        self
944    }
945
946    /// Set the shape of the text cursor. See [`CursorStyle`].
947    pub fn cursor_style(mut self, cursor_style: impl Into<CursorStyle>) -> Self {
948        self.element.cursor_style = cursor_style.into();
949        self
950    }
951
952    /// Attach a [`ParagraphHolder`] that receives the laid-out paragraph for hit-testing and measurement.
953    pub fn holder(mut self, holder: ParagraphHolder) -> Self {
954        self.element.sk_paragraph = holder;
955        self
956    }
957
958    /// Place the text cursor at the given character index. Pass `None` to hide it.
959    pub fn cursor_index(mut self, cursor_index: impl Into<Option<usize>>) -> Self {
960        self.element.cursor_index = cursor_index.into();
961        self
962    }
963
964    /// Highlight the given `(start, end)` character ranges, used for text selection.
965    pub fn highlights(mut self, highlights: impl Into<Option<Vec<(usize, usize)>>>) -> Self {
966        if let Some(highlights) = highlights.into() {
967            self.element.highlights = highlights;
968        }
969        self
970    }
971
972    /// Limit the paragraph to at most this many lines, truncating the rest. Pass `None` for no limit.
973    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
974        self.element.max_lines = max_lines.into();
975        self
976    }
977
978    /// Override the height of each line as a multiple of the font size. Pass `None` for the default.
979    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
980        self.element.line_height = line_height.into();
981        self
982    }
983
984    /// Set the cursor mode for the paragraph.
985    /// - `CursorMode::Fit`: cursor/highlights use the paragraph's visible_area. VerticalAlign affects cursor positions.
986    /// - `CursorMode::Expanded`: cursor/highlights use the paragraph's inner_area. VerticalAlign does NOT affect cursor positions.
987    pub fn cursor_mode(mut self, cursor_mode: impl Into<CursorMode>) -> Self {
988        self.element.cursor_mode = cursor_mode.into();
989        self
990    }
991
992    /// Set the vertical alignment for the paragraph text.
993    /// This affects how the text is rendered within the paragraph area, but cursor/highlight behavior
994    /// depends on the `cursor_mode` setting.
995    pub fn vertical_align(mut self, vertical_align: impl Into<VerticalAlign>) -> Self {
996        self.element.vertical_align = vertical_align.into();
997        self
998    }
999}
1000
1001/// A run of text with its own style, used to build a [`paragraph()`].
1002///
1003/// Create it with [`Span::new`] (or from a `&str`/`String`) and style it with the
1004/// [`TextStyleExt`] methods such as [`font_size`](TextStyleExt::font_size) and
1005/// [`color`](TextStyleExt::color):
1006///
1007/// ```
1008/// # use freya_core::prelude::*;
1009/// let span = Span::new("Hello").font_size(24.0).color(Color::RED);
1010/// ```
1011#[derive(Clone, PartialEq, Hash)]
1012pub struct Span<'a> {
1013    pub text_style_data: TextStyleData,
1014    pub text: Cow<'a, str>,
1015}
1016
1017impl From<&'static str> for Span<'static> {
1018    fn from(text: &'static str) -> Self {
1019        Span {
1020            text_style_data: TextStyleData::default(),
1021            text: text.into(),
1022        }
1023    }
1024}
1025
1026impl From<String> for Span<'static> {
1027    fn from(text: String) -> Self {
1028        Span {
1029            text_style_data: TextStyleData::default(),
1030            text: text.into(),
1031        }
1032    }
1033}
1034
1035impl<'a> Span<'a> {
1036    /// Create a [`Span`] from the given text, with the default text style.
1037    pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
1038        Self {
1039            text: text.into(),
1040            text_style_data: TextStyleData::default(),
1041        }
1042    }
1043}
1044
1045impl<'a> TextStyleExt for Span<'a> {
1046    fn get_text_style_data(&mut self) -> &mut TextStyleData {
1047        &mut self.text_style_data
1048    }
1049}