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