Skip to main content

ferroui_base/
combined_geometry.rs

1use crate::media::{Geometry, GeometryImpl, Transform};
2use crate::platform::{self, IGeometryImpl};
3use crate::reactive::IDisposable;
4use crate::{
5    ferro_class, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
6    FerroPropertyChangedEventArgs, Nullable, Ref, StyledProperty,
7};
8use std::cell::RefCell;
9use std::rc::Rc;
10use std::sync::Arc;
11
12/// Specifies the different methods by which two geometries can be combined.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
14#[repr(i32)]
15pub enum GeometryCombineMode {
16    /// The two regions are combined by taking the union of both. The
17    /// resulting geometry is geometry A + geometry B.
18    #[default]
19    Union = 0,
20
21    /// The two regions are combined by taking the intersection of both. The
22    /// new area consists of the overlapping region between the two
23    /// geometries.
24    Intersect = 1,
25
26    /// The two regions are combined by taking the area that exists in the
27    /// first region but not the second and the area that exists in the
28    /// second region but not the first. The new region consists of
29    /// (A-B) + (B-A), where A and B are geometries.
30    Xor = 2,
31
32    /// The second region is excluded from the first. Given two geometries, A
33    /// and B, the area of geometry B is removed from the area of geometry A,
34    /// producing a region that is A-B.
35    Exclude = 3,
36}
37
38/// Represents a 2-D geometric shape defined by the combination of two
39/// geometry objects.
40#[repr(C)]
41pub struct CombinedGeometry {
42    base: Geometry,
43    geometry1_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
44    geometry2_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
45}
46
47ferro_class!(CombinedGeometry: Geometry);
48crate::ferro_class_info!(CombinedGeometry { new: CombinedGeometry::new });
49
50impl FerroObjectImpl for CombinedGeometry {
51    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
52        Self::parent_on_property_changed(this, change);
53
54        let slot = if change.property() == Self::geometry1_property().as_property() {
55            &this.geometry1_subscription
56        } else if change.property() == Self::geometry2_property().as_property() {
57            &this.geometry2_subscription
58        } else {
59            return;
60        };
61
62        if let Some(subscription) = slot.take() {
63            subscription.dispose();
64        }
65
66        if let Some(new_value) = change.get_new_value::<Option<Ref<Geometry>>>() {
67            let weak = this.to_ref().downgrade();
68            let subscription = new_value.changed(move || {
69                if let Some(this) = weak.upgrade() {
70                    this.invalidate_geometry();
71                }
72            });
73            *slot.borrow_mut() = Some(subscription);
74        }
75    }
76}
77
78impl GeometryImpl for CombinedGeometry {
79    fn clone_geometry(this: &Self) -> Ref<Geometry> {
80        CombinedGeometry::with_mode_and_transform(
81            this.geometry_combine_mode(),
82            this.geometry1(),
83            this.geometry2(),
84            this.transform(),
85        )
86        .upcast()
87    }
88
89    fn create_defining_geometry(this: &Self) -> Option<Arc<dyn IGeometryImpl>> {
90        let g1 = this.geometry1().and_then(|g| g.platform_impl());
91        let g2 = this.geometry2().and_then(|g| g.platform_impl());
92
93        if let (Some(g1), Some(g2)) = (&g1, &g2) {
94            let factory = platform::render_interface();
95            return Some(factory.create_combined_geometry(this.geometry_combine_mode(), g1.clone(), g2.clone()));
96        }
97
98        if this.geometry_combine_mode() == GeometryCombineMode::Intersect {
99            return None;
100        }
101
102        g1.or(g2)
103    }
104}
105
106crate::ferro_properties! { impl CombinedGeometry {
107    ferro_property!(pub fn geometry1_property() -> StyledProperty<Option<Ref<Geometry>>> {
108        FerroProperty::register::<CombinedGeometry, _>("Geometry1", None)
109    });
110
111    ferro_property!(pub fn geometry2_property() -> StyledProperty<Option<Ref<Geometry>>> {
112        FerroProperty::register::<CombinedGeometry, _>("Geometry2", None)
113    });
114
115    ferro_property!(pub fn geometry_combine_mode_property() -> StyledProperty<GeometryCombineMode> {
116        FerroProperty::register::<CombinedGeometry, _>("GeometryCombineMode", GeometryCombineMode::Union)
117    });
118} }
119
120impl CombinedGeometry {
121    fn static_constructor() {
122        Geometry::affects_geometry(&[
123            Self::geometry1_property().as_property(),
124            Self::geometry2_property().as_property(),
125            Self::geometry_combine_mode_property().as_property(),
126        ]);
127    }
128
129    /// Creates the class data.
130    pub fn construct() -> Self {
131        Self {
132            base: Geometry::construct(),
133            geometry1_subscription: RefCell::new(None),
134            geometry2_subscription: RefCell::new(None),
135        }
136    }
137
138    pub fn new() -> Ref<Self> {
139        instantiate(Self::construct())
140    }
141
142    /// Creates the union of the two geometries.
143    pub fn with_geometries(geometry1: Ref<Geometry>, geometry2: Ref<Geometry>) -> Ref<Self> {
144        let result = Self::new();
145        result.set_geometry1(geometry1);
146        result.set_geometry2(geometry2);
147        result
148    }
149
150    /// Creates a combination of the two geometries with the given mode.
151    pub fn with_mode(
152        combine_mode: GeometryCombineMode,
153        geometry1: Option<Ref<Geometry>>,
154        geometry2: Option<Ref<Geometry>>,
155    ) -> Ref<Self> {
156        let result = Self::new();
157        result.set_geometry1(geometry1);
158        result.set_geometry2(geometry2);
159        result.set_geometry_combine_mode(combine_mode);
160        result
161    }
162
163    /// Creates a combination of the two geometries with the given mode and
164    /// transform.
165    pub fn with_mode_and_transform(
166        combine_mode: GeometryCombineMode,
167        geometry1: Option<Ref<Geometry>>,
168        geometry2: Option<Ref<Geometry>>,
169        transform: impl Into<Nullable<Transform>>,
170    ) -> Ref<Self> {
171        let result = Self::new();
172        result.set_geometry1(geometry1);
173        result.set_geometry2(geometry2);
174        result.set_geometry_combine_mode(combine_mode);
175        result.set_transform(transform);
176        result
177    }
178
179    /// The first geometry that should be combined.
180    pub fn geometry1(&self) -> Option<Ref<Geometry>> {
181        self.get_value(Self::geometry1_property())
182    }
183
184    pub fn set_geometry1(&self, value: impl Into<Nullable<Geometry>>) {
185        self.set_value(Self::geometry1_property(), value.into().0)
186    }
187
188    /// The second geometry that should be combined.
189    pub fn geometry2(&self) -> Option<Ref<Geometry>> {
190        self.get_value(Self::geometry2_property())
191    }
192
193    pub fn set_geometry2(&self, value: impl Into<Nullable<Geometry>>) {
194        self.set_value(Self::geometry2_property(), value.into().0)
195    }
196
197    /// The method by which the two geometries (specified by the
198    /// [`geometry1`](Self::geometry1) and [`geometry2`](Self::geometry2)
199    /// properties) are combined. The default value is
200    /// [`GeometryCombineMode::Union`].
201    pub fn geometry_combine_mode(&self) -> GeometryCombineMode {
202        self.get_value(Self::geometry_combine_mode_property())
203    }
204
205    pub fn set_geometry_combine_mode(&self, value: GeometryCombineMode) {
206        self.set_value(Self::geometry_combine_mode_property(), value)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use std::cell::Cell;
214    use crate::media::{GeometryGroup, PathGeometry};
215
216    #[test]
217    fn child_geometry_change_invalidates() {
218        let child = PathGeometry::new();
219        let target = CombinedGeometry::with_mode(GeometryCombineMode::Xor, Some(child.clone().upcast()), None);
220        let count = Rc::new(Cell::new(0));
221        let c = count.clone();
222        target.changed(move || c.set(c.get() + 1));
223
224        child.set_fill_rule(crate::media::FillRule::NonZero);
225        // PathGeometry.FillRule is not registered with affects_geometry, so
226        // nothing is raised.
227        assert_eq!(0, count.get());
228
229        child.set_figures(None);
230        child.set_figures(Some(crate::media::PathFigures::new()));
231        child.figures().unwrap().add(crate::media::PathFigure::new());
232        assert_eq!(1, count.get());
233
234        target.set_geometry1(None);
235        assert_eq!(2, count.get());
236        child.figures().unwrap().add(crate::media::PathFigure::new());
237        assert_eq!(2, count.get());
238    }
239
240    #[test]
241    fn missing_operands() {
242        let target = CombinedGeometry::new();
243        assert!(target.platform_impl().is_none());
244        target.set_geometry_combine_mode(GeometryCombineMode::Intersect);
245        target.set_geometry1(GeometryGroup::new());
246        assert!(target.platform_impl().is_none());
247        assert_eq!(GeometryCombineMode::Intersect, target.geometry_combine_mode());
248        let clone = target.clone_geometry();
249        assert!(clone.is::<CombinedGeometry>());
250    }
251}