Skip to main content

i_slint_core/graphics/
path.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4/*!
5This module contains path related types and functions for the run-time library.
6*/
7
8use crate::debug_log;
9use crate::items::{ImageFit, PathEvent};
10#[cfg(feature = "rtti")]
11use crate::rtti::*;
12use auto_enums::auto_enum;
13use const_field_offset::FieldOffsets;
14use i_slint_core_macros::*;
15
16#[repr(C)]
17#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
18#[pin]
19/// PathMoveTo describes the event of setting the cursor on the path to use as starting
20/// point for sub-sequent events, such as `LineTo`. Moving the cursor also implicitly closes
21/// sub-paths and therefore beings a new sub-path.
22pub struct PathMoveTo {
23    #[rtti_field]
24    /// The x coordinate where the current position should be.
25    pub x: f32,
26    #[rtti_field]
27    /// The y coordinate where the current position should be.
28    pub y: f32,
29}
30
31#[repr(C)]
32#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
33#[pin]
34/// PathLineTo describes the event of moving the cursor on the path to the specified location
35/// along a straight line.
36pub struct PathLineTo {
37    #[rtti_field]
38    /// The x coordinate where the line should go to.
39    pub x: f32,
40    #[rtti_field]
41    /// The y coordinate where the line should go to.
42    pub y: f32,
43}
44
45#[repr(C)]
46#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
47#[pin]
48/// PathArcTo describes the event of moving the cursor on the path across an arc to the specified
49/// x/y coordinates, with the specified x/y radius and additional properties.
50pub struct PathArcTo {
51    #[rtti_field]
52    /// The x coordinate where the arc should end up.
53    pub x: f32,
54    #[rtti_field]
55    /// The y coordinate where the arc should end up.
56    pub y: f32,
57    #[rtti_field]
58    /// The radius on the x-axis of the arc.
59    pub radius_x: f32,
60    #[rtti_field]
61    /// The radius on the y-axis of the arc.
62    pub radius_y: f32,
63    #[rtti_field]
64    /// The rotation along the x-axis of the arc in degrees.
65    pub x_rotation: f32,
66    #[rtti_field]
67    /// large_arc indicates whether to take the long or the shorter path to complete the arc.
68    pub large_arc: bool,
69    #[rtti_field]
70    /// sweep indicates the direction of the arc. If true, a clockwise direction is chosen,
71    /// otherwise counter-clockwise.
72    pub sweep: bool,
73}
74
75#[repr(C)]
76#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
77#[pin]
78/// PathCubicTo describes a smooth Bézier curve from the path's current position
79/// to the specified x/y location, using two control points.
80pub struct PathCubicTo {
81    #[rtti_field]
82    /// The x coordinate of the curve's end point.
83    pub x: f32,
84    #[rtti_field]
85    /// The y coordinate of the curve's end point.
86    pub y: f32,
87    #[rtti_field]
88    /// The x coordinate of the curve's first control point.
89    pub control_1_x: f32,
90    #[rtti_field]
91    /// The y coordinate of the curve's first control point.
92    pub control_1_y: f32,
93    #[rtti_field]
94    /// The x coordinate of the curve's second control point.
95    pub control_2_x: f32,
96    #[rtti_field]
97    /// The y coordinate of the curve's second control point.
98    pub control_2_y: f32,
99}
100
101#[repr(C)]
102#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
103#[pin]
104/// PathCubicTo describes a smooth Bézier curve from the path's current position
105/// to the specified x/y location, using one control points.
106pub struct PathQuadraticTo {
107    #[rtti_field]
108    /// The x coordinate of the curve's end point.
109    pub x: f32,
110    #[rtti_field]
111    /// The y coordinate of the curve's end point.
112    pub y: f32,
113    #[rtti_field]
114    /// The x coordinate of the curve's control point.
115    pub control_x: f32,
116    #[rtti_field]
117    /// The y coordinate of the curve's control point.
118    pub control_y: f32,
119}
120
121#[repr(C)]
122#[derive(Clone, Debug, PartialEq, derive_more::From)]
123/// PathElement describes a single element on a path, such as move-to, line-to, etc.
124pub enum PathElement {
125    /// The MoveTo variant sets the current position on the path.
126    MoveTo(PathMoveTo),
127    /// The LineTo variant describes a line.
128    LineTo(PathLineTo),
129    /// The PathArcTo variant describes an arc.
130    ArcTo(PathArcTo),
131    /// The CubicTo variant describes a Bézier curve with two control points.
132    CubicTo(PathCubicTo),
133    /// The QuadraticTo variant describes a Bézier curve with one control point.
134    QuadraticTo(PathQuadraticTo),
135    /// Indicates that the path should be closed now by connecting to the starting point.
136    Close,
137}
138
139struct ToLyonPathEventIterator<'a> {
140    events_it: core::slice::Iter<'a, PathEvent>,
141    coordinates_it: core::slice::Iter<'a, lyon_path::math::Point>,
142    first: Option<&'a lyon_path::math::Point>,
143    last: Option<&'a lyon_path::math::Point>,
144}
145
146impl Iterator for ToLyonPathEventIterator<'_> {
147    type Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>;
148    fn next(&mut self) -> Option<Self::Item> {
149        use lyon_path::Event;
150
151        self.events_it.next().map(|event| match event {
152            PathEvent::Begin => Event::Begin { at: *self.coordinates_it.next().unwrap() },
153            PathEvent::Line => Event::Line {
154                from: *self.coordinates_it.next().unwrap(),
155                to: *self.coordinates_it.next().unwrap(),
156            },
157            PathEvent::Quadratic => Event::Quadratic {
158                from: *self.coordinates_it.next().unwrap(),
159                ctrl: *self.coordinates_it.next().unwrap(),
160                to: *self.coordinates_it.next().unwrap(),
161            },
162            PathEvent::Cubic => Event::Cubic {
163                from: *self.coordinates_it.next().unwrap(),
164                ctrl1: *self.coordinates_it.next().unwrap(),
165                ctrl2: *self.coordinates_it.next().unwrap(),
166                to: *self.coordinates_it.next().unwrap(),
167            },
168            PathEvent::EndOpen => {
169                Event::End { first: *self.first.unwrap(), last: *self.last.unwrap(), close: false }
170            }
171            PathEvent::EndClosed => {
172                Event::End { first: *self.first.unwrap(), last: *self.last.unwrap(), close: true }
173            }
174        })
175    }
176
177    fn size_hint(&self) -> (usize, Option<usize>) {
178        self.events_it.size_hint()
179    }
180}
181
182impl ExactSizeIterator for ToLyonPathEventIterator<'_> {}
183
184struct TransformedLyonPathIterator<EventIt> {
185    it: EventIt,
186    transform: lyon_path::math::Transform,
187}
188
189impl<EventIt: Iterator<Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>>>
190    Iterator for TransformedLyonPathIterator<EventIt>
191{
192    type Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>;
193    fn next(&mut self) -> Option<Self::Item> {
194        self.it.next().map(|ev| ev.transformed(&self.transform))
195    }
196
197    fn size_hint(&self) -> (usize, Option<usize>) {
198        self.it.size_hint()
199    }
200}
201
202impl<EventIt: Iterator<Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>>>
203    ExactSizeIterator for TransformedLyonPathIterator<EventIt>
204{
205}
206
207/// PathDataIterator is a data structure that acts as starting point for iterating
208/// through the low-level events of a path. If the path was constructed from said
209/// events, then it is a very thin abstraction. If the path was created from higher-level
210/// elements, then an intermediate lyon path is required/built.
211pub struct PathDataIterator {
212    it: LyonPathIteratorVariant,
213    transform: lyon_path::math::Transform,
214}
215
216enum LyonPathIteratorVariant {
217    FromPath(lyon_path::Path),
218    FromEvents(crate::SharedVector<PathEvent>, crate::SharedVector<lyon_path::math::Point>),
219}
220
221impl PathDataIterator {
222    /// Create a new iterator for path traversal.
223    #[auto_enum(Iterator)]
224    pub fn iter(
225        &self,
226    ) -> impl Iterator<Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>> + '_
227    {
228        match &self.it {
229            LyonPathIteratorVariant::FromPath(path) => {
230                TransformedLyonPathIterator { it: path.iter(), transform: self.transform }
231            }
232            LyonPathIteratorVariant::FromEvents(events, coordinates) => {
233                TransformedLyonPathIterator {
234                    it: ToLyonPathEventIterator {
235                        events_it: events.iter(),
236                        coordinates_it: coordinates.iter(),
237                        first: coordinates.first(),
238                        last: coordinates.last(),
239                    },
240                    transform: self.transform,
241                }
242            }
243        }
244    }
245
246    /// Applies a transformation on the elements this iterator provides that tries to fit everything
247    /// into the specified width/height, respecting the provided viewbox. If no viewbox is specified,
248    /// the bounding rectangle of the path is used.
249    pub fn fit(
250        &mut self,
251        width: f32,
252        height: f32,
253        viewbox: Option<lyon_path::math::Box2D>,
254        style: ImageFit,
255    ) {
256        if width > 0. || height > 0. {
257            let fit_style = match style {
258                ImageFit::Contain => lyon_algorithms::fit::FitStyle::Min,
259                ImageFit::Cover => lyon_algorithms::fit::FitStyle::Max,
260                ImageFit::Fill => lyon_algorithms::fit::FitStyle::Stretch,
261                ImageFit::Preserve => return,
262            };
263            let viewbox =
264                viewbox.unwrap_or_else(|| lyon_algorithms::aabb::bounding_box(self.iter()));
265            self.transform = lyon_algorithms::fit::fit_box(
266                &viewbox,
267                &lyon_path::math::Box2D::from_size(lyon_path::math::Size::new(width, height)),
268                fit_style,
269            );
270        }
271    }
272}
273
274#[repr(C)]
275#[derive(Clone, Debug, PartialEq)]
276/// PathData represents a path described by either high-level elements or low-level
277/// events and coordinates.
278#[derive(Default)]
279pub enum PathData {
280    /// None is the variant when the path is empty.
281    #[default]
282    None,
283    /// The Elements variant is used to make a Path from shared arrays of elements.
284    Elements(crate::SharedVector<PathElement>),
285    /// The Events variant describes the path as a series of low-level events and
286    /// associated coordinates.
287    Events(crate::SharedVector<PathEvent>, crate::SharedVector<lyon_path::math::Point>),
288    /// The Commands variant describes the path as a series of SVG encoded path commands.
289    Commands(crate::SharedString),
290}
291
292impl PathData {
293    /// This function returns an iterator that allows traversing the path by means of lyon events.
294    pub fn iter(self) -> Option<PathDataIterator> {
295        PathDataIterator {
296            it: match self {
297                PathData::None => return None,
298                PathData::Elements(elements) => LyonPathIteratorVariant::FromPath(
299                    PathData::build_path(elements.as_slice().iter()),
300                ),
301                PathData::Events(events, coordinates) => {
302                    LyonPathIteratorVariant::FromEvents(events, coordinates)
303                }
304                PathData::Commands(commands) => {
305                    let mut builder = lyon_path::Path::builder();
306                    let mut parser = lyon_extra::parser::PathParser::new();
307                    match parser.parse(
308                        &lyon_extra::parser::ParserOptions::DEFAULT,
309                        &mut lyon_extra::parser::Source::new(commands.chars()),
310                        &mut builder,
311                    ) {
312                        Ok(()) => LyonPathIteratorVariant::FromPath(builder.build()),
313                        Err(e) => {
314                            debug_log!("Error while parsing path commands '{commands}': {e:?}");
315                            LyonPathIteratorVariant::FromPath(Default::default())
316                        }
317                    }
318                }
319            },
320            transform: Default::default(),
321        }
322        .into()
323    }
324
325    fn build_path(element_it: core::slice::Iter<PathElement>) -> lyon_path::Path {
326        use lyon_geom::SvgArc;
327        use lyon_path::ArcFlags;
328        use lyon_path::math::{Angle, Point, Vector};
329        use lyon_path::traits::SvgPathBuilder;
330
331        let mut path_builder = lyon_path::Path::builder().with_svg();
332        for element in element_it {
333            match element {
334                PathElement::MoveTo(PathMoveTo { x, y }) => {
335                    path_builder.move_to(Point::new(*x, *y));
336                }
337                PathElement::LineTo(PathLineTo { x, y }) => {
338                    path_builder.line_to(Point::new(*x, *y));
339                }
340                PathElement::ArcTo(PathArcTo {
341                    x,
342                    y,
343                    radius_x,
344                    radius_y,
345                    x_rotation,
346                    large_arc,
347                    sweep,
348                }) => {
349                    let radii = Vector::new(*radius_x, *radius_y);
350                    let x_rotation = Angle::degrees(*x_rotation);
351                    let flags = ArcFlags { large_arc: *large_arc, sweep: *sweep };
352                    let to = Point::new(*x, *y);
353
354                    let svg_arc = SvgArc {
355                        from: path_builder.current_position(),
356                        radii,
357                        x_rotation,
358                        flags,
359                        to,
360                    };
361
362                    if svg_arc.is_straight_line() {
363                        path_builder.line_to(to);
364                    } else {
365                        path_builder.arc_to(radii, x_rotation, flags, to)
366                    }
367                }
368                PathElement::CubicTo(PathCubicTo {
369                    x,
370                    y,
371                    control_1_x,
372                    control_1_y,
373                    control_2_x,
374                    control_2_y,
375                }) => {
376                    path_builder.cubic_bezier_to(
377                        Point::new(*control_1_x, *control_1_y),
378                        Point::new(*control_2_x, *control_2_y),
379                        Point::new(*x, *y),
380                    );
381                }
382                PathElement::QuadraticTo(PathQuadraticTo { x, y, control_x, control_y }) => {
383                    path_builder.quadratic_bezier_to(
384                        Point::new(*control_x, *control_y),
385                        Point::new(*x, *y),
386                    );
387                }
388                PathElement::Close => path_builder.close(),
389            }
390        }
391
392        path_builder.build()
393    }
394}
395
396#[cfg(not(target_arch = "wasm32"))]
397pub(crate) mod ffi {
398    #![allow(unsafe_code)]
399
400    use super::super::*;
401    use super::*;
402    use core::ffi::c_void;
403
404    #[unsafe(no_mangle)]
405    /// This function is used for the low-level C++ interface to allocate the backing vector for a shared path element array.
406    pub unsafe extern "C" fn slint_new_path_elements(
407        out: *mut c_void,
408        first_element: *const PathElement,
409        count: usize,
410    ) {
411        let arr =
412            crate::SharedVector::from(unsafe { core::slice::from_raw_parts(first_element, count) });
413        unsafe { core::ptr::write(out as *mut crate::SharedVector<PathElement>, arr) };
414    }
415
416    #[unsafe(no_mangle)]
417    /// This function is used for the low-level C++ interface to allocate the backing vector for a shared path event array.
418    pub unsafe extern "C" fn slint_new_path_events(
419        out_events: *mut c_void,
420        out_coordinates: *mut c_void,
421        first_event: *const PathEvent,
422        event_count: usize,
423        first_coordinate: *const Point,
424        coordinate_count: usize,
425    ) {
426        let events = crate::SharedVector::from(unsafe {
427            core::slice::from_raw_parts(first_event, event_count)
428        });
429        unsafe { core::ptr::write(out_events as *mut crate::SharedVector<PathEvent>, events) };
430        let coordinates = crate::SharedVector::from(unsafe {
431            core::slice::from_raw_parts(first_coordinate, coordinate_count)
432        });
433        unsafe {
434            core::ptr::write(out_coordinates as *mut crate::SharedVector<Point>, coordinates)
435        };
436    }
437}