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    FontStyle,
16    Paint,
17    PaintStyle,
18    ParagraphBuilder,
19    ParagraphStyle,
20    RectHeightStyle,
21    RectWidthStyle,
22    SkParagraph,
23    SkRect,
24    TextStyle,
25};
26use rustc_hash::FxHashMap;
27use torin::prelude::Size2D;
28
29use crate::{
30    data::{
31        AccessibilityData,
32        CursorStyleData,
33        EffectData,
34        LayoutData,
35        StyleState,
36        TextStyleData,
37        TextStyleState,
38    },
39    diff_key::DiffKey,
40    element::{
41        Element,
42        ElementExt,
43        EventHandlerType,
44        LayoutContext,
45        RenderContext,
46    },
47    events::name::EventName,
48    layers::Layer,
49    prelude::{
50        AccessibilityExt,
51        Color,
52        ContainerExt,
53        EventHandlersExt,
54        KeyExt,
55        LayerExt,
56        LayoutExt,
57        MaybeExt,
58        TextAlign,
59        TextStyleExt,
60        VerticalAlign,
61    },
62    style::cursor::{
63        CursorMode,
64        CursorStyle,
65    },
66    text_cache::CachedParagraph,
67    tree::DiffModifies,
68};
69
70/// [paragraph()] makes it possible to render rich text with different styles. Its a more customizable API than [crate::elements::label].
71///
72/// See the available methods in [Paragraph].
73///
74/// ```rust
75/// # use freya::prelude::*;
76/// fn app() -> impl IntoElement {
77///     paragraph()
78///         .span(Span::new("Hello").font_size(24.0))
79///         .span(Span::new("World").font_size(16.0))
80/// }
81/// ```
82pub fn paragraph() -> Paragraph {
83    Paragraph {
84        key: DiffKey::None,
85        element: ParagraphElement::default(),
86    }
87}
88
89pub struct ParagraphHolderInner {
90    pub paragraph: Rc<SkParagraph>,
91    pub scale_factor: f64,
92}
93
94#[derive(Clone)]
95pub struct ParagraphHolder(pub Rc<RefCell<Option<ParagraphHolderInner>>>);
96
97impl PartialEq for ParagraphHolder {
98    fn eq(&self, other: &Self) -> bool {
99        Rc::ptr_eq(&self.0, &other.0)
100    }
101}
102
103impl Debug for ParagraphHolder {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.write_str("ParagraphHolder")
106    }
107}
108
109impl Default for ParagraphHolder {
110    fn default() -> Self {
111        Self(Rc::new(RefCell::new(None)))
112    }
113}
114
115#[derive(PartialEq, Clone)]
116pub struct ParagraphElement {
117    pub layout: LayoutData,
118    pub spans: Vec<Span<'static>>,
119    pub accessibility: AccessibilityData,
120    pub text_style_data: TextStyleData,
121    pub cursor_style_data: CursorStyleData,
122    pub event_handlers: FxHashMap<EventName, EventHandlerType>,
123    pub sk_paragraph: ParagraphHolder,
124    pub cursor_index: Option<usize>,
125    pub highlights: Vec<(usize, usize)>,
126    pub max_lines: Option<usize>,
127    pub line_height: Option<f32>,
128    pub relative_layer: Layer,
129    pub cursor_style: CursorStyle,
130    pub cursor_mode: CursorMode,
131    pub vertical_align: VerticalAlign,
132}
133
134impl Default for ParagraphElement {
135    fn default() -> Self {
136        let mut accessibility = AccessibilityData::default();
137        accessibility.builder.set_role(accesskit::Role::Paragraph);
138        Self {
139            layout: Default::default(),
140            spans: Default::default(),
141            accessibility,
142            text_style_data: Default::default(),
143            cursor_style_data: Default::default(),
144            event_handlers: Default::default(),
145            sk_paragraph: Default::default(),
146            cursor_index: Default::default(),
147            highlights: Default::default(),
148            max_lines: Default::default(),
149            line_height: Default::default(),
150            relative_layer: Default::default(),
151            cursor_style: CursorStyle::default(),
152            cursor_mode: CursorMode::default(),
153            vertical_align: VerticalAlign::default(),
154        }
155    }
156}
157
158impl Display for ParagraphElement {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.write_str(
161            &self
162                .spans
163                .iter()
164                .map(|s| s.text.clone())
165                .collect::<Vec<_>>()
166                .join("\n"),
167        )
168    }
169}
170
171impl ElementExt for ParagraphElement {
172    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
173        let Some(paragraph) = (other.as_ref() as &dyn Any).downcast_ref::<ParagraphElement>()
174        else {
175            return false;
176        };
177        self != paragraph
178    }
179
180    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
181        let Some(paragraph) = (other.as_ref() as &dyn Any).downcast_ref::<ParagraphElement>()
182        else {
183            return DiffModifies::all();
184        };
185
186        let mut diff = DiffModifies::empty();
187
188        if self.spans != paragraph.spans {
189            diff.insert(DiffModifies::STYLE);
190            diff.insert(DiffModifies::LAYOUT);
191        }
192
193        if self.accessibility != paragraph.accessibility {
194            diff.insert(DiffModifies::ACCESSIBILITY);
195        }
196
197        if self.relative_layer != paragraph.relative_layer {
198            diff.insert(DiffModifies::LAYER);
199        }
200
201        if self.text_style_data != paragraph.text_style_data {
202            diff.insert(DiffModifies::STYLE);
203        }
204
205        if self.event_handlers != paragraph.event_handlers {
206            diff.insert(DiffModifies::EVENT_HANDLERS);
207        }
208
209        if self.cursor_index != paragraph.cursor_index
210            || self.highlights != paragraph.highlights
211            || self.cursor_mode != paragraph.cursor_mode
212            || self.vertical_align != paragraph.vertical_align
213        {
214            diff.insert(DiffModifies::STYLE);
215        }
216
217        if self.text_style_data != paragraph.text_style_data
218            || self.line_height != paragraph.line_height
219            || self.max_lines != paragraph.max_lines
220        {
221            diff.insert(DiffModifies::TEXT_STYLE);
222            diff.insert(DiffModifies::LAYOUT);
223        }
224
225        if self.layout != paragraph.layout {
226            diff.insert(DiffModifies::STYLE);
227            diff.insert(DiffModifies::LAYOUT);
228        }
229
230        diff
231    }
232
233    fn layout(&'_ self) -> Cow<'_, LayoutData> {
234        Cow::Borrowed(&self.layout)
235    }
236    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
237        None
238    }
239
240    fn style(&'_ self) -> Cow<'_, StyleState> {
241        Cow::Owned(StyleState::default())
242    }
243
244    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
245        Cow::Borrowed(&self.text_style_data)
246    }
247
248    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
249        Cow::Borrowed(&self.accessibility)
250    }
251
252    fn layer(&self) -> Layer {
253        self.relative_layer
254    }
255
256    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
257        let cached_paragraph = CachedParagraph {
258            text_style_state: context.text_style_state,
259            spans: &self.spans,
260            max_lines: self.max_lines,
261            line_height: self.line_height,
262            width: context.area_size.width,
263        };
264        let paragraph = context
265            .text_cache
266            .utilize(context.node_id, &cached_paragraph)
267            .unwrap_or_else(|| {
268                let mut paragraph_style = ParagraphStyle::default();
269                let mut text_style = TextStyle::default();
270
271                let mut font_families = context.text_style_state.font_families.clone();
272                font_families.extend_from_slice(context.fallback_fonts);
273
274                text_style.set_color(context.text_style_state.color);
275                text_style.set_font_size(
276                    f32::from(context.text_style_state.font_size) * context.scale_factor as f32,
277                );
278                text_style.set_font_families(&font_families);
279                text_style.set_font_style(FontStyle::new(
280                    context.text_style_state.font_weight.into(),
281                    context.text_style_state.font_width.into(),
282                    context.text_style_state.font_slant.into(),
283                ));
284
285                if context.text_style_state.text_height.needs_custom_height() {
286                    text_style.set_height_override(true);
287                    text_style.set_half_leading(true);
288                }
289
290                if let Some(line_height) = self.line_height {
291                    text_style.set_height_override(true).set_height(line_height);
292                }
293
294                for text_shadow in context.text_style_state.text_shadows.iter() {
295                    text_style.add_shadow((*text_shadow).into());
296                }
297
298                if let Some(ellipsis) = context.text_style_state.text_overflow.get_ellipsis() {
299                    paragraph_style.set_ellipsis(ellipsis);
300                }
301
302                paragraph_style.set_text_style(&text_style);
303                paragraph_style.set_max_lines(self.max_lines);
304                paragraph_style.set_text_align(context.text_style_state.text_align.into());
305
306                let mut paragraph_builder =
307                    ParagraphBuilder::new(&paragraph_style, context.font_collection);
308
309                for span in &self.spans {
310                    let text_style_state =
311                        TextStyleState::from_data(context.text_style_state, &span.text_style_data);
312                    let mut text_style = TextStyle::new();
313                    let mut font_families = context.text_style_state.font_families.clone();
314                    font_families.extend_from_slice(context.fallback_fonts);
315
316                    for text_shadow in text_style_state.text_shadows.iter() {
317                        text_style.add_shadow((*text_shadow).into());
318                    }
319
320                    text_style.set_color(text_style_state.color);
321                    text_style.set_font_size(
322                        f32::from(text_style_state.font_size) * context.scale_factor as f32,
323                    );
324                    text_style.set_font_families(&font_families);
325                    paragraph_builder.push_style(&text_style);
326                    paragraph_builder.add_text(&span.text);
327                }
328
329                let mut paragraph = paragraph_builder.build();
330                paragraph.layout(
331                    if self.max_lines == Some(1)
332                        && context.text_style_state.text_align == TextAlign::default()
333                        && !paragraph_style.ellipsized()
334                    {
335                        f32::MAX
336                    } else {
337                        context.area_size.width + 1.0
338                    },
339                );
340                context
341                    .text_cache
342                    .insert(context.node_id, &cached_paragraph, paragraph)
343            });
344
345        let size = Size2D::new(paragraph.longest_line(), paragraph.height());
346
347        self.sk_paragraph
348            .0
349            .borrow_mut()
350            .replace(ParagraphHolderInner {
351                paragraph,
352                scale_factor: context.scale_factor,
353            });
354
355        Some((size, Rc::new(())))
356    }
357
358    fn should_hook_measurement(&self) -> bool {
359        true
360    }
361
362    fn should_measure_inner_children(&self) -> bool {
363        false
364    }
365
366    fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
367        Some(Cow::Borrowed(&self.event_handlers))
368    }
369
370    fn render(&self, context: RenderContext) {
371        let paragraph = self.sk_paragraph.0.borrow();
372        let ParagraphHolderInner { paragraph, .. } = paragraph.as_ref().unwrap();
373        let visible_area = context.layout_node.visible_area();
374
375        let cursor_area = match self.cursor_mode {
376            CursorMode::Fit => visible_area,
377            CursorMode::Expanded => context.layout_node.area,
378        };
379
380        let paragraph_height = paragraph.height();
381        let area_height = visible_area.height();
382        let vertical_offset = match self.vertical_align {
383            VerticalAlign::Start => 0.0,
384            VerticalAlign::Center => (area_height - paragraph_height).max(0.0) / 2.0,
385        };
386
387        let cursor_vertical_offset = match self.cursor_mode {
388            CursorMode::Fit => vertical_offset,
389            CursorMode::Expanded => 0.0,
390        };
391        let cursor_vertical_size_offset = match self.cursor_mode {
392            CursorMode::Fit => 0.,
393            CursorMode::Expanded => vertical_offset * 2.,
394        };
395
396        // Draw highlights
397        for (from, to) in self.highlights.iter() {
398            if from == to {
399                continue;
400            }
401            let (from, to) = { if from < to { (from, to) } else { (to, from) } };
402            let rects = paragraph.get_rects_for_range(
403                *from..*to,
404                RectHeightStyle::Tight,
405                RectWidthStyle::Tight,
406            );
407
408            let mut highlights_paint = Paint::default();
409            highlights_paint.set_anti_alias(true);
410            highlights_paint.set_style(PaintStyle::Fill);
411            highlights_paint.set_color(self.cursor_style_data.highlight_color);
412
413            if rects.is_empty() && *from == 0 {
414                let avg_line_height =
415                    paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
416                let rect = SkRect::new(
417                    cursor_area.min_x(),
418                    cursor_area.min_y() + cursor_vertical_offset,
419                    cursor_area.min_x() + 6.,
420                    cursor_area.min_y() + avg_line_height + cursor_vertical_size_offset,
421                );
422
423                context.canvas.draw_rect(rect, &highlights_paint);
424            }
425
426            for rect in rects {
427                let rect = SkRect::new(
428                    cursor_area.min_x() + rect.rect.left,
429                    cursor_area.min_y() + rect.rect.top + cursor_vertical_offset,
430                    cursor_area.min_x() + rect.rect.right.max(6.),
431                    cursor_area.min_y() + rect.rect.bottom + cursor_vertical_size_offset,
432                );
433                context.canvas.draw_rect(rect, &highlights_paint);
434            }
435        }
436
437        // We exclude those highlights that on the same start and end (e.g the user just started dragging)
438        let visible_highlights = self
439            .highlights
440            .iter()
441            .filter(|highlight| highlight.0 != highlight.1)
442            .count()
443            > 0;
444
445        // Draw block cursor behind text if needed
446        if let Some(cursor_index) = self.cursor_index
447            && self.cursor_style == CursorStyle::Block
448            && let Some(cursor_rect) = paragraph
449                .get_rects_for_range(
450                    cursor_index..cursor_index + 1,
451                    RectHeightStyle::Tight,
452                    RectWidthStyle::Tight,
453                )
454                .first()
455                .map(|text| text.rect)
456                .or_else(|| {
457                    // Show the cursor at the end of the text if possible
458                    let text_len = paragraph
459                        .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
460                        .position as usize;
461                    let last_rects = paragraph.get_rects_for_range(
462                        text_len.saturating_sub(1)..text_len,
463                        RectHeightStyle::Tight,
464                        RectWidthStyle::Tight,
465                    );
466
467                    if let Some(last_rect) = last_rects.first() {
468                        let mut caret = last_rect.rect;
469                        caret.left = caret.right;
470                        Some(caret)
471                    } else {
472                        let avg_line_height =
473                            paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
474                        Some(SkRect::new(0., 0., 6., avg_line_height))
475                    }
476                })
477        {
478            let width = (cursor_rect.right - cursor_rect.left).max(6.0);
479            let cursor_rect = SkRect::new(
480                cursor_area.min_x() + cursor_rect.left,
481                cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
482                cursor_area.min_x() + cursor_rect.left + width,
483                cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
484            );
485
486            let mut paint = Paint::default();
487            paint.set_anti_alias(true);
488            paint.set_style(PaintStyle::Fill);
489            paint.set_color(self.cursor_style_data.color);
490
491            context.canvas.draw_rect(cursor_rect, &paint);
492        }
493
494        // Draw text (always uses visible_area with vertical_offset)
495        paragraph.paint(
496            context.canvas,
497            (visible_area.min_x(), visible_area.min_y() + vertical_offset),
498        );
499
500        // Draw cursor
501        if let Some(cursor_index) = self.cursor_index
502            && !visible_highlights
503        {
504            let cursor_rects = paragraph.get_rects_for_range(
505                cursor_index..cursor_index + 1,
506                RectHeightStyle::Tight,
507                RectWidthStyle::Tight,
508            );
509            if let Some(cursor_rect) = cursor_rects.first().map(|text| text.rect).or_else(|| {
510                // Show the cursor at the end of the text if possible
511                let text_len = paragraph
512                    .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
513                    .position as usize;
514                let last_rects = paragraph.get_rects_for_range(
515                    (text_len - 1)..text_len,
516                    RectHeightStyle::Tight,
517                    RectWidthStyle::Tight,
518                );
519
520                if let Some(last_rect) = last_rects.first() {
521                    let mut caret = last_rect.rect;
522                    caret.left = caret.right;
523                    Some(caret)
524                } else {
525                    None
526                }
527            }) {
528                let paint_color = self.cursor_style_data.color;
529                match self.cursor_style {
530                    CursorStyle::Underline => {
531                        let thickness = 2.0_f32;
532                        let underline_rect = SkRect::new(
533                            cursor_area.min_x() + cursor_rect.left,
534                            cursor_area.min_y() + cursor_rect.bottom - thickness
535                                + cursor_vertical_offset,
536                            cursor_area.min_x() + cursor_rect.right,
537                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
538                        );
539
540                        let mut paint = Paint::default();
541                        paint.set_anti_alias(true);
542                        paint.set_style(PaintStyle::Fill);
543                        paint.set_color(paint_color);
544
545                        context.canvas.draw_rect(underline_rect, &paint);
546                    }
547                    CursorStyle::Line => {
548                        let cursor_rect = SkRect::new(
549                            cursor_area.min_x() + cursor_rect.left,
550                            cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
551                            cursor_area.min_x() + cursor_rect.left + 2.,
552                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
553                        );
554
555                        let mut paint = Paint::default();
556                        paint.set_anti_alias(true);
557                        paint.set_style(PaintStyle::Fill);
558                        paint.set_color(paint_color);
559
560                        context.canvas.draw_rect(cursor_rect, &paint);
561                    }
562                    _ => {}
563                }
564            }
565        }
566    }
567}
568
569impl From<Paragraph> for Element {
570    fn from(value: Paragraph) -> Self {
571        Element::Element {
572            key: value.key,
573            element: Rc::new(value.element),
574            elements: vec![],
575        }
576    }
577}
578
579impl KeyExt for Paragraph {
580    fn write_key(&mut self) -> &mut DiffKey {
581        &mut self.key
582    }
583}
584
585impl EventHandlersExt for Paragraph {
586    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
587        &mut self.element.event_handlers
588    }
589}
590
591impl MaybeExt for Paragraph {}
592
593impl LayerExt for Paragraph {
594    fn get_layer(&mut self) -> &mut Layer {
595        &mut self.element.relative_layer
596    }
597}
598
599pub struct Paragraph {
600    key: DiffKey,
601    element: ParagraphElement,
602}
603
604impl LayoutExt for Paragraph {
605    fn get_layout(&mut self) -> &mut LayoutData {
606        &mut self.element.layout
607    }
608}
609
610impl ContainerExt for Paragraph {}
611
612impl AccessibilityExt for Paragraph {
613    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
614        &mut self.element.accessibility
615    }
616}
617
618impl TextStyleExt for Paragraph {
619    fn get_text_style_data(&mut self) -> &mut TextStyleData {
620        &mut self.element.text_style_data
621    }
622}
623
624impl Paragraph {
625    pub fn try_downcast(element: &dyn ElementExt) -> Option<ParagraphElement> {
626        (element as &dyn Any)
627            .downcast_ref::<ParagraphElement>()
628            .cloned()
629    }
630
631    pub fn spans_iter(mut self, spans: impl Iterator<Item = Span<'static>>) -> Self {
632        let spans = spans.collect::<Vec<Span>>();
633        // TODO: Accessible paragraphs
634        // self.element.accessibility.builder.set_value(text.clone());
635        self.element.spans.extend(spans);
636        self
637    }
638
639    pub fn span(mut self, span: impl Into<Span<'static>>) -> Self {
640        let span = span.into();
641        // TODO: Accessible paragraphs
642        // self.element.accessibility.builder.set_value(text.clone());
643        self.element.spans.push(span);
644        self
645    }
646
647    pub fn cursor_color(mut self, cursor_color: impl Into<Color>) -> Self {
648        self.element.cursor_style_data.color = cursor_color.into();
649        self
650    }
651
652    pub fn highlight_color(mut self, highlight_color: impl Into<Color>) -> Self {
653        self.element.cursor_style_data.highlight_color = highlight_color.into();
654        self
655    }
656
657    pub fn cursor_style(mut self, cursor_style: impl Into<CursorStyle>) -> Self {
658        self.element.cursor_style = cursor_style.into();
659        self
660    }
661
662    pub fn holder(mut self, holder: ParagraphHolder) -> Self {
663        self.element.sk_paragraph = holder;
664        self
665    }
666
667    pub fn cursor_index(mut self, cursor_index: impl Into<Option<usize>>) -> Self {
668        self.element.cursor_index = cursor_index.into();
669        self
670    }
671
672    pub fn highlights(mut self, highlights: impl Into<Option<Vec<(usize, usize)>>>) -> Self {
673        if let Some(highlights) = highlights.into() {
674            self.element.highlights = highlights;
675        }
676        self
677    }
678
679    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
680        self.element.max_lines = max_lines.into();
681        self
682    }
683
684    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
685        self.element.line_height = line_height.into();
686        self
687    }
688
689    /// Set the cursor mode for the paragraph.
690    /// - `CursorMode::Fit`: cursor/highlights use the paragraph's visible_area. VerticalAlign affects cursor positions.
691    /// - `CursorMode::Expanded`: cursor/highlights use the paragraph's inner_area. VerticalAlign does NOT affect cursor positions.
692    pub fn cursor_mode(mut self, cursor_mode: impl Into<CursorMode>) -> Self {
693        self.element.cursor_mode = cursor_mode.into();
694        self
695    }
696
697    /// Set the vertical alignment for the paragraph text.
698    /// This affects how the text is rendered within the paragraph area, but cursor/highlight behavior
699    /// depends on the `cursor_mode` setting.
700    pub fn vertical_align(mut self, vertical_align: impl Into<VerticalAlign>) -> Self {
701        self.element.vertical_align = vertical_align.into();
702        self
703    }
704}
705
706#[derive(Clone, PartialEq, Hash)]
707pub struct Span<'a> {
708    pub text_style_data: TextStyleData,
709    pub text: Cow<'a, str>,
710}
711
712impl From<&'static str> for Span<'static> {
713    fn from(text: &'static str) -> Self {
714        Span {
715            text_style_data: TextStyleData::default(),
716            text: text.into(),
717        }
718    }
719}
720
721impl From<String> for Span<'static> {
722    fn from(text: String) -> Self {
723        Span {
724            text_style_data: TextStyleData::default(),
725            text: text.into(),
726        }
727    }
728}
729
730impl<'a> Span<'a> {
731    pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
732        Self {
733            text: text.into(),
734            text_style_data: TextStyleData::default(),
735        }
736    }
737}
738
739impl<'a> TextStyleExt for Span<'a> {
740    fn get_text_style_data(&mut self) -> &mut TextStyleData {
741        &mut self.text_style_data
742    }
743}