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 is_transparent(&self) -> bool {
280        false
281    }
282
283    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
284        Cow::Borrowed(&self.text_style_data)
285    }
286
287    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
288        Cow::Borrowed(&self.accessibility)
289    }
290
291    fn layer(&self) -> Layer {
292        self.relative_layer
293    }
294
295    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
296        let cached_paragraph = CachedParagraph {
297            text_style_state: context.text_style_state,
298            spans: &self.spans,
299            max_lines: self.max_lines,
300            line_height: self.line_height,
301            width: context.area_size.width,
302        };
303        let paragraph = context
304            .text_cache
305            .utilize(context.node_id, &cached_paragraph)
306            .unwrap_or_else(|| {
307                let width = if self.max_lines == Some(1)
308                    && context.text_style_state.text_align == TextAlign::default()
309                    && context
310                        .text_style_state
311                        .text_overflow
312                        .get_ellipsis()
313                        .is_none()
314                {
315                    f32::MAX
316                } else {
317                    context.area_size.width + 1.0
318                };
319
320                let paragraph = self.build_paragraph(
321                    context.text_style_state,
322                    context.fallback_fonts,
323                    context.scale_factor,
324                    context.font_collection,
325                    width,
326                    &[],
327                );
328                context
329                    .text_cache
330                    .insert(context.node_id, &cached_paragraph, paragraph)
331            });
332
333        let size = Size2D::new(paragraph.longest_line(), paragraph.height()).max(Size2D::zero());
334
335        self.sk_paragraph
336            .0
337            .borrow_mut()
338            .replace(ParagraphHolderInner {
339                paragraph,
340                scale_factor: context.scale_factor,
341            });
342
343        Some((size, Rc::new(())))
344    }
345
346    fn should_hook_measurement(&self) -> bool {
347        true
348    }
349
350    fn should_measure_inner_children(&self) -> bool {
351        self.has_inline_content()
352    }
353
354    fn needs_post_measure(&self) -> bool {
355        self.has_inline_content()
356    }
357
358    fn post_measure(&self, context: PostMeasureContext) -> PostMeasure<NodeId> {
359        if context.children.is_empty() {
360            return PostMeasure::default();
361        }
362
363        let placeholders: Vec<Size2D> = context
364            .children
365            .iter()
366            .map(|child| {
367                context
368                    .layout
369                    .get(child)
370                    .map(|node| node.area.size)
371                    .unwrap()
372            })
373            .collect();
374
375        let width = self
376            .sk_paragraph
377            .0
378            .borrow()
379            .as_ref()
380            .map(|holder| holder.paragraph.max_width())
381            .unwrap();
382
383        let paragraph = self.build_paragraph(
384            context.text_style_state,
385            context.fallback_fonts,
386            context.scale_factor,
387            context.font_collection,
388            width,
389            &placeholders,
390        );
391        let rects = paragraph.get_rects_for_placeholders();
392        let paragraph_height = paragraph.height();
393        // The size with the placeholders in place, so the node is sized around the inline children.
394        let content_size = Size2D::new(paragraph.longest_line(), paragraph_height);
395
396        self.sk_paragraph
397            .0
398            .borrow_mut()
399            .replace(ParagraphHolderInner {
400                paragraph: Rc::new(paragraph),
401                scale_factor: context.scale_factor,
402            });
403
404        let visible_area = context.node_layout.visible_area();
405        let vertical_offset = match self.vertical_align {
406            VerticalAlign::Start => 0.0,
407            VerticalAlign::Center => (visible_area.height() - paragraph_height).max(0.0) / 2.0,
408        };
409        let origin = visible_area.origin;
410
411        let mut offsets = Vec::new();
412        let mut hidden_children = Vec::new();
413        for (index, child_id) in context.children.iter().enumerate() {
414            let Some(current) = context.layout.get(child_id).map(|node| node.area.origin) else {
415                continue;
416            };
417            match rects.get(index) {
418                Some(rect) => {
419                    let offset_x = origin.x + rect.rect.left - current.x;
420                    let offset_y = origin.y + vertical_offset + rect.rect.top - current.y;
421                    offsets.push((*child_id, Length::new(offset_x), Length::new(offset_y)));
422                }
423                // Children whose placeholder got cut off by max_lines or ellipsis are hidden.
424                None => hidden_children.push(*child_id),
425            }
426        }
427
428        PostMeasure {
429            content_size: Some(content_size),
430            offsets,
431            hidden_children,
432        }
433    }
434
435    fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
436        Some(Cow::Borrowed(&self.event_handlers))
437    }
438
439    fn render(&self, context: RenderContext) {
440        let paragraph = self.sk_paragraph.0.borrow();
441        let ParagraphHolderInner { paragraph, .. } = paragraph.as_ref().unwrap();
442        let visible_area = context.layout_node.visible_area();
443
444        let cursor_area = match self.cursor_mode {
445            CursorMode::Fit => visible_area,
446            CursorMode::Expanded => context.layout_node.area,
447        };
448
449        let paragraph_height = paragraph.height();
450        let area_height = visible_area.height();
451        let vertical_offset = match self.vertical_align {
452            VerticalAlign::Start => 0.0,
453            VerticalAlign::Center => (area_height - paragraph_height).max(0.0) / 2.0,
454        };
455
456        let cursor_vertical_offset = match self.cursor_mode {
457            CursorMode::Fit => vertical_offset,
458            CursorMode::Expanded => 0.0,
459        };
460        let cursor_vertical_size_offset = match self.cursor_mode {
461            CursorMode::Fit => 0.,
462            CursorMode::Expanded => vertical_offset * 2.,
463        };
464
465        // Draw highlights
466        for (from, to) in self.highlights.iter() {
467            if from == to {
468                continue;
469            }
470            let (from, to) = { if from < to { (from, to) } else { (to, from) } };
471            let rects = paragraph.get_rects_for_range(
472                *from..*to,
473                RectHeightStyle::Tight,
474                RectWidthStyle::Tight,
475            );
476
477            let mut highlights_paint = Paint::default();
478            highlights_paint.set_anti_alias(true);
479            highlights_paint.set_style(PaintStyle::Fill);
480            highlights_paint.set_color(self.cursor_style_data.highlight_color);
481
482            if rects.is_empty() && *from == 0 {
483                let avg_line_height =
484                    paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
485                let rect = SkRect::new(
486                    cursor_area.min_x(),
487                    cursor_area.min_y() + cursor_vertical_offset,
488                    cursor_area.min_x() + 6.,
489                    cursor_area.min_y() + avg_line_height + cursor_vertical_size_offset,
490                );
491
492                context.canvas.draw_rect(rect, &highlights_paint);
493            }
494
495            for rect in rects {
496                let rect = SkRect::new(
497                    cursor_area.min_x() + rect.rect.left,
498                    cursor_area.min_y() + rect.rect.top + cursor_vertical_offset,
499                    cursor_area.min_x() + rect.rect.right.max(6.),
500                    cursor_area.min_y() + rect.rect.bottom + cursor_vertical_size_offset,
501                );
502                context.canvas.draw_rect(rect, &highlights_paint);
503            }
504        }
505
506        // We exclude those highlights that on the same start and end (e.g the user just started dragging)
507        let visible_highlights = self
508            .highlights
509            .iter()
510            .filter(|highlight| highlight.0 != highlight.1)
511            .count()
512            > 0;
513
514        // Draw block cursor behind text if needed
515        if let Some(cursor_index) = self.cursor_index
516            && self.cursor_style == CursorStyle::Block
517            && let Some(cursor_rect) = paragraph
518                .get_rects_for_range(
519                    cursor_index..cursor_index + 1,
520                    RectHeightStyle::Tight,
521                    RectWidthStyle::Tight,
522                )
523                .first()
524                .map(|text| text.rect)
525                .or_else(|| {
526                    // Show the cursor at the end of the text if possible
527                    let text_len = paragraph
528                        .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
529                        .position as usize;
530                    let last_rects = paragraph.get_rects_for_range(
531                        text_len.saturating_sub(1)..text_len,
532                        RectHeightStyle::Tight,
533                        RectWidthStyle::Tight,
534                    );
535
536                    if let Some(last_rect) = last_rects.first() {
537                        let mut caret = last_rect.rect;
538                        caret.left = caret.right;
539                        Some(caret)
540                    } else {
541                        let avg_line_height =
542                            paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
543                        Some(SkRect::new(0., 0., 6., avg_line_height))
544                    }
545                })
546        {
547            let width = (cursor_rect.right - cursor_rect.left).max(6.0);
548            let cursor_rect = SkRect::new(
549                cursor_area.min_x() + cursor_rect.left,
550                cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
551                cursor_area.min_x() + cursor_rect.left + width,
552                cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
553            );
554
555            let mut paint = Paint::default();
556            paint.set_anti_alias(true);
557            paint.set_style(PaintStyle::Fill);
558            paint.set_color(self.cursor_style_data.color);
559
560            context.canvas.draw_rect(cursor_rect, &paint);
561        }
562
563        // Draw text (always uses visible_area with vertical_offset)
564        paint_paragraph_with_fill(
565            paragraph,
566            context.canvas,
567            Point2D::new(visible_area.min_x(), visible_area.min_y() + vertical_offset),
568            &context.text_style_state.color,
569        );
570
571        // Draw cursor
572        if let Some(cursor_index) = self.cursor_index
573            && !visible_highlights
574        {
575            let cursor_rects = paragraph.get_rects_for_range(
576                cursor_index..cursor_index + 1,
577                RectHeightStyle::Tight,
578                RectWidthStyle::Tight,
579            );
580            if let Some(cursor_rect) = cursor_rects.first().map(|text| text.rect).or_else(|| {
581                // Show the cursor at the end of the text if possible
582                let text_len = paragraph
583                    .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
584                    .position as usize;
585                let last_rects = paragraph.get_rects_for_range(
586                    text_len.saturating_sub(1)..text_len,
587                    RectHeightStyle::Tight,
588                    RectWidthStyle::Tight,
589                );
590
591                if let Some(last_rect) = last_rects.first() {
592                    let mut caret = last_rect.rect;
593                    caret.left = caret.right;
594                    Some(caret)
595                } else {
596                    None
597                }
598            }) {
599                let paint_color = self.cursor_style_data.color;
600                match self.cursor_style {
601                    CursorStyle::Underline => {
602                        let thickness = 2.0;
603                        let underline_rect = SkRect::new(
604                            cursor_area.min_x() + cursor_rect.left,
605                            cursor_area.min_y() + cursor_rect.bottom - thickness
606                                + cursor_vertical_offset,
607                            cursor_area.min_x() + cursor_rect.right,
608                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
609                        );
610
611                        let mut paint = Paint::default();
612                        paint.set_anti_alias(true);
613                        paint.set_style(PaintStyle::Fill);
614                        paint.set_color(paint_color);
615
616                        context.canvas.draw_rect(underline_rect, &paint);
617                    }
618                    CursorStyle::Line => {
619                        let cursor_rect = SkRect::new(
620                            cursor_area.min_x() + cursor_rect.left,
621                            cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
622                            cursor_area.min_x() + cursor_rect.left + 2.,
623                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
624                        );
625
626                        let mut paint = Paint::default();
627                        paint.set_anti_alias(true);
628                        paint.set_style(PaintStyle::Fill);
629                        paint.set_color(paint_color);
630
631                        context.canvas.draw_rect(cursor_rect, &paint);
632                    }
633                    _ => {}
634                }
635            }
636        }
637    }
638}
639
640impl ParagraphElement {
641    fn has_inline_content(&self) -> bool {
642        self.contents
643            .iter()
644            .any(|content| matches!(content, ParagraphContent::Element))
645    }
646
647    /// Builds the Skia paragraph from the content, reserving a placeholder (sized from
648    /// `placeholders`, in order) for each inline child, laid out against `width`.
649    fn build_paragraph(
650        &self,
651        text_style_state: &TextStyleState,
652        fallback_fonts: &[Cow<'static, str>],
653        scale_factor: f64,
654        font_collection: &FontCollection,
655        width: f32,
656        placeholders: &[Size2D],
657    ) -> SkParagraph {
658        let mut paragraph_style = ParagraphStyle::default();
659
660        if let Some(ellipsis) = text_style_state.text_overflow.get_ellipsis() {
661            paragraph_style.set_ellipsis(ellipsis);
662        }
663
664        paragraph_style.set_text_style(&base_text_style(
665            text_style_state,
666            fallback_fonts,
667            scale_factor,
668            self.line_height,
669        ));
670        paragraph_style.set_max_lines(self.max_lines);
671        paragraph_style.set_text_align(text_style_state.text_align.into());
672
673        let mut paragraph_builder = ParagraphBuilder::new(&paragraph_style, font_collection);
674
675        let mut spans = self.spans.iter();
676        let mut placeholders = placeholders.iter();
677        for content in &self.contents {
678            match content {
679                ParagraphContent::Span => {
680                    let Some(span) = spans.next() else { continue };
681                    paragraph_builder.push_style(&span_text_style(
682                        text_style_state,
683                        fallback_fonts,
684                        scale_factor,
685                        span,
686                        self.line_height,
687                    ));
688                    paragraph_builder.add_text(&span.text);
689                }
690                ParagraphContent::Element => {
691                    let Some(size) = placeholders.next() else {
692                        continue;
693                    };
694                    paragraph_builder.add_placeholder(&PlaceholderStyle::new(
695                        size.width,
696                        size.height,
697                        PlaceholderAlignment::Middle,
698                        TextBaseline::Alphabetic,
699                        0.0,
700                    ));
701                }
702            }
703        }
704
705        let mut paragraph = paragraph_builder.build();
706        paragraph.layout(width);
707        paragraph
708    }
709}
710
711impl From<Paragraph> for Element {
712    fn from(value: Paragraph) -> Self {
713        let elements = value
714            .children
715            .into_iter()
716            .map(|child| {
717                rect()
718                    .position(Position::new_absolute())
719                    .child(child)
720                    .into_element()
721            })
722            .collect();
723
724        Element::Element {
725            key: value.key,
726            element: Rc::new(value.element),
727            elements,
728        }
729    }
730}
731
732/// Builds the paragraph-level base [TextStyle] from the inherited text style state.
733fn base_text_style(
734    text_style_state: &TextStyleState,
735    fallback_fonts: &[Cow<'static, str>],
736    scale_factor: f64,
737    line_height: Option<f32>,
738) -> TextStyle {
739    let mut text_style = TextStyle::default();
740
741    let mut font_families = text_style_state.font_families.clone();
742    font_families.extend_from_slice(fallback_fonts);
743
744    text_style.set_color(text_style_state.color.as_color().unwrap_or(Color::WHITE));
745    text_style.set_font_size(f32::from(text_style_state.font_size) * scale_factor as f32);
746    text_style.set_font_families(&font_families);
747    text_style.set_font_style(FontStyle::new(
748        text_style_state.font_weight.into(),
749        text_style_state.font_width.into(),
750        text_style_state.font_slant.into(),
751    ));
752
753    if text_style_state.text_height.needs_custom_height() {
754        text_style.set_height_override(true);
755        text_style.set_half_leading(true);
756    }
757
758    if let Some(line_height) = line_height {
759        text_style.set_height_override(true);
760        text_style.set_height(line_height);
761    }
762
763    for text_shadow in text_style_state.text_shadows.iter() {
764        text_style.add_shadow((*text_shadow).into());
765    }
766
767    text_style
768}
769
770/// Builds the [TextStyle] for a single [Span], layering its overrides over the inherited state.
771fn span_text_style(
772    text_style_state: &TextStyleState,
773    fallback_fonts: &[Cow<'static, str>],
774    scale_factor: f64,
775    span: &Span,
776    line_height: Option<f32>,
777) -> TextStyle {
778    let span_style = TextStyleState::from_data(text_style_state, &span.text_style_data);
779    let mut text_style = TextStyle::new();
780    let mut font_families = text_style_state.font_families.clone();
781    font_families.extend_from_slice(fallback_fonts);
782
783    for text_shadow in span_style.text_shadows.iter() {
784        text_style.add_shadow((*text_shadow).into());
785    }
786
787    text_style.set_color(span_style.color.as_color().unwrap_or(Color::WHITE));
788    text_style.set_font_size(f32::from(span_style.font_size) * scale_factor as f32);
789    text_style.set_font_families(&font_families);
790    text_style.set_font_style(FontStyle::new(
791        span_style.font_weight.into(),
792        span_style.font_width.into(),
793        span_style.font_slant.into(),
794    ));
795    text_style.set_decoration_type(span_style.text_decoration.into());
796    if let Some(line_height) = line_height {
797        text_style.set_height_override(true);
798        text_style.set_height(line_height);
799    }
800    text_style
801}
802
803/// Paints a paragraph with a [Fill] as the text color. Non-color fills are masked
804/// onto the rendered glyph alpha via an offscreen layer + [BlendMode::SrcIn].
805pub(crate) fn paint_paragraph_with_fill(
806    paragraph: &SkParagraph,
807    canvas: &Canvas,
808    origin: Point2D,
809    fill: &Fill,
810) {
811    if matches!(fill, Fill::Color(_)) {
812        paragraph.paint(canvas, origin.to_tuple());
813        return;
814    }
815
816    let width = paragraph.longest_line();
817    let height = paragraph.height();
818    let area = Area::new(origin, Size2D::new(width, height));
819    let bounds_rect = SkRect::from_xywh(area.min_x(), area.min_y(), width, height);
820
821    let layer = canvas.save_layer(&SaveLayerRec::default().bounds(&bounds_rect));
822
823    paragraph.paint(canvas, origin.to_tuple());
824
825    let mut paint = Paint::default();
826    paint.set_anti_alias(true);
827    paint.set_style(PaintStyle::Fill);
828    paint.set_blend_mode(BlendMode::SrcIn);
829    fill.apply_to_paint(&mut paint, area);
830
831    canvas.draw_rect(bounds_rect, &paint);
832
833    canvas.restore_to_count(layer);
834}
835
836impl KeyExt for Paragraph {
837    fn write_key(&mut self) -> &mut DiffKey {
838        &mut self.key
839    }
840}
841
842impl EventHandlersExt for Paragraph {
843    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
844        &mut self.element.event_handlers
845    }
846}
847
848impl MaybeExt for Paragraph {}
849
850impl LayerExt for Paragraph {
851    fn get_layer(&mut self) -> &mut Layer {
852        &mut self.element.relative_layer
853    }
854}
855
856pub struct Paragraph {
857    key: DiffKey,
858    element: ParagraphElement,
859    children: Vec<Element>,
860}
861
862impl LayoutExt for Paragraph {
863    fn get_layout(&mut self) -> &mut LayoutData {
864        &mut self.element.layout
865    }
866}
867
868impl ContainerExt for Paragraph {}
869
870/// Children added to a [Paragraph] flow inline at the point they were added, each laid out at
871/// its own measured size, so give them an explicit `width` and `height`.
872impl ChildrenExt for Paragraph {
873    fn get_children(&mut self) -> &mut Vec<Element> {
874        &mut self.children
875    }
876
877    fn child<C: IntoElement>(mut self, child: C) -> Self {
878        self.element.contents.push(ParagraphContent::Element);
879        self.children.push(child.into_element());
880        self
881    }
882
883    fn children(self, children: impl IntoIterator<Item = Element>) -> Self {
884        children
885            .into_iter()
886            .fold(self, |paragraph, child| paragraph.child(child))
887    }
888
889    fn maybe_child<C: IntoElement>(self, child: Option<C>) -> Self {
890        match child {
891            Some(child) => self.child(child),
892            None => self,
893        }
894    }
895}
896
897impl AccessibilityExt for Paragraph {
898    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
899        &mut self.element.accessibility
900    }
901}
902
903impl TextStyleExt for Paragraph {
904    fn get_text_style_data(&mut self) -> &mut TextStyleData {
905        &mut self.element.text_style_data
906    }
907}
908
909impl Paragraph {
910    pub fn try_downcast(element: &dyn ElementExt) -> Option<ParagraphElement> {
911        (element as &dyn Any)
912            .downcast_ref::<ParagraphElement>()
913            .cloned()
914    }
915
916    /// Append every [`Span`] yielded by the iterator to the paragraph.
917    pub fn spans_iter(mut self, spans: impl Iterator<Item = Span<'static>>) -> Self {
918        // TODO: Accessible paragraphs
919        // self.element.accessibility.builder.set_value(text.clone());
920        for span in spans {
921            self.push_span(span);
922        }
923        self
924    }
925
926    /// Append a single [`Span`] of styled text to the paragraph.
927    pub fn span(mut self, span: impl Into<Span<'static>>) -> Self {
928        // TODO: Accessible paragraphs
929        // self.element.accessibility.builder.set_value(text.clone());
930        self.push_span(span.into());
931        self
932    }
933
934    fn push_span(&mut self, span: Span<'static>) {
935        self.element.contents.push(ParagraphContent::Span);
936        self.element.spans.push(span);
937    }
938
939    /// Set the color of the text cursor. See [`Color`].
940    pub fn cursor_color(mut self, cursor_color: impl Into<Color>) -> Self {
941        self.element.cursor_style_data.color = cursor_color.into();
942        self
943    }
944
945    /// Set the color used to highlight selected text. See [`Color`].
946    pub fn highlight_color(mut self, highlight_color: impl Into<Color>) -> Self {
947        self.element.cursor_style_data.highlight_color = highlight_color.into();
948        self
949    }
950
951    /// Set the shape of the text cursor. See [`CursorStyle`].
952    pub fn cursor_style(mut self, cursor_style: impl Into<CursorStyle>) -> Self {
953        self.element.cursor_style = cursor_style.into();
954        self
955    }
956
957    /// Attach a [`ParagraphHolder`] that receives the laid-out paragraph for hit-testing and measurement.
958    pub fn holder(mut self, holder: ParagraphHolder) -> Self {
959        self.element.sk_paragraph = holder;
960        self
961    }
962
963    /// Place the text cursor at the given character index. Pass `None` to hide it.
964    pub fn cursor_index(mut self, cursor_index: impl Into<Option<usize>>) -> Self {
965        self.element.cursor_index = cursor_index.into();
966        self
967    }
968
969    /// Highlight the given `(start, end)` character ranges, used for text selection.
970    pub fn highlights(mut self, highlights: impl Into<Option<Vec<(usize, usize)>>>) -> Self {
971        if let Some(highlights) = highlights.into() {
972            self.element.highlights = highlights;
973        }
974        self
975    }
976
977    /// Limit the paragraph to at most this many lines, truncating the rest. Pass `None` for no limit.
978    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
979        self.element.max_lines = max_lines.into();
980        self
981    }
982
983    /// Override the height of each line as a multiple of the font size. Pass `None` for the default.
984    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
985        self.element.line_height = line_height.into();
986        self
987    }
988
989    /// Set the cursor mode for the paragraph.
990    /// - `CursorMode::Fit`: cursor/highlights use the paragraph's visible_area. VerticalAlign affects cursor positions.
991    /// - `CursorMode::Expanded`: cursor/highlights use the paragraph's inner_area. VerticalAlign does NOT affect cursor positions.
992    pub fn cursor_mode(mut self, cursor_mode: impl Into<CursorMode>) -> Self {
993        self.element.cursor_mode = cursor_mode.into();
994        self
995    }
996
997    /// Set the vertical alignment for the paragraph text.
998    /// This affects how the text is rendered within the paragraph area, but cursor/highlight behavior
999    /// depends on the `cursor_mode` setting.
1000    pub fn vertical_align(mut self, vertical_align: impl Into<VerticalAlign>) -> Self {
1001        self.element.vertical_align = vertical_align.into();
1002        self
1003    }
1004}
1005
1006/// A run of text with its own style, used to build a [`paragraph()`].
1007///
1008/// Create it with [`Span::new`] (or from a `&str`/`String`) and style it with the
1009/// [`TextStyleExt`] methods such as [`font_size`](TextStyleExt::font_size) and
1010/// [`color`](TextStyleExt::color):
1011///
1012/// ```
1013/// # use freya_core::prelude::*;
1014/// let span = Span::new("Hello").font_size(24.0).color(Color::RED);
1015/// ```
1016#[derive(Clone, PartialEq, Hash)]
1017pub struct Span<'a> {
1018    pub text_style_data: TextStyleData,
1019    pub text: Cow<'a, str>,
1020}
1021
1022impl From<&'static str> for Span<'static> {
1023    fn from(text: &'static str) -> Self {
1024        Span {
1025            text_style_data: TextStyleData::default(),
1026            text: text.into(),
1027        }
1028    }
1029}
1030
1031impl From<String> for Span<'static> {
1032    fn from(text: String) -> Self {
1033        Span {
1034            text_style_data: TextStyleData::default(),
1035            text: text.into(),
1036        }
1037    }
1038}
1039
1040impl<'a> Span<'a> {
1041    /// Create a [`Span`] from the given text, with the default text style.
1042    pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
1043        Self {
1044            text: text.into(),
1045            text_style_data: TextStyleData::default(),
1046        }
1047    }
1048}
1049
1050impl<'a> TextStyleExt for Span<'a> {
1051    fn get_text_style_data(&mut self) -> &mut TextStyleData {
1052        &mut self.text_style_data
1053    }
1054}