freya_core/elements/
paragraph.rs

1use std::ops::Mul;
2
3use freya_engine::prelude::*;
4use freya_native_core::{
5    prelude::{
6        ElementNode,
7        NodeType,
8    },
9    real_dom::NodeImmutable,
10    tags::TagName,
11};
12use torin::{
13    geometry::Area,
14    prelude::{
15        AreaModel,
16        CursorPoint,
17        LayoutNode,
18        Length,
19        Size2D,
20    },
21};
22
23use super::utils::ElementUtils;
24use crate::{
25    custom_attributes::CursorLayoutResponse,
26    dom::{
27        DioxusNode,
28        ImagesCache,
29    },
30    event_loop_messages::TextGroupMeasurement,
31    render::{
32        align_main_align_paragraph,
33        create_paragraph,
34        draw_cursor,
35        run_cursor_highlights,
36        ParagraphData,
37    },
38    states::{
39        CursorState,
40        FontStyleState,
41        StyleState,
42    },
43};
44
45pub struct CachedParagraph(pub Paragraph);
46
47/// # Safety
48/// Skia `Paragraph` are neither Sync or Send, but in order to store them in the Associated
49/// data of the Nodes in Torin (which will be used across threads when making the attributes diffing),
50/// we must manually mark the Paragraph as Send and Sync, this is fine because `Paragraph`s will only be accessed and modified
51/// In the main thread when measuring the layout and painting.
52unsafe impl Send for CachedParagraph {}
53unsafe impl Sync for CachedParagraph {}
54
55pub struct ParagraphElement;
56
57impl ParagraphElement {
58    /// Merasure the cursor positio and text selection and notify the subscribed component of the element.
59    pub fn measure_paragraph(
60        node: &DioxusNode,
61        layout_node: &LayoutNode,
62        text_measurement: &TextGroupMeasurement,
63        scale_factor: f64,
64    ) {
65        let paragraph = &layout_node
66            .data
67            .as_ref()
68            .unwrap()
69            .get::<CachedParagraph>()
70            .unwrap()
71            .0;
72
73        let cursor_state = node.get::<CursorState>().unwrap();
74
75        if cursor_state.cursor_id != Some(text_measurement.cursor_id) {
76            return;
77        }
78
79        let y = align_main_align_paragraph(node, &layout_node.area, paragraph);
80
81        if let Some(cursor_reference) = &cursor_state.cursor_ref {
82            if let Some(cursor_position) = text_measurement.cursor_position {
83                let position = CursorPoint::new(cursor_position.x, cursor_position.y - y as f64);
84
85                // Calculate the new cursor position
86                let char_position = paragraph.get_glyph_position_at_coordinate(
87                    position.mul(scale_factor).to_i32().to_tuple(),
88                );
89
90                // Notify the cursor reference listener
91                cursor_reference
92                    .cursor_sender
93                    .send(CursorLayoutResponse::CursorPosition {
94                        position: char_position.position as usize,
95                        id: text_measurement.cursor_id,
96                    })
97                    .ok();
98            }
99
100            if let Some((origin, dist)) = text_measurement.cursor_selection {
101                let origin_position = CursorPoint::new(origin.x, origin.y - y as f64);
102                let dist_position = CursorPoint::new(dist.x, dist.y - y as f64);
103
104                // Calculate the start of the highlighting
105                let origin_char = paragraph.get_glyph_position_at_coordinate(
106                    origin_position.mul(scale_factor).to_i32().to_tuple(),
107                );
108                // Calculate the end of the highlighting
109                let dist_char = paragraph.get_glyph_position_at_coordinate(
110                    dist_position.mul(scale_factor).to_i32().to_tuple(),
111                );
112
113                cursor_reference
114                    .cursor_sender
115                    .send(CursorLayoutResponse::TextSelection {
116                        from: origin_char.position as usize,
117                        to: dist_char.position as usize,
118                        id: text_measurement.cursor_id,
119                    })
120                    .ok();
121            }
122        }
123    }
124}
125
126impl ElementUtils for ParagraphElement {
127    fn render(
128        self,
129        layout_node: &torin::prelude::LayoutNode,
130        node_ref: &DioxusNode,
131        canvas: &Canvas,
132        font_collection: &mut FontCollection,
133        _font_manager: &FontMgr,
134        default_fonts: &[String],
135        _images_cache: &mut ImagesCache,
136        scale_factor: f32,
137    ) {
138        let area = layout_node.visible_area();
139        let node_cursor_state = &*node_ref.get::<CursorState>().unwrap();
140
141        let paint = |paragraph: &Paragraph| {
142            let x = area.min_x();
143            let y = area.min_y() + align_main_align_paragraph(node_ref, &area, paragraph);
144
145            let mut highlights_paint = Paint::default();
146            highlights_paint.set_anti_alias(true);
147            highlights_paint.set_style(PaintStyle::Fill);
148            highlights_paint.set_color(node_cursor_state.highlight_color);
149
150            // Draw the highlights if specified
151            run_cursor_highlights(area, paragraph, node_ref, |rect| {
152                canvas.draw_rect(rect, &highlights_paint);
153            });
154
155            // Draw a cursor if specified
156            draw_cursor(&area, paragraph, canvas, node_ref);
157
158            paragraph.paint(canvas, (x, y));
159        };
160
161        if node_cursor_state.position.is_some() {
162            let ParagraphData { paragraph, .. } = create_paragraph(
163                node_ref,
164                &area.size,
165                font_collection,
166                true,
167                default_fonts,
168                scale_factor,
169            );
170            paint(&paragraph);
171        } else {
172            let paragraph = &layout_node
173                .data
174                .as_ref()
175                .unwrap()
176                .get::<CachedParagraph>()
177                .unwrap()
178                .0;
179            paint(paragraph);
180        };
181    }
182
183    fn clip(
184        &self,
185        layout_node: &LayoutNode,
186        _node_ref: &DioxusNode,
187        canvas: &Canvas,
188        _scale_factor: f32,
189    ) {
190        canvas.clip_rect(
191            Rect::new(
192                layout_node.area.min_x(),
193                layout_node.area.min_y(),
194                layout_node.area.max_x(),
195                layout_node.area.max_y(),
196            ),
197            ClipOp::Intersect,
198            true,
199        );
200    }
201
202    fn element_needs_cached_area(&self, node_ref: &DioxusNode, _style_state: &StyleState) -> bool {
203        let node_cursor_state = &*node_ref.get::<CursorState>().unwrap();
204
205        if node_cursor_state.highlights.is_some() {
206            return true;
207        }
208
209        for text_span in node_ref.children() {
210            if let NodeType::Element(ElementNode {
211                tag: TagName::Text, ..
212            }) = &*text_span.node_type()
213            {
214                let font_style = text_span.get::<FontStyleState>().unwrap();
215
216                if !font_style.text_shadows.is_empty() {
217                    return true;
218                }
219            }
220        }
221
222        false
223    }
224
225    fn element_drawing_area(
226        &self,
227        layout_node: &LayoutNode,
228        node_ref: &DioxusNode,
229        scale_factor: f32,
230        _node_style: &StyleState,
231    ) -> Area {
232        let mut area = layout_node.visible_area();
233
234        // Iterate over all the text spans inside this paragraph and if any of them
235        // has a shadow at all, apply this shadow to the general paragraph.
236        // Is this fully correct? Not really.
237        // Best thing would be to know if any of these text spans withs shadow actually increase
238        // the paragraph area, but I honestly don't know how to properly know the layout of X
239        // text span with shadow.
240        // Therefore I simply assume that the shadow of any text span is referring to the paragraph.
241        // Better to have a big dirty area rather than smaller than what is supposed to be rendered again.
242
243        for text_span in node_ref.children() {
244            if let NodeType::Element(ElementNode {
245                tag: TagName::Text, ..
246            }) = &*text_span.node_type()
247            {
248                let font_style = text_span.get::<FontStyleState>().unwrap();
249
250                let mut text_shadow_area = area;
251
252                for text_shadow in font_style.text_shadows.iter() {
253                    if text_shadow.color != Color::TRANSPARENT {
254                        text_shadow_area.move_with_offsets(
255                            &Length::new(text_shadow.offset.x),
256                            &Length::new(text_shadow.offset.y),
257                        );
258
259                        let expanded_size = text_shadow.blur_sigma.ceil() as f32 * scale_factor;
260
261                        text_shadow_area.expand(&Size2D::new(expanded_size, expanded_size))
262                    }
263                }
264
265                area = area.union(&text_shadow_area);
266            }
267        }
268
269        let paragraph = &layout_node
270            .data
271            .as_ref()
272            .unwrap()
273            .get::<CachedParagraph>()
274            .unwrap()
275            .0;
276
277        run_cursor_highlights(area, paragraph, node_ref, |rect| {
278            area = area.union(&Area::new(
279                (rect.left, rect.top).into(),
280                (rect.width(), rect.height()).into(),
281            ))
282        });
283
284        area
285    }
286}