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