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