Skip to main content

ferroui_base/media/
path_geometry.rs

1use crate::media::media_collection::track_item_property_changed;
2use crate::media::{
3    FillRule, GeometryImpl, PathFigure, PathFigures, PathMarkupParser, StreamGeometry, StreamGeometryImpl,
4};
5use crate::platform::{self, IGeometryContext, IGeometryImpl, PathGeometryContext};
6use crate::reactive::IDisposable;
7use crate::utilities::FormatError;
8use crate::{
9    ferro_class, ferro_property, instantiate, DirectProperty, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
10    Ref, StyledProperty,
11};
12use std::cell::RefCell;
13use std::fmt;
14use std::rc::Rc;
15use std::sync::Arc;
16
17type FigureSubscriptions = Rc<RefCell<Vec<(Ref<PathFigure>, Rc<dyn IDisposable>)>>>;
18
19/// Represents a complex shape that may be composed of arcs, curves,
20/// ellipses, lines, and rectangles.
21#[repr(C)]
22pub struct PathGeometry {
23    base: StreamGeometry,
24    figures: RefCell<Option<PathFigures>>,
25    figures_observer: RefCell<Option<Rc<dyn IDisposable>>>,
26    figures_properties_observer: RefCell<Option<Rc<dyn IDisposable>>>,
27    figure_subscriptions: FigureSubscriptions,
28}
29
30ferro_class!(PathGeometry: StreamGeometry);
31crate::ferro_class_info!(PathGeometry { new: PathGeometry::new });
32
33impl FerroObjectImpl for PathGeometry {
34    fn constructed(this: &Self) {
35        Self::parent_constructed(this);
36        this.set_figures(Some(PathFigures::new()));
37    }
38}
39
40impl GeometryImpl for PathGeometry {
41    fn create_defining_geometry(this: &Self) -> Option<Arc<dyn IGeometryImpl>> {
42        let figures = this.figures()?;
43
44        let factory = platform::render_interface();
45        let mut builder = factory.create_stream_geometry_builder(None);
46
47        builder.set_fill_rule(this.fill_rule());
48        for f in figures.iter() {
49            f.apply_to(&mut *builder);
50        }
51
52        let geometry = builder.to_geometry();
53        builder.dispose();
54        Some(geometry)
55    }
56}
57
58impl StreamGeometryImpl for PathGeometry {
59    fn open_core(this: &Self) -> Box<dyn IGeometryContext> {
60        Box::new(PathGeometryContext::new(this.to_ref()))
61    }
62}
63
64crate::ferro_properties! { impl PathGeometry {
65    ferro_property!(pub fn figures_property() -> DirectProperty<PathGeometry, Option<PathFigures>> {
66        FerroProperty::register_direct::<PathGeometry, _>(
67            "Figures",
68            |g| g.figures.borrow().clone(),
69            Some(|g, f| g.set_figures(f)),
70            None,
71        )
72    });
73
74    ferro_property!(pub fn fill_rule_property() -> StyledProperty<FillRule> {
75        FerroProperty::register::<PathGeometry, _>("FillRule", FillRule::EvenOdd)
76    });
77} }
78
79impl PathGeometry {
80    fn static_constructor() {
81        Self::figures_property().changed().add_class_handler::<PathGeometry>(|s, e| {
82            s.on_figures_changed(e.get_new_value::<Option<PathFigures>>())
83        });
84    }
85
86    /// Creates the class data.
87    pub fn construct() -> Self {
88        Self {
89            base: StreamGeometry::construct(),
90            figures: RefCell::new(None),
91            figures_observer: RefCell::new(None),
92            figures_properties_observer: RefCell::new(None),
93            figure_subscriptions: Rc::new(RefCell::new(Vec::new())),
94        }
95    }
96
97    /// Creates a path geometry with an empty figures collection.
98    pub fn new() -> Ref<Self> {
99        instantiate(Self::construct())
100    }
101
102    /// Parses the specified path data to a path geometry.
103    pub fn parse(path_data: &str) -> Result<Ref<PathGeometry>, FormatError> {
104        let path_geometry = PathGeometry::new();
105
106        let mut context = PathGeometryContext::new(path_geometry.clone());
107        let result = PathMarkupParser::new(&mut context).parse(path_data);
108        context.dispose();
109        result?;
110
111        Ok(path_geometry)
112    }
113
114    /// The figures.
115    pub fn figures(&self) -> Option<PathFigures> {
116        self.figures.borrow().clone()
117    }
118
119    pub fn set_figures(&self, value: Option<PathFigures>) {
120        self.set_and_raise(Self::figures_property(), &self.figures, value);
121    }
122
123    /// How the intersecting areas contained in the path geometry are
124    /// combined. The default value is [`FillRule::EvenOdd`].
125    pub fn fill_rule(&self) -> FillRule {
126        self.get_value(Self::fill_rule_property())
127    }
128
129    pub fn set_fill_rule(&self, value: FillRule) {
130        self.set_value(Self::fill_rule_property(), value)
131    }
132
133    fn on_figures_changed(&self, figures: Option<PathFigures>) {
134        if let Some(observer) = self.figures_observer.take() {
135            observer.dispose();
136        }
137        if let Some(observer) = self.figures_properties_observer.take() {
138            observer.dispose();
139        }
140
141        let Some(figures) = figures else {
142            return;
143        };
144
145        let weak = self.to_ref().downgrade();
146        let invalidate = move || {
147            if let Some(this) = weak.upgrade() {
148                this.invalidate_geometry();
149            }
150        };
151
152        let subscriptions = self.figure_subscriptions.clone();
153        let subscriptions_added = subscriptions.clone();
154        let (invalidate_added, invalidate_removed, invalidate_reset) =
155            (invalidate.clone(), invalidate.clone(), invalidate.clone());
156        let observer = figures.for_each_item(
157            move |_, s: &Ref<PathFigure>| {
158                let from_segments = invalidate_added.clone();
159                let subscription = s.segments_invalidated(from_segments);
160                subscriptions_added.borrow_mut().push((s.clone(), subscription));
161                invalidate_added();
162            },
163            move |_, s: &Ref<PathFigure>| {
164                let position = subscriptions.borrow().iter().position(|(f, _)| f.ptr_eq(s));
165                if let Some(position) = position {
166                    let (_, subscription) = subscriptions.borrow_mut().remove(position);
167                    subscription.dispose();
168                }
169                invalidate_removed();
170            },
171            invalidate_reset,
172        );
173        *self.figures_observer.borrow_mut() = Some(observer);
174
175        let observer = track_item_property_changed(&figures, invalidate);
176        *self.figures_properties_observer.borrow_mut() = Some(observer);
177    }
178}
179
180/// Writes the path markup of the geometry.
181impl fmt::Display for PathGeometry {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        let figures: Vec<String> = match self.figures() {
184            Some(figures) => figures.iter().map(|figure| figure.to_string()).collect(),
185            None => Vec::new(),
186        };
187        write!(f, "{}{}", if self.fill_rule() != FillRule::EvenOdd { "F1 " } else { "" }, figures.join(" "))
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use std::cell::Cell;
195    use crate::media::{ArcSegment, PathSegments, PolyLineSegment};
196    use crate::{Point, Size};
197
198    #[test]
199    fn path_geometry_triggers_invalidation_on_figures_add() {
200        let segment = PolyLineSegment::with_points([Point::new(1.0, 1.0), Point::new(2.0, 2.0)]);
201
202        let figure = PathFigure::new();
203        figure.set_segments(Some(PathSegments::from_items([segment.upcast()])));
204        figure.set_is_closed(false);
205        figure.set_is_filled(false);
206
207        let target = PathGeometry::new();
208        let changed = Rc::new(Cell::new(false));
209        let c = changed.clone();
210        target.changed(move || c.set(true));
211
212        target.figures().unwrap().add(figure);
213        assert!(changed.get());
214    }
215
216    #[test]
217    fn open_adds_to_figures() {
218        crate::media::geometry::tests::with_mock_render_interface(|_| {
219            let target = PathGeometry::new();
220
221            {
222                let mut context = target.open();
223                context.begin_figure(Point::new(0.0, 0.0), true);
224                context.line_to(Point::new(10.0, 0.0), true);
225                context.end_figure(true);
226            }
227
228            assert_eq!(1, target.figures().unwrap().len());
229        });
230    }
231
232    #[test]
233    fn open_adds_to_figures_when_a_transform_is_set() {
234        crate::media::geometry::tests::with_mock_render_interface(|_| {
235            let target = PathGeometry::new();
236            target.set_transform(crate::media::TranslateTransform::with_offset(10.0, 20.0));
237
238            {
239                let mut context = target.open();
240                context.begin_figure(Point::new(0.0, 0.0), true);
241                context.line_to(Point::new(10.0, 0.0), true);
242                context.end_figure(true);
243            }
244
245            assert_eq!(1, target.figures().unwrap().len());
246        });
247    }
248
249    #[test]
250    fn path_segment_triggers_invalidation_on_property_change() {
251        let target_segment = ArcSegment::new();
252        target_segment.set_size(Size::new(10.0, 10.0));
253        target_segment.set_point(Point::new(5.0, 5.0));
254
255        let figure = PathFigure::new();
256        figure.set_is_closed(false);
257        figure.set_segments(Some(PathSegments::from_items([target_segment.clone().upcast()])));
258
259        let target = PathGeometry::new();
260        target.set_figures(Some(PathFigures::from_items([figure])));
261
262        let changed = Rc::new(Cell::new(false));
263        let c = changed.clone();
264        target.changed(move || c.set(true));
265
266        target_segment.set_size(Size::new(20.0, 20.0));
267
268        assert!(changed.get());
269    }
270
271    #[test]
272    fn adding_a_segment_and_changing_a_figure_invalidate() {
273        let target = PathGeometry::parse("M 0,0 L 10,10").unwrap();
274        let count = Rc::new(Cell::new(0));
275        let c = count.clone();
276        target.changed(move || c.set(c.get() + 1));
277
278        let figure = target.figures().unwrap().get(0);
279        figure.segments().unwrap().add(crate::media::LineSegment::new().upcast());
280        assert_eq!(1, count.get());
281
282        figure.set_is_closed(true);
283        assert_eq!(2, count.get());
284
285        target.figures().unwrap().remove_at(0);
286        assert_eq!(3, count.get());
287
288        // A removed figure no longer invalidates the geometry.
289        figure.set_is_closed(false);
290        figure.segments().unwrap().add(crate::media::LineSegment::new().upcast());
291        assert_eq!(3, count.get());
292    }
293
294    #[test]
295    fn to_string_writes_path_markup() {
296        let target = PathGeometry::parse("F1 M10,20 L30,40 C1,2 3,4 5,6 Q1,2 3,4 A10,20 30 1 0 40,50 Z").unwrap();
297        assert_eq!(
298            "F1 M 10, 20 L 30, 40 C 1, 2 3, 4 5, 6 Q 1, 2 3, 4 A 10, 20 30 1 0 40, 50Z",
299            target.to_string()
300        );
301        assert_eq!("", PathGeometry::new().to_string());
302    }
303}