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