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