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