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