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