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#[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 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 pub fn new() -> Ref<Self> {
99 instantiate(Self::construct())
100 }
101
102 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 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 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
180impl 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 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}