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