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