freya_core/elements/
paragraph.rs

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