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