Skip to main content

ferroui_base/media/
geometry.rs

1use crate::media::{
2    CombinedGeometry, GeometryCombineMode, IPen, ImmutableGeometry, IntersectionResult, PlatformGeometry,
3    RectangleGeometry, StreamGeometry, Transform,
4};
5use crate::media::ref_adapter::RefAdapter;
6use crate::platform::IGeometryImpl;
7use crate::reactive::{Disposable, IDisposable};
8use crate::rendering::composition::drawing::{CompositorResourceHolder, ICompositionRenderResource};
9use crate::rendering::composition::generated::ServerCompositionSimpleGeometryProps;
10use crate::rendering::composition::server::{IServerObject, ServerCompositionSimpleGeometry, ServerObjectId};
11use crate::rendering::composition::transport::BatchStreamWriter;
12use crate::rendering::composition::{Compositor, ICompositorSerializable};
13use crate::utilities::{FormatError, HandlerList};
14use crate::{
15    ferro_class, ferro_property, FerroObject, FerroObjectImpl, FerroProperty,
16    FerroPropertyChangedEventArgs, IntoRef, Matrix, Nullable, Point, Rect, Ref, StyledProperty, Upcast,
17};
18use std::cell::{Cell, RefCell};
19use std::rc::Rc;
20use std::sync::Arc;
21
22/// Defines a geometric shape.
23#[repr(C)]
24pub struct Geometry {
25    base: FerroObject,
26    is_dirty: Cell<bool>,
27    can_invalidate: bool,
28    platform_impl: RefCell<Option<Arc<dyn IGeometryImpl>>>,
29    changed: HandlerList<dyn Fn()>,
30    transform_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
31    resource: CompositorResourceHolder,
32}
33
34ferro_class! {
35    Geometry: FerroObject, virtuals GeometryImpl: FerroObjectImpl {
36        /// Clones the geometry.
37        fn clone_geometry(this) -> Ref<Geometry>;
38
39        /// Creates the platform implementation of the geometry, without the
40        /// transform applied.
41        fn create_defining_geometry(this) -> Option<Arc<dyn IGeometryImpl>>;
42    }
43}
44
45crate::ferro_impl_classes!(Geometry: FerroObjectImpl);
46
47impl GeometryImpl for Geometry {
48    fn clone_geometry(_this: &Self) -> Ref<Geometry> {
49        panic!("Geometry is abstract: 'clone_geometry' must be implemented by the deriving class")
50    }
51
52    fn create_defining_geometry(_this: &Self) -> Option<Arc<dyn IGeometryImpl>> {
53        panic!("Geometry is abstract: 'create_defining_geometry' must be implemented by the deriving class")
54    }
55}
56
57crate::ferro_properties! { impl Geometry {
58    ferro_property!(pub fn transform_property() -> StyledProperty<Option<Ref<Transform>>> {
59        FerroProperty::register::<Geometry, _>("Transform", None)
60    });
61} }
62
63impl Geometry {
64    fn static_constructor() {
65        Self::transform_property().changed().add_class_handler::<Geometry>(|x, e| x.transform_property_changed(e));
66    }
67
68    /// Creates the class data of a geometry whose platform implementation is
69    /// created on demand.
70    pub fn construct() -> Self {
71        Self {
72            base: FerroObject::construct(),
73            is_dirty: Cell::new(true),
74            can_invalidate: true,
75            platform_impl: RefCell::new(None),
76            changed: HandlerList::new(),
77            transform_subscription: RefCell::new(None),
78            resource: CompositorResourceHolder::new(),
79        }
80    }
81
82    /// Creates the class data of a geometry with a fixed platform
83    /// implementation, which is never invalidated.
84    pub fn construct_with_impl(platform_impl: Option<Arc<dyn IGeometryImpl>>) -> Self {
85        Self {
86            base: FerroObject::construct(),
87            is_dirty: Cell::new(false),
88            can_invalidate: false,
89            platform_impl: RefCell::new(platform_impl),
90            changed: HandlerList::new(),
91            transform_subscription: RefCell::new(None),
92            resource: CompositorResourceHolder::new(),
93        }
94    }
95
96    /// The number of subscribers to the changes of the geometry (a
97    /// diagnostics member, used by tests to verify that subscriptions are
98    /// released).
99    pub fn changed_subscriber_count(&self) -> usize {
100        self.changed.len()
101    }
102
103    /// Subscribes to changes of the geometry. Disposing the returned handle
104    /// unsubscribes.
105    pub fn changed(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
106        let token = self.changed.add(Rc::new(handler));
107        let weak = self.to_ref().downgrade();
108        Disposable::create(move || {
109            if let Some(this) = weak.upgrade() {
110                this.changed.remove(token);
111            }
112        })
113    }
114
115    fn raise_changed(&self) {
116        if self.changed.is_empty() {
117            return;
118        }
119        for (_, handler) in self.changed.snapshot().iter() {
120            handler();
121        }
122    }
123
124    /// The geometry's bounding rectangle.
125    pub fn bounds(&self) -> Rect {
126        self.platform_impl().map(|g| g.bounds()).unwrap_or_default()
127    }
128
129    /// The platform-specific implementation of the geometry.
130    pub fn platform_impl(&self) -> Option<Arc<dyn IGeometryImpl>> {
131        if self.is_dirty.get() {
132            let mut geometry = self.create_defining_geometry();
133            let transform = self.transform();
134
135            if let (Some(defining), Some(transform)) = (&geometry, &transform) {
136                let value = transform.value();
137                if value != Matrix::IDENTITY {
138                    let transformed: Arc<dyn IGeometryImpl> = defining.with_transform(value);
139                    geometry = Some(transformed);
140                }
141            }
142
143            *self.platform_impl.borrow_mut() = geometry;
144            self.is_dirty.set(false);
145        }
146
147        self.platform_impl.borrow().clone()
148    }
149
150    /// A transform to apply to the geometry.
151    pub fn transform(&self) -> Option<Ref<Transform>> {
152        self.get_value(Self::transform_property())
153    }
154
155    pub fn set_transform(&self, value: impl Into<Nullable<Transform>>) {
156        self.set_value(Self::transform_property(), value.into().0)
157    }
158
159    /// Creates a geometry from a string of path data.
160    pub fn parse(s: &str) -> Result<Ref<Geometry>, FormatError> {
161        Ok(StreamGeometry::parse(s)?.upcast())
162    }
163
164    /// Gets the geometry's bounding rectangle with the specified pen.
165    pub fn get_render_bounds(&self, pen: &dyn IPen) -> Rect {
166        self.platform_impl().map(|g| g.get_render_bounds(Some(pen))).unwrap_or_default()
167    }
168
169    /// Indicates whether the geometry's fill contains the specified point.
170    pub fn fill_contains(&self, point: Point) -> bool {
171        self.platform_impl().is_some_and(|g| g.fill_contains(point))
172    }
173
174    /// Gets the relation between this geometry's fill and another one's.
175    pub fn get_fill_intersection_result(&self, geometry: &Geometry) -> Option<IntersectionResult> {
176        match geometry.platform_impl() {
177            None => Some(IntersectionResult::Empty),
178            Some(other) => self.platform_impl().map(|g| g.get_fill_intersection_result(&*other)),
179        }
180    }
181
182    /// Indicates whether the geometry's stroke contains the specified point.
183    pub fn stroke_contains(&self, pen: &dyn IPen, point: Point) -> bool {
184        self.platform_impl().is_some_and(|g| g.stroke_contains(Some(pen), point))
185    }
186
187    /// Gets a geometry that is the shape defined by the stroke on the
188    /// geometry produced by the specified pen.
189    pub fn get_widened_geometry(&self, pen: &dyn IPen) -> Ref<Geometry> {
190        ImmutableGeometry::new(self.platform_impl().map(|g| g.get_widened_geometry(pen))).upcast()
191    }
192
193    /// Marks the given properties as affecting the geometry's platform
194    /// implementation: after a change to any of them, the implementation is
195    /// recreated and the changed notification is raised.
196    pub fn affects_geometry(properties: &[&'static FerroProperty]) {
197        for property in properties {
198            property.changed().subscribe(Self::affects_geometry_invalidate);
199        }
200    }
201
202    /// Invalidates the platform implementation of the geometry.
203    pub fn invalidate_geometry(&self) {
204        if !self.can_invalidate {
205            return;
206        }
207        self.is_dirty.set(true);
208        *self.platform_impl.borrow_mut() = None;
209        self.register_for_serialization();
210        self.raise_changed();
211    }
212
213    fn transform_property_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
214        let new_value = e.get_new_value::<Option<Ref<Transform>>>();
215
216        if let Some(subscription) = self.transform_subscription.take() {
217            subscription.dispose();
218        }
219
220        if let Some(new_value) = &new_value {
221            let weak = self.to_ref().downgrade();
222            let weak_transform = new_value.downgrade();
223            let subscription = new_value.changed(move || {
224                if let (Some(this), Some(transform)) = (weak.upgrade(), weak_transform.upgrade()) {
225                    this.transform_changed(Some(&transform));
226                }
227            });
228            *self.transform_subscription.borrow_mut() = Some(subscription);
229        }
230
231        self.transform_changed(new_value.as_ref());
232    }
233
234    fn transform_changed(&self, sender: Option<&Ref<Transform>>) {
235        let transform = sender.map(|t| t.value());
236        let current = self.platform_impl.borrow().clone();
237
238        if let Some(current) = current {
239            let replacement: Option<Arc<dyn IGeometryImpl>> = match current.as_transformed_geometry() {
240                Some(t) => match transform {
241                    None => Some(t.source_geometry()),
242                    Some(transform) if transform == Matrix::IDENTITY => Some(t.source_geometry()),
243                    Some(transform) if transform != t.transform() => {
244                        let transformed: Arc<dyn IGeometryImpl> = t.source_geometry().with_transform(transform);
245                        Some(transformed)
246                    }
247                    Some(_) => None,
248                },
249                None => match transform {
250                    Some(transform) if transform != Matrix::IDENTITY => {
251                        let transformed: Arc<dyn IGeometryImpl> = current.with_transform(transform);
252                        Some(transformed)
253                    }
254                    _ => None,
255                },
256            };
257
258            if let Some(replacement) = replacement {
259                *self.platform_impl.borrow_mut() = Some(replacement);
260            }
261        }
262
263        self.register_for_serialization();
264        self.raise_changed();
265    }
266
267    /// The object as something a compositor serializes: the adapter keeps
268    /// the object alive while it is queued.
269    pub(crate) fn as_compositor_serializable(&self) -> Rc<dyn ICompositorSerializable> {
270        Rc::new(RefAdapter(self.to_ref()))
271    }
272
273    fn register_for_serialization(&self) {
274        if self.resource.is_attached() {
275            let serializable = self.as_compositor_serializable();
276            self.resource.register_for_invalidation_on_all_compositors(&serializable);
277        }
278    }
279
280    fn affects_geometry_invalidate(e: &FerroPropertyChangedEventArgs<'_>) {
281        if let Some(control) = e.sender().downcast_ref::<Geometry>() {
282            control.invalidate_geometry();
283        }
284    }
285
286    /// The geometry's total length as if all its contours are placed in a
287    /// straight line.
288    pub fn contour_length(&self) -> f64 {
289        self.platform_impl().map(|g| g.contour_length()).unwrap_or(0.0)
290    }
291
292    /// Combines the two geometries using the specified combine mode and
293    /// applies the specified transform to the resulting geometry.
294    pub fn combine(
295        geometry1: impl IntoRef<Geometry>,
296        geometry2: &Ref<RectangleGeometry>,
297        combine_mode: GeometryCombineMode,
298        transform: impl Into<Nullable<Transform>>,
299    ) -> Ref<Geometry> {
300        CombinedGeometry::with_mode_and_transform(
301            combine_mode,
302            Some(geometry1.into_ref()),
303            Some(geometry2.clone().upcast()),
304            transform,
305        )
306        .upcast()
307    }
308
309    /// Attempts to get the corresponding point at the specified distance.
310    pub fn try_get_point_at_distance(&self, distance: f64) -> Option<Point> {
311        self.platform_impl()?.try_get_point_at_distance(distance)
312    }
313
314    /// Attempts to get the corresponding point and tangent from the
315    /// specified distance along the contour of the geometry.
316    pub fn try_get_point_and_tangent_at_distance(&self, distance: f64) -> Option<(Point, Point)> {
317        self.platform_impl()?.try_get_point_and_tangent_at_distance(distance)
318    }
319
320    /// Attempts to get the corresponding path segment given by the two
321    /// distances specified. Imagine it like snipping a part of the current
322    /// geometry.
323    pub fn try_get_segment(
324        &self,
325        start_distance: f64,
326        stop_distance: f64,
327        start_on_begin_figure: bool,
328    ) -> Option<Ref<Geometry>> {
329        let segment = self.platform_impl()?.try_get_segment(start_distance, stop_distance, start_on_begin_figure)?;
330        Some(PlatformGeometry::new(segment).upcast())
331    }
332
333    #[allow(dead_code)]
334    #[inline]
335    fn object(&self) -> &FerroObject {
336        self.upcast()
337    }
338}
339
340impl ICompositionRenderResource for Geometry {
341    fn add_ref_on_compositor(&self, c: &Rc<Compositor>) {
342        let owner = self.as_compositor_serializable();
343        self.resource.create_or_add_ref(c, Some(owner), |c| {
344            c.create_server_object(|server, _| ServerCompositionSimpleGeometry::new(server) as Rc<dyn IServerObject>)
345        });
346    }
347
348    fn release_on_compositor(&self, c: &Rc<Compositor>) {
349        self.resource.release(c);
350    }
351
352    fn get_for_compositor(&self, c: &Compositor) -> ServerObjectId {
353        self.resource.get_for_compositor(c)
354    }
355}
356
357impl ICompositorSerializable for RefAdapter<Geometry> {
358    fn try_get_server(&self, c: &Compositor) -> Option<ServerObjectId> {
359        self.0.resource.try_get_for_compositor(c)
360    }
361
362    fn serialization_key(&self) -> *const () {
363        RefAdapter::reference_id(self)
364    }
365
366    fn serialize_changes(&self, _c: &Compositor, writer: &mut BatchStreamWriter<'_>) {
367        ServerCompositionSimpleGeometryProps::serialize_all_changes(writer, self.0.platform_impl());
368    }
369}
370
371#[cfg(test)]
372pub(crate) mod tests {
373    use super::*;
374    use crate::media::{RotateTransform, GeometryImplExt};
375    use crate::platform::{IStreamGeometryBuilder, ITransformedGeometryImpl};
376    use crate::{ferro_impl_classes, instantiate};
377
378    /// A platform geometry that does nothing but track transforms.
379    pub(crate) struct MockGeometryImpl;
380
381    impl MockGeometryImpl {
382        pub fn create() -> Arc<dyn IGeometryImpl> {
383            Arc::new(MockGeometryImpl)
384        }
385    }
386
387    pub(crate) struct MockTransformedGeometryImpl {
388        source: Arc<dyn IGeometryImpl>,
389        transform: Matrix,
390    }
391
392    macro_rules! mock_geometry_members {
393        () => {
394            fn bounds(&self) -> Rect {
395                Rect::default()
396            }
397            fn contour_length(&self) -> f64 {
398                0.0
399            }
400            fn get_render_bounds(&self, _pen: Option<&dyn IPen>) -> Rect {
401                Rect::default()
402            }
403            fn get_widened_geometry(&self, _pen: &dyn IPen) -> Arc<dyn IGeometryImpl> {
404                MockGeometryImpl::create()
405            }
406            fn fill_contains(&self, _point: Point) -> bool {
407                false
408            }
409            fn get_fill_intersection_result(&self, _geometry: &dyn IGeometryImpl) -> IntersectionResult {
410                IntersectionResult::Empty
411            }
412            fn intersect(&self, _geometry: &dyn IGeometryImpl) -> Option<Arc<dyn IGeometryImpl>> {
413                None
414            }
415            fn stroke_contains(&self, _pen: Option<&dyn IPen>, _point: Point) -> bool {
416                false
417            }
418            fn try_get_point_at_distance(&self, _distance: f64) -> Option<Point> {
419                None
420            }
421            fn try_get_point_and_tangent_at_distance(&self, _distance: f64) -> Option<(Point, Point)> {
422                None
423            }
424            fn as_any(&self) -> &dyn std::any::Any {
425                self
426            }
427            fn try_get_segment(&self, _start: f64, _stop: f64, _begin: bool) -> Option<Arc<dyn IGeometryImpl>> {
428                None
429            }
430        };
431    }
432    pub(crate) use mock_geometry_members;
433
434    impl IGeometryImpl for MockGeometryImpl {
435        mock_geometry_members!();
436
437        fn with_transform(&self, transform: Matrix) -> Arc<dyn ITransformedGeometryImpl> {
438            // The source of a transformed mock is a fresh mock: the tests
439            // only look at the kind of the implementation.
440            Arc::new(MockTransformedGeometryImpl { source: MockGeometryImpl::create(), transform })
441        }
442    }
443
444    impl IGeometryImpl for MockTransformedGeometryImpl {
445        mock_geometry_members!();
446
447        fn with_transform(&self, transform: Matrix) -> Arc<dyn ITransformedGeometryImpl> {
448            Arc::new(MockTransformedGeometryImpl { source: self.source.clone(), transform })
449        }
450
451        fn as_transformed_geometry(&self) -> Option<&dyn ITransformedGeometryImpl> {
452            Some(self)
453        }
454    }
455
456    impl ITransformedGeometryImpl for MockTransformedGeometryImpl {
457        fn source_geometry(&self) -> Arc<dyn IGeometryImpl> {
458            self.source.clone()
459        }
460
461        fn transform(&self) -> Matrix {
462            self.transform
463        }
464    }
465
466    pub(crate) type CommandLog = Arc<std::sync::Mutex<Vec<String>>>;
467
468    /// A stream geometry builder that records the drawing commands it
469    /// receives.
470    struct MockStreamGeometryBuilder {
471        log: CommandLog,
472    }
473
474    impl crate::platform::IGeometryContext for MockStreamGeometryBuilder {
475        fn arc_to(
476            &mut self,
477            point: Point,
478            size: crate::Size,
479            rotation_angle: f64,
480            is_large_arc: bool,
481            sweep_direction: crate::media::SweepDirection,
482            is_stroked: bool,
483        ) {
484            self.log.lock().unwrap().push(format!(
485                "arc {point} {size} {rotation_angle} {is_large_arc} {sweep_direction:?} {is_stroked}"
486            ));
487        }
488        fn begin_figure(&mut self, start_point: Point, is_filled: bool) {
489            self.log.lock().unwrap().push(format!("begin {start_point} {is_filled}"));
490        }
491        fn cubic_bezier_to(&mut self, p1: Point, p2: Point, p3: Point, is_stroked: bool) {
492            self.log.lock().unwrap().push(format!("cubic {p1} {p2} {p3} {is_stroked}"));
493        }
494        fn quadratic_bezier_to(&mut self, p1: Point, p2: Point, is_stroked: bool) {
495            self.log.lock().unwrap().push(format!("quad {p1} {p2} {is_stroked}"));
496        }
497        fn line_to(&mut self, point: Point, is_stroked: bool) {
498            self.log.lock().unwrap().push(format!("line {point} {is_stroked}"));
499        }
500        fn end_figure(&mut self, is_closed: bool) {
501            self.log.lock().unwrap().push(format!("end {is_closed}"));
502        }
503        fn set_fill_rule(&mut self, fill_rule: crate::media::FillRule) {
504            self.log.lock().unwrap().push(format!("fill {fill_rule:?}"));
505        }
506        fn dispose(&mut self) {
507            self.log.lock().unwrap().push("dispose".to_string());
508        }
509    }
510
511    impl IStreamGeometryBuilder for MockStreamGeometryBuilder {
512        fn to_geometry(&mut self) -> Arc<dyn IGeometryImpl> {
513            MockGeometryImpl::create()
514        }
515    }
516
517    /// A geometry factory that records what it was asked to create.
518    #[derive(Default)]
519    pub(crate) struct MockFactory {
520        pub created: RefCell<Vec<String>>,
521        pub streams: RefCell<Vec<CommandLog>>,
522    }
523
524    impl crate::platform::IPlatformRenderInterface for MockFactory {
525        fn build_glyph_run_geometry(&self, _glyph_run: &crate::media::GlyphRun) -> Arc<dyn IGeometryImpl> {
526            unimplemented!()
527        }
528        fn create_glyph_run(
529            &self,
530            _glyph_typeface: &Rc<crate::media::GlyphTypeface>,
531            _font_rendering_em_size: f64,
532            _glyph_infos: &[crate::media::text_formatting::GlyphInfo],
533            _baseline_origin: Point,
534        ) -> std::sync::Arc<dyn crate::platform::IGlyphRunImpl> {
535            unimplemented!()
536        }
537        fn create_ellipse_geometry(&self, rect: Rect) -> Arc<dyn IGeometryImpl> {
538            self.created.borrow_mut().push(format!("ellipse {rect}"));
539            MockGeometryImpl::create()
540        }
541        fn create_line_geometry(&self, p1: Point, p2: Point) -> Arc<dyn IGeometryImpl> {
542            self.created.borrow_mut().push(format!("line {p1} {p2}"));
543            MockGeometryImpl::create()
544        }
545        fn create_rectangle_geometry(&self, rect: Rect) -> Arc<dyn IGeometryImpl> {
546            self.created.borrow_mut().push(format!("rectangle {rect}"));
547            MockGeometryImpl::create()
548        }
549        fn create_stream_geometry_builder(&self, _source: Option<&dyn IGeometryImpl>) -> Box<dyn IStreamGeometryBuilder> {
550            self.created.borrow_mut().push("stream".to_string());
551            let log = Arc::new(std::sync::Mutex::new(Vec::new()));
552            self.streams.borrow_mut().push(log.clone());
553            Box::new(MockStreamGeometryBuilder { log })
554        }
555        fn create_geometry_group(
556            &self,
557            fill_rule: crate::media::FillRule,
558            children: &[Arc<dyn IGeometryImpl>],
559        ) -> Arc<dyn IGeometryImpl> {
560            self.created.borrow_mut().push(format!("group {fill_rule:?} {}", children.len()));
561            MockGeometryImpl::create()
562        }
563        fn create_combined_geometry(
564            &self,
565            combine_mode: GeometryCombineMode,
566            _g1: Arc<dyn IGeometryImpl>,
567            _g2: Arc<dyn IGeometryImpl>,
568        ) -> Arc<dyn IGeometryImpl> {
569            self.created.borrow_mut().push(format!("combined {combine_mode:?}"));
570            MockGeometryImpl::create()
571        }
572
573        // The remaining members are not exercised by the geometry tests.
574        fn create_render_target_bitmap(
575            &self,
576            _: crate::PixelSize,
577            _: crate::Vector,
578        ) -> std::sync::Arc<dyn crate::platform::IRenderTargetBitmapImpl> {
579            unimplemented!()
580        }
581        fn create_writeable_bitmap(
582            &self,
583            _: crate::PixelSize,
584            _: crate::Vector,
585            _: crate::platform::PixelFormat,
586            _: crate::platform::AlphaFormat,
587        ) -> std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl> {
588            unimplemented!()
589        }
590        fn load_bitmap_from_file(&self, _: &str) -> std::io::Result<std::sync::Arc<crate::platform::SharedBitmapImpl>> {
591            unimplemented!()
592        }
593        fn load_bitmap(&self, _: &mut dyn std::io::Read) -> std::io::Result<std::sync::Arc<crate::platform::SharedBitmapImpl>> {
594            unimplemented!()
595        }
596        fn load_writeable_bitmap_to_width(
597            &self,
598            _: &mut dyn std::io::Read,
599            _: i32,
600            _: crate::media::imaging::BitmapInterpolationMode,
601        ) -> std::io::Result<std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl>> {
602            unimplemented!()
603        }
604        fn load_writeable_bitmap_to_height(
605            &self,
606            _: &mut dyn std::io::Read,
607            _: i32,
608            _: crate::media::imaging::BitmapInterpolationMode,
609        ) -> std::io::Result<std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl>> {
610            unimplemented!()
611        }
612        fn load_writeable_bitmap_from_file(
613            &self,
614            _: &str,
615        ) -> std::io::Result<std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl>> {
616            unimplemented!()
617        }
618        fn load_writeable_bitmap(
619            &self,
620            _: &mut dyn std::io::Read,
621        ) -> std::io::Result<std::sync::Arc<dyn crate::platform::IWriteableBitmapImpl>> {
622            unimplemented!()
623        }
624        fn load_bitmap_to_width(
625            &self,
626            _: &mut dyn std::io::Read,
627            _: i32,
628            _: crate::media::imaging::BitmapInterpolationMode,
629        ) -> std::io::Result<std::sync::Arc<crate::platform::SharedBitmapImpl>> {
630            unimplemented!()
631        }
632        fn load_bitmap_to_height(
633            &self,
634            _: &mut dyn std::io::Read,
635            _: i32,
636            _: crate::media::imaging::BitmapInterpolationMode,
637        ) -> std::io::Result<std::sync::Arc<crate::platform::SharedBitmapImpl>> {
638            unimplemented!()
639        }
640        fn resize_bitmap(
641            &self,
642            _: &dyn crate::platform::IBitmapImpl,
643            _: crate::PixelSize,
644            _: crate::media::imaging::BitmapInterpolationMode,
645        ) -> std::sync::Arc<crate::platform::SharedBitmapImpl> {
646            unimplemented!()
647        }
648        fn load_bitmap_from_pixels(
649            &self,
650            _: crate::platform::PixelFormat,
651            _: crate::platform::AlphaFormat,
652            _: &[u8],
653            _: crate::PixelSize,
654            _: crate::Vector,
655            _: i32,
656        ) -> std::sync::Arc<crate::platform::SharedBitmapImpl> {
657            unimplemented!()
658        }
659        fn create_backend_context(
660            &self,
661            _: Option<Rc<dyn crate::platform::IPlatformGraphicsContext>>,
662        ) -> Rc<dyn crate::platform::IPlatformRenderInterfaceContext> {
663            unimplemented!()
664        }
665        fn supports_individual_round_rects(&self) -> bool {
666            false
667        }
668        fn default_alpha_format(&self) -> crate::platform::AlphaFormat {
669            crate::platform::AlphaFormat::Premul
670        }
671        fn default_pixel_format(&self) -> crate::platform::PixelFormat {
672            crate::platform::PixelFormats::RGBA8888
673        }
674        fn is_supported_bitmap_pixel_format(&self, _: crate::platform::PixelFormat) -> bool {
675            true
676        }
677        fn supports_regions(&self) -> bool {
678            false
679        }
680        fn create_region(&self) -> Rc<dyn crate::platform::IPlatformRenderInterfaceRegion> {
681            unimplemented!()
682        }
683    }
684
685    /// Runs `f` with a mock render interface registered in a locator scope.
686    pub(crate) fn with_mock_render_interface(f: impl FnOnce(&MockFactory)) {
687        let scope = crate::FerroLocator::enter_scope();
688        let factory = Rc::new(MockFactory::default());
689        crate::FerroLocator::current_mutable()
690            .bind::<dyn crate::platform::IPlatformRenderInterface>()
691            .to_constant(factory.clone());
692        f(&factory);
693        scope.dispose();
694    }
695
696    #[repr(C)]
697    struct TestGeometry {
698        base: Geometry,
699    }
700
701    ferro_class!(TestGeometry: Geometry);
702    ferro_impl_classes!(TestGeometry: FerroObjectImpl);
703
704    impl GeometryImpl for TestGeometry {
705        fn clone_geometry(this: &Self) -> Ref<Geometry> {
706            Self::parent_clone_geometry(this)
707        }
708
709        fn create_defining_geometry(_this: &Self) -> Option<Arc<dyn IGeometryImpl>> {
710            Some(MockGeometryImpl::create())
711        }
712    }
713
714    impl TestGeometry {
715        ferro_property!(pub fn foo_property() -> StyledProperty<bool> {
716            FerroProperty::register::<TestGeometry, _>("Foo", false)
717        });
718
719        fn new() -> Ref<Self> {
720            crate::ferro_thread_local! {
721                static DONE: Cell<bool> = const { Cell::new(false) };
722            }
723            if !DONE.replace(true) {
724                Geometry::affects_geometry(&[Self::foo_property().as_property()]);
725            }
726            instantiate(Self { base: Geometry::construct() })
727        }
728
729        fn set_foo(&self, value: bool) {
730            self.set_value(Self::foo_property(), value)
731        }
732    }
733
734    fn is_transformed(target: &TestGeometry) -> bool {
735        assert!(target.to_ref().get_type() == TestGeometry::TYPE);
736        target.platform_impl().unwrap().as_transformed_geometry().is_some()
737    }
738
739    fn track_changed(target: &Geometry) -> Rc<Cell<bool>> {
740        let raised = Rc::new(Cell::new(false));
741        let r = raised.clone();
742        target.changed(move || r.set(true));
743        raised
744    }
745
746    #[test]
747    fn changing_affects_geometry_property_causes_platform_impl_to_be_updated() {
748        let target = TestGeometry::new();
749        let platform_impl = target.platform_impl().unwrap();
750
751        target.set_foo(true);
752
753        assert!(!Arc::ptr_eq(&platform_impl, &target.platform_impl().unwrap()));
754    }
755
756    #[test]
757    fn changing_affects_geometry_property_causes_changed_to_be_raised() {
758        let target = TestGeometry::new();
759        let raised = track_changed(&target);
760
761        target.set_foo(true);
762
763        assert!(raised.get());
764    }
765
766    #[test]
767    fn setting_transform_causes_changed_to_be_raised() {
768        let target = TestGeometry::new();
769        let raised = track_changed(&target);
770
771        target.set_transform(RotateTransform::with_angle(45.0));
772
773        assert!(raised.get());
774    }
775
776    #[test]
777    fn changing_transform_causes_changed_to_be_raised() {
778        let transform = RotateTransform::with_angle(45.0);
779        let target = TestGeometry::new();
780        target.set_transform(&transform);
781        let raised = track_changed(&target);
782
783        transform.set_angle(90.0);
784
785        assert!(raised.get());
786    }
787
788    #[test]
789    fn removing_transform_causes_changed_to_be_raised() {
790        let transform = RotateTransform::with_angle(45.0);
791        let target = TestGeometry::new();
792        target.set_transform(&transform);
793        let raised = track_changed(&target);
794
795        target.set_transform(None);
796
797        assert!(raised.get());
798    }
799
800    #[test]
801    fn transform_produces_transformed_platform_impl() {
802        let target = TestGeometry::new();
803        let rotate = RotateTransform::with_angle(45.0);
804
805        assert!(!is_transformed(&target));
806
807        target.set_transform(&rotate);
808
809        assert!(is_transformed(&target));
810
811        rotate.set_angle(0.0);
812
813        assert!(!is_transformed(&target));
814    }
815
816    #[test]
817    fn geometries_create_their_platform_implementation() {
818        use crate::media::{
819            EllipseGeometry, FillRule, GeometryGroup, LineGeometry, PathGeometry, PolylineGeometry, StreamGeometry,
820        };
821        use crate::platform::IGeometryContext;
822
823        with_mock_render_interface(|factory| {
824            let ellipse = EllipseGeometry::with_rect(Rect::new(1.0, 2.0, 3.0, 4.0));
825            assert!(ellipse.platform_impl().is_some());
826            let ellipse = EllipseGeometry::new();
827            ellipse.set_center(Point::new(10.0, 10.0));
828            ellipse.set_radius_x(2.0);
829            ellipse.set_radius_y(3.0);
830            assert!(ellipse.platform_impl().is_some());
831            let clone = ellipse.clone_geometry().cast::<EllipseGeometry>().unwrap();
832            assert_eq!((Point::new(10.0, 10.0), 2.0, 3.0), (clone.center(), clone.radius_x(), clone.radius_y()));
833
834            let line = LineGeometry::with_points(Point::new(0.0, 0.0), Point::new(5.0, 5.0));
835            assert!(line.platform_impl().is_some());
836            assert!(line.clone_geometry().is::<LineGeometry>());
837
838            let polyline = PolylineGeometry::with_points_and_fill_rule(
839                [Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(1.0, 1.0)],
840                true,
841                FillRule::NonZero,
842            );
843            assert!(polyline.platform_impl().is_some());
844            let clone = polyline.clone_geometry().cast::<PolylineGeometry>().unwrap();
845            assert_eq!((3, true, FillRule::NonZero), (clone.points().len(), clone.is_filled(), clone.fill_rule()));
846
847            let path = PathGeometry::parse("F1 M0,0 L1,1 Q1,2 3,4 Z").unwrap();
848            assert!(path.platform_impl().is_some());
849
850            let group = GeometryGroup::new();
851            group.children().add(line.clone().upcast());
852            group.children().add(GeometryGroup::new().upcast());
853            assert!(group.platform_impl().is_some());
854            let clone = group.clone_geometry().cast::<GeometryGroup>().unwrap();
855            assert_eq!(2, clone.children().len());
856
857            let combined = CombinedGeometry::with_geometries(line.upcast(), ellipse.upcast());
858            assert!(combined.platform_impl().is_some());
859
860            let stream = StreamGeometry::new();
861            {
862                let mut context = stream.open();
863                context.begin_figure(Point::new(1.0, 1.0), true);
864                context.line_to(Point::new(2.0, 2.0), true);
865                context.end_figure(false);
866            }
867            assert!(stream.clone_geometry().is::<StreamGeometry>());
868            let parsed = StreamGeometry::parse("M1,1 L2,2").unwrap();
869            assert!(parsed.platform_impl().is_some());
870            assert!(StreamGeometry::parse("M1,1 X").is_err());
871
872            assert_eq!(
873                vec![
874                    "ellipse 1, 2, 3, 4",
875                    "ellipse 8, 7, 4, 6",
876                    "line 0, 0 5, 5",
877                    "stream",
878                    "stream",
879                    "group EvenOdd 1",
880                    "combined Union",
881                    "stream",
882                    "stream",
883                    "stream",
884                ],
885                *factory.created.borrow()
886            );
887            let streams = factory.streams.borrow();
888            assert_eq!(
889                vec!["fill NonZero", "begin 0, 0 true", "line 1, 0 true", "line 1, 1 true", "end true", "dispose"],
890                *streams[0].lock().unwrap()
891            );
892            assert_eq!(
893                vec![
894                    "fill NonZero",
895                    "begin 0, 0 true",
896                    "line 1, 1 true",
897                    "quad 1, 2 3, 4 true",
898                    "end true",
899                    "dispose"
900                ],
901                *streams[1].lock().unwrap()
902            );
903            assert_eq!(vec!["begin 1, 1 true", "line 2, 2 true", "end false", "dispose"], *streams[2].lock().unwrap());
904            // As upstream, parsing does not dispose its builder.
905            assert_eq!(vec!["begin 1, 1 true", "line 2, 2 true", "end false"], *streams[3].lock().unwrap());
906        });
907    }
908
909    #[test]
910    fn replaced_transform_no_longer_raises_changed() {
911        let transform = RotateTransform::with_angle(45.0);
912        let target = TestGeometry::new();
913        target.set_transform(&transform);
914        target.set_transform(None);
915        let raised = track_changed(&target);
916
917        transform.set_angle(90.0);
918
919        assert!(!raised.get());
920    }
921}