freya_core/render/utils/
paragraph.rs

1use freya_engine::prelude::*;
2use freya_native_core::{
3    node::ElementNode,
4    prelude::NodeType,
5    real_dom::NodeImmutable,
6    tags::TagName,
7};
8use torin::prelude::{
9    Alignment,
10    Area,
11    Size2D,
12};
13
14use crate::{
15    dom::DioxusNode,
16    states::{
17        CursorState,
18        FontStyleState,
19        LayoutState,
20    },
21    values::HighlightMode,
22};
23
24pub struct ParagraphData {
25    pub paragraph: Paragraph,
26    pub size: Size2D,
27}
28
29/// Compose a new SkParagraph
30pub fn create_paragraph(
31    node: &DioxusNode,
32    area_size: &Size2D,
33    font_collection: &FontCollection,
34    is_rendering: bool,
35    default_font_family: &[String],
36    scale_factor: f32,
37) -> ParagraphData {
38    let font_style = &*node.get::<FontStyleState>().unwrap();
39
40    let mut paragraph_style = ParagraphStyle::default();
41    paragraph_style.set_text_align(font_style.text_align);
42    paragraph_style.set_max_lines(font_style.max_lines);
43    paragraph_style.set_replace_tab_characters(true);
44    paragraph_style.set_text_height_behavior(font_style.text_height);
45
46    if let Some(ellipsis) = font_style.text_overflow.get_ellipsis() {
47        paragraph_style.set_ellipsis(ellipsis);
48    }
49
50    let mut paragraph_builder = ParagraphBuilder::new(&paragraph_style, font_collection);
51
52    let text_style =
53        font_style.text_style(default_font_family, scale_factor, font_style.text_height);
54    paragraph_builder.push_style(&text_style);
55
56    for text_span in node.children() {
57        if let NodeType::Element(ElementNode {
58            tag: TagName::Text, ..
59        }) = &*text_span.node_type()
60        {
61            let text_nodes = text_span.children();
62            let text_node = *text_nodes.first().unwrap();
63            let text_node_type = &*text_node.node_type();
64            let text_font_style = text_span.get::<FontStyleState>().unwrap();
65            let text_style = text_font_style.text_style(
66                default_font_family,
67                scale_factor,
68                font_style.text_height,
69            );
70            paragraph_builder.push_style(&text_style);
71
72            if let NodeType::Text(text) = text_node_type {
73                paragraph_builder.add_text(text);
74            }
75        }
76    }
77
78    if is_rendering {
79        // This is very tricky, but it works! It allows freya to render the cursor at the end of a line.
80        paragraph_builder.add_text(" ");
81    }
82
83    let mut paragraph = paragraph_builder.build();
84    paragraph.layout(
85        if font_style.max_lines == Some(1) && font_style.text_align == TextAlign::default() {
86            f32::MAX
87        } else {
88            area_size.width + 1.0
89        },
90    );
91
92    let width = match font_style.text_align {
93        TextAlign::Start | TextAlign::Left => paragraph.longest_line(),
94        _ => paragraph.max_width(),
95    };
96
97    ParagraphData {
98        size: Size2D::new(width, paragraph.height()),
99        paragraph,
100    }
101}
102
103pub fn run_cursor_highlights(
104    area: Area,
105    paragraph: &Paragraph,
106    node_ref: &DioxusNode,
107    mut run: impl FnMut(Rect),
108) -> Option<()> {
109    let node_cursor_state = &*node_ref.get::<CursorState>().unwrap();
110
111    let highlights = node_cursor_state.highlights.as_ref()?;
112
113    for (from, to) in highlights.iter() {
114        let (from, to) = {
115            if from < to {
116                (from, to)
117            } else {
118                (to, from)
119            }
120        };
121        let cursor_rects = paragraph.get_rects_for_range(
122            *from..*to,
123            RectHeightStyle::Tight,
124            RectWidthStyle::Tight,
125        );
126
127        for cursor_rect in cursor_rects {
128            let rect = align_highlights_and_cursor_paragraph(
129                node_ref,
130                &area,
131                paragraph,
132                &cursor_rect,
133                None,
134            );
135
136            run(rect)
137        }
138    }
139
140    Some(())
141}
142
143pub fn draw_cursor(
144    area: &Area,
145    paragraph: &Paragraph,
146    canvas: &Canvas,
147    node_ref: &DioxusNode,
148) -> Option<()> {
149    let node_cursor_state = &*node_ref.get::<CursorState>().unwrap();
150
151    let cursor = node_cursor_state.position?;
152    let cursor_color = node_cursor_state.color;
153    let cursor_position = cursor as usize;
154
155    let cursor_rects = paragraph.get_rects_for_range(
156        cursor_position..cursor_position + 1,
157        RectHeightStyle::Tight,
158        RectWidthStyle::Tight,
159    );
160    let cursor_rect = cursor_rects.first()?;
161
162    let rect =
163        align_highlights_and_cursor_paragraph(node_ref, area, paragraph, cursor_rect, Some(1.0));
164
165    let mut paint = Paint::default();
166    paint.set_anti_alias(true);
167    paint.set_style(PaintStyle::Fill);
168    paint.set_color(cursor_color);
169
170    canvas.draw_rect(rect, &paint);
171
172    Some(())
173}
174
175/// Align the Y axis of the highlights and cursor of a paragraph
176pub fn align_highlights_and_cursor_paragraph(
177    node: &DioxusNode,
178    area: &Area,
179    paragraph: &Paragraph,
180    cursor_rect: &TextBox,
181    width: Option<f32>,
182) -> Rect {
183    let cursor_state = node.get::<CursorState>().unwrap();
184
185    let left = area.min_x() + cursor_rect.rect.left;
186    let right = left + width.unwrap_or(cursor_rect.rect.right - cursor_rect.rect.left);
187
188    match cursor_state.highlight_mode {
189        HighlightMode::Fit => {
190            let top = (area.min_y()
191                + align_main_align_paragraph(node, area, paragraph)
192                + cursor_rect.rect.top)
193                .clamp(area.min_y(), area.max_y());
194            let bottom = (top + (cursor_rect.rect.bottom - cursor_rect.rect.top))
195                .clamp(area.min_y(), area.max_y());
196
197            Rect::new(left, top, right, bottom)
198        }
199        HighlightMode::Expanded => {
200            let top = area.min_y();
201            let bottom = area.max_y();
202
203            Rect::new(left, top, right, bottom)
204        }
205    }
206}
207
208/// Align the main alignment of a paragraph
209pub fn align_main_align_paragraph(node: &DioxusNode, area: &Area, paragraph: &Paragraph) -> f32 {
210    let layout = node.get::<LayoutState>().unwrap();
211
212    match layout.main_alignment {
213        Alignment::Start => 0.,
214        Alignment::Center => (area.height() / 2.0) - (paragraph.height() / 2.0),
215        Alignment::End => area.height() - paragraph.height(),
216        Alignment::SpaceBetween => 0.,
217        Alignment::SpaceEvenly => 0.,
218        Alignment::SpaceAround => 0.,
219    }
220}