Skip to main content

i_slint_core/items/
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 the builtin Path related items.
6
7When adding an item or a property, it needs to be kept in sync with different place.
8Lookup the [`crate::items`] module documentation.
9*/
10
11use super::{
12    FillRule, Item, ItemConsts, ItemRc, ItemRendererRef, LineCap, LineJoin, RenderingResult,
13};
14use crate::graphics::{Brush, FittedPath, PathData, PathDataIterator};
15use crate::input::{
16    FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
17    KeyEventResult, MouseEvent,
18};
19use crate::item_rendering::CachedRenderingData;
20
21use crate::items::ImageFit;
22use crate::layout::{LayoutInfo, Orientation};
23use crate::lengths::{
24    LogicalBorderRadius, LogicalLength, LogicalPx, LogicalRect, LogicalSize, LogicalVector,
25    RectLengths,
26};
27#[cfg(feature = "rtti")]
28use crate::rtti::*;
29use crate::window::WindowAdapter;
30use crate::{Coord, Property};
31use alloc::boxed::Box;
32use alloc::rc::Rc;
33use const_field_offset::FieldOffsets;
34use core::cell::RefCell;
35use core::pin::Pin;
36use euclid::Point2D;
37use euclid::num::Zero;
38use i_slint_core_macros::*;
39
40/// The implementation of the `Path` element
41#[repr(C)]
42#[derive(FieldOffsets, Default, SlintElement)]
43#[pin]
44pub struct Path {
45    pub elements: Property<PathData>,
46    pub fill: Property<Brush>,
47    pub fill_rule: Property<FillRule>,
48    pub stroke: Property<Brush>,
49    pub stroke_width: Property<LogicalLength>,
50    pub stroke_line_cap: Property<LineCap>,
51    pub stroke_line_join: Property<LineJoin>,
52    pub stroke_miter_limit: Property<f32>,
53    pub viewbox_x: Property<f32>,
54    pub viewbox_y: Property<f32>,
55    pub viewbox_width: Property<f32>,
56    pub viewbox_height: Property<f32>,
57    pub fit: Property<ImageFit>,
58    pub clip: Property<bool>,
59    pub anti_alias: Property<bool>,
60    pub cached_rendering_data: CachedRenderingData,
61    fitted_path: FittedPathBox,
62    tracker: crate::properties::PropertyTracker,
63}
64
65impl Item for Path {
66    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
67
68    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
69
70    fn layout_info(
71        self: Pin<&Self>,
72        _orientation: Orientation,
73        _cross_axis_constraint: Coord,
74        _window_adapter: &Rc<dyn WindowAdapter>,
75        _self_rc: &ItemRc,
76    ) -> LayoutInfo {
77        LayoutInfo { stretch: 1., ..LayoutInfo::default() }
78    }
79
80    fn input_event_filter_before_children(
81        self: Pin<&Self>,
82        _: &MouseEvent,
83        _window_adapter: &Rc<dyn WindowAdapter>,
84        _self_rc: &ItemRc,
85        _: &mut super::MouseCursorInner,
86    ) -> InputEventFilterResult {
87        InputEventFilterResult::ForwardAndIgnore
88    }
89
90    fn input_event(
91        self: Pin<&Self>,
92        _: &MouseEvent,
93        _window_adapter: &Rc<dyn WindowAdapter>,
94        _self_rc: &ItemRc,
95        _: &mut super::MouseCursorInner,
96    ) -> InputEventResult {
97        InputEventResult::EventIgnored
98    }
99
100    fn capture_key_event(
101        self: Pin<&Self>,
102        _: &InternalKeyEvent,
103        _window_adapter: &Rc<dyn WindowAdapter>,
104        _self_rc: &ItemRc,
105    ) -> KeyEventResult {
106        KeyEventResult::EventIgnored
107    }
108
109    fn key_event(
110        self: Pin<&Self>,
111        _: &InternalKeyEvent,
112        _window_adapter: &Rc<dyn WindowAdapter>,
113        _self_rc: &ItemRc,
114    ) -> KeyEventResult {
115        KeyEventResult::EventIgnored
116    }
117
118    fn focus_event(
119        self: Pin<&Self>,
120        _: &FocusEvent,
121        _window_adapter: &Rc<dyn WindowAdapter>,
122        _self_rc: &ItemRc,
123    ) -> FocusEventResult {
124        FocusEventResult::FocusIgnored
125    }
126
127    fn render(
128        self: Pin<&Self>,
129        backend: &mut ItemRendererRef,
130        self_rc: &ItemRc,
131        size: LogicalSize,
132    ) -> RenderingResult {
133        let clip = self.clip();
134        if clip {
135            (*backend).save_state();
136            (*backend).combine_clip(size.into(), LogicalBorderRadius::zero());
137        }
138        (*backend).draw_path(self, self_rc, size);
139        if clip {
140            (*backend).restore_state();
141        }
142        RenderingResult::ContinueRenderingChildren
143    }
144
145    fn bounding_rect(
146        self: core::pin::Pin<&Self>,
147        _window_adapter: &Rc<dyn WindowAdapter>,
148        _self_rc: &ItemRc,
149        geometry: LogicalRect,
150    ) -> LogicalRect {
151        geometry
152    }
153
154    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
155        false
156    }
157}
158
159impl Path {
160    /// Returns an iterator of the events of the path and an offset, so that the
161    /// shape fits into the width/height of the path while respecting the stroke
162    /// width.
163    pub fn fitted_path_events(
164        self: Pin<&Self>,
165        self_rc: &ItemRc,
166    ) -> Option<(LogicalVector, PathDataIterator)> {
167        let mut elements_iter = self.elements().iter()?;
168
169        let fit = self.fit();
170        if fit == ImageFit::Preserve {
171            return (LogicalVector::zero(), elements_iter).into();
172        }
173
174        let stroke_width = self.stroke_width();
175        let geometry = self_rc.geometry();
176        let bounds_width = (geometry.width_length() - stroke_width).max(LogicalLength::zero());
177        let bounds_height = (geometry.height_length() - stroke_width).max(LogicalLength::zero());
178        let offset =
179            LogicalVector::from_lengths(stroke_width / 2 as Coord, stroke_width / 2 as Coord);
180
181        let viewbox_width = self.viewbox_width();
182        let viewbox_height = self.viewbox_height();
183
184        let maybe_viewbox = if viewbox_width > 0. && viewbox_height > 0. {
185            Some(
186                euclid::rect(self.viewbox_x(), self.viewbox_y(), viewbox_width, viewbox_height)
187                    .to_box2d(),
188            )
189        } else {
190            None
191        };
192
193        elements_iter.fit(bounds_width.get() as _, bounds_height.get() as _, maybe_viewbox, fit);
194        (offset, elements_iter).into()
195    }
196
197    fn sample_at(
198        self: Pin<&Self>,
199        self_rc: &ItemRc,
200        t: f32,
201    ) -> Option<(Point2D<f32, LogicalPx>, f32)> {
202        if let Some(new_path) =
203            Path::FIELD_OFFSETS.tracker().apply_pin(self).evaluate_if_dirty(|| {
204                let (offset, elements_iter) = self.fitted_path_events(self_rc)?;
205                Some(elements_iter.to_fitted_path(offset))
206            })
207        {
208            *self.fitted_path.borrow_mut() = new_path;
209        }
210        self.fitted_path.borrow().as_ref()?.sample_at(t)
211    }
212
213    pub fn point_at(self: Pin<&Self>, self_rc: &ItemRc, t: f32) -> Point2D<f32, LogicalPx> {
214        self.sample_at(self_rc, t).map(|(pos, _)| pos).unwrap_or_default()
215    }
216    pub fn angle_at(self: Pin<&Self>, self_rc: &ItemRc, t: f32) -> f32 {
217        self.sample_at(self_rc, t).map(|(_, tangent)| tangent).unwrap_or_default()
218    }
219}
220
221impl ItemConsts for Path {
222    const cached_rendering_data_offset: const_field_offset::FieldOffset<Path, CachedRenderingData> =
223        Path::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
224}
225
226struct FittedPathInner(RefCell<Option<FittedPath>>);
227
228/// Opaque box holding the FittedPath, allocated lazily on first use
229#[repr(C)]
230pub struct FittedPathBox(core::cell::Cell<*mut FittedPathInner>);
231
232impl Default for FittedPathBox {
233    fn default() -> Self {
234        FittedPathBox(core::cell::Cell::new(core::ptr::null_mut()))
235    }
236}
237impl FittedPathBox {
238    fn get_or_init(&self) -> &FittedPathInner {
239        if self.0.get().is_null() {
240            self.0.set(Box::leak(Box::new(FittedPathInner(Default::default()))));
241        }
242        // Safety: the pointer is guaranteed non-null above, and was created from a Box::leak
243        unsafe { &*self.0.get() }
244    }
245}
246impl Drop for FittedPathBox {
247    fn drop(&mut self) {
248        let ptr = self.0.get();
249        if !ptr.is_null() {
250            // Safety: ptr was constructed from a Box::leak in get_or_init
251            drop(unsafe { Box::from_raw(ptr) });
252        }
253    }
254}
255impl core::ops::Deref for FittedPathBox {
256    type Target = RefCell<Option<FittedPath>>;
257    fn deref(&self) -> &Self::Target {
258        &self.get_or_init().0
259    }
260}
261
262/// # Safety
263/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
264/// hold a FittedPathBox
265#[cfg(feature = "ffi")]
266#[unsafe(no_mangle)]
267pub unsafe extern "C" fn slint_path_fitted_cache_init(cache: *mut FittedPathBox) {
268    unsafe { core::ptr::write(cache, FittedPathBox::default()) };
269}
270
271/// # Safety
272/// This must be called using a non-null pointer pointing to an initialized FittedPathBox
273#[cfg(feature = "ffi")]
274#[unsafe(no_mangle)]
275pub unsafe extern "C" fn slint_path_fitted_cache_free(cache: *mut FittedPathBox) {
276    unsafe {
277        core::ptr::drop_in_place(cache);
278    }
279}
280
281#[cfg(feature = "ffi")]
282#[unsafe(no_mangle)]
283pub unsafe extern "C" fn slint_path_point_at(
284    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
285    self_index: u32,
286    t: f32,
287) -> crate::lengths::LogicalPoint {
288    let self_rc = ItemRc::new(self_component.clone(), self_index);
289    self_rc.downcast::<Path>().unwrap().as_pin_ref().point_at(&self_rc, t)
290}
291
292#[cfg(feature = "ffi")]
293#[unsafe(no_mangle)]
294pub unsafe extern "C" fn slint_path_angle_at(
295    self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
296    self_index: u32,
297    t: f32,
298) -> f32 {
299    let self_rc = ItemRc::new(self_component.clone(), self_index);
300    self_rc.downcast::<Path>().unwrap().as_pin_ref().angle_at(&self_rc, t)
301}