Skip to main content

ferroui_controls/shapes/
shape.rs

1use crate::{Control, ControlImpl};
2use ferroui_base::input::InputElementImpl;
3use ferroui_base::interactivity::InteractiveImpl;
4use ferroui_base::layout::LayoutableImpl;
5use ferroui_base::media::{
6    DrawingContext, Geometry, IBrush, IPen, MatrixTransform, MediaCollection, Pen, PenLineCap, PenLineJoin, Stretch,
7    StrokeDashArray,
8};
9use ferroui_base::reactive::IDisposable;
10use ferroui_base::{
11    ferro_class, ferro_impl_classes, ferro_property, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
12    FerroPropertyChangedEventArgs, Matrix, ObjectType, Rect, Ref, Size, StyledElementImpl, StyledProperty, Upcast,
13    Vector, Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
14};
15use std::cell::{Cell, RefCell};
16use std::rc::Rc;
17
18/// Provides a base class for shape elements, such as `Ellipse`, `Polygon`
19/// and `Rectangle`.
20#[repr(C)]
21pub struct Shape {
22    base: Control,
23    transform: Cell<Matrix>,
24    defining_geometry: RefCell<Option<Ref<Geometry>>>,
25    rendered_geometry: RefCell<Option<Ref<Geometry>>>,
26    stroke_pen: RefCell<Option<Rc<dyn IPen>>>,
27    geometry_changed_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
28}
29
30ferro_class! {
31    Shape: Control, virtuals ShapeImpl: ControlImpl {
32        /// Creates the shape's defining geometry.
33        fn create_defining_geometry(this) -> Option<Ref<Geometry>>;
34
35        /// Called when the underlying [`Geometry`] changed.
36        fn on_geometry_changed(this);
37    }
38}
39
40ferro_impl_classes!(Shape: StyledElementImpl, InteractiveImpl, InputElementImpl, ControlImpl);
41
42impl ShapeImpl for Shape {
43    fn create_defining_geometry(_this: &Self) -> Option<Ref<Geometry>> {
44        panic!("Shape is abstract: 'create_defining_geometry' must be implemented by the deriving class")
45    }
46
47    fn on_geometry_changed(this: &Self) {
48        *this.rendered_geometry.borrow_mut() = None;
49
50        this.invalidate_measure();
51    }
52}
53
54impl FerroObjectImpl for Shape {
55    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
56        Self::parent_on_property_changed(this, change);
57
58        let property = change.property();
59        let is_stroke = property == Self::stroke_property().as_property();
60        let is_stroke_thickness = property == Self::stroke_thickness_property().as_property();
61
62        if is_stroke
63            || is_stroke_thickness
64            || property == Self::stroke_dash_array_property().as_property()
65            || property == Self::stroke_dash_offset_property().as_property()
66            || property == Self::stroke_line_cap_property().as_property()
67            || property == Self::stroke_join_property().as_property()
68            || property == Self::stroke_miter_limit_property().as_property()
69        {
70            if is_stroke || is_stroke_thickness {
71                this.invalidate_measure();
72            }
73
74            // The pen is taken out of its slot while it is updated: updating
75            // a mutable pen raises its change notifications.
76            let mut pen = this.stroke_pen.borrow().clone();
77            let dash_array = this.stroke_dash_array();
78            let changed = Pen::try_modify_or_create(
79                &mut pen,
80                this.stroke(),
81                this.stroke_thickness(),
82                dash_array.as_ref().map(StrokeDashArray::Observable),
83                this.stroke_dash_offset(),
84                this.stroke_line_cap(),
85                this.stroke_join(),
86                this.stroke_miter_limit(),
87            );
88            *this.stroke_pen.borrow_mut() = pen;
89
90            if changed {
91                this.invalidate_visual();
92            }
93        } else if property == Self::fill_property().as_property() {
94            this.invalidate_visual();
95        }
96    }
97}
98
99impl VisualImpl for Shape {
100    fn render(this: &Self, context: &mut DrawingContext) {
101        let geometry = this.rendered_geometry();
102
103        if let Some(geometry) = geometry {
104            let fill = this.fill();
105            let pen = this.stroke_pen.borrow().clone();
106            context.draw_geometry(fill.as_ref(), pen.as_ref(), &geometry);
107        }
108    }
109
110    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
111        Self::parent_on_attached_to_visual_tree(this, e);
112
113        let defining_geometry = this.defining_geometry.borrow().clone();
114        if let Some(defining_geometry) = defining_geometry {
115            this.subscribe_to_geometry(&defining_geometry);
116        }
117    }
118
119    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
120        Self::parent_on_detached_from_visual_tree(this, e);
121
122        if this.defining_geometry.borrow().is_some() {
123            this.unsubscribe_from_geometry();
124        }
125    }
126}
127
128impl LayoutableImpl for Shape {
129    fn measure_override(this: &Self, available_size: Size) -> Size {
130        let Some(defining_geometry) = this.defining_geometry() else {
131            return Size::default();
132        };
133
134        Self::calculate_size_and_transform(available_size, defining_geometry.bounds(), this.stretch()).0
135    }
136
137    fn arrange_override(this: &Self, final_size: Size) -> Size {
138        if let Some(defining_geometry) = this.defining_geometry() {
139            // This should probably use the render bounds for the stroke
140            // thickness, but then the calculations would multiply the stroke
141            // thickness as well, which isn't correct.
142            let (_, transform) =
143                Self::calculate_size_and_transform(final_size, defining_geometry.bounds(), this.stretch());
144
145            if this.transform.get() != transform {
146                this.transform.set(transform);
147                *this.rendered_geometry.borrow_mut() = None;
148            }
149
150            return final_size;
151        }
152
153        Size::default()
154    }
155}
156
157ferroui_base::ferro_properties! { impl Shape {
158    ferro_property!(
159        /// Defines the `Fill` property.
160        pub fn fill_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
161            FerroProperty::register::<Shape, _>("Fill", None)
162        }
163    );
164
165    ferro_property!(
166        /// Defines the `Stretch` property.
167        pub fn stretch_property() -> StyledProperty<Stretch> {
168            FerroProperty::register::<Shape, _>("Stretch", Stretch::None)
169        }
170    );
171
172    ferro_property!(
173        /// Defines the `Stroke` property.
174        pub fn stroke_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
175            FerroProperty::register::<Shape, _>("Stroke", None)
176        }
177    );
178
179    ferro_property!(
180        /// Defines the `StrokeDashArray` property.
181        pub fn stroke_dash_array_property() -> StyledProperty<Option<MediaCollection<f64>>> {
182            FerroProperty::register::<Shape, _>("StrokeDashArray", None)
183        }
184    );
185
186    ferro_property!(
187        /// Defines the `StrokeDashOffset` property.
188        pub fn stroke_dash_offset_property() -> StyledProperty<f64> {
189            FerroProperty::register::<Shape, _>("StrokeDashOffset", 0.0)
190        }
191    );
192
193    ferro_property!(
194        /// Defines the `StrokeThickness` property.
195        pub fn stroke_thickness_property() -> StyledProperty<f64> {
196            FerroProperty::register::<Shape, _>("StrokeThickness", 0.0)
197        }
198    );
199
200    ferro_property!(
201        /// Defines the `StrokeLineCap` property.
202        pub fn stroke_line_cap_property() -> StyledProperty<PenLineCap> {
203            FerroProperty::register::<Shape, _>("StrokeLineCap", PenLineCap::Flat)
204        }
205    );
206
207    ferro_property!(
208        /// Defines the `StrokeJoin` property.
209        pub fn stroke_join_property() -> StyledProperty<PenLineJoin> {
210            FerroProperty::register::<Shape, _>("StrokeJoin", PenLineJoin::Miter)
211        }
212    );
213
214    ferro_property!(
215        /// Defines the `StrokeMiterLimit` property.
216        pub fn stroke_miter_limit_property() -> StyledProperty<f64> {
217            FerroProperty::register::<Shape, _>("StrokeMiterLimit", 10.0)
218        }
219    );
220} }
221
222impl Shape {
223    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
224    pub fn construct() -> Self {
225        Self {
226            base: Control::construct(),
227            transform: Cell::new(Matrix::IDENTITY),
228            defining_geometry: RefCell::new(None),
229            rendered_geometry: RefCell::new(None),
230            stroke_pen: RefCell::new(None),
231            geometry_changed_subscription: RefCell::new(None),
232        }
233    }
234
235    /// Gets a value that represents the [`Geometry`] of the shape.
236    pub fn defining_geometry(&self) -> Option<Ref<Geometry>> {
237        let current = self.defining_geometry.borrow().clone();
238        if current.is_some() {
239            return current;
240        }
241
242        let created = self.create_defining_geometry();
243        *self.defining_geometry.borrow_mut() = created.clone();
244
245        if let Some(geometry) = &created {
246            if self.visual_root().is_some() {
247                self.subscribe_to_geometry(geometry);
248            }
249        }
250
251        created
252    }
253
254    /// Gets a value that represents the final rendered [`Geometry`] of the
255    /// shape.
256    pub fn rendered_geometry(&self) -> Option<Ref<Geometry>> {
257        let current = self.rendered_geometry.borrow().clone();
258        if current.is_some() {
259            return current;
260        }
261
262        let defining_geometry = self.defining_geometry()?;
263        let transform = self.transform.get();
264
265        let rendered = if transform == Matrix::IDENTITY {
266            defining_geometry
267        } else {
268            let rendered = defining_geometry.clone_geometry();
269
270            match rendered.transform().map(|t| t.value()) {
271                Some(value) if value != Matrix::IDENTITY => {
272                    rendered.set_transform(MatrixTransform::with_matrix(value * transform));
273                }
274                _ => rendered.set_transform(MatrixTransform::with_matrix(transform)),
275            }
276
277            rendered
278        };
279
280        *self.rendered_geometry.borrow_mut() = Some(rendered.clone());
281        Some(rendered)
282    }
283
284    /// Gets the brush that specifies how the shape's interior is painted.
285    pub fn fill(&self) -> Option<Rc<dyn IBrush>> {
286        self.get_value(Self::fill_property())
287    }
288
289    /// Sets the brush that specifies how the shape's interior is painted.
290    pub fn set_fill(&self, value: Option<Rc<dyn IBrush>>) {
291        self.set_value(Self::fill_property(), value)
292    }
293
294    /// Gets a [`Stretch`] value that describes how the shape fills its
295    /// allocated space.
296    pub fn stretch(&self) -> Stretch {
297        self.get_value(Self::stretch_property())
298    }
299
300    /// Sets a [`Stretch`] value that describes how the shape fills its
301    /// allocated space.
302    pub fn set_stretch(&self, value: Stretch) {
303        self.set_value(Self::stretch_property(), value)
304    }
305
306    /// Gets the brush that specifies how the shape's outline is painted.
307    pub fn stroke(&self) -> Option<Rc<dyn IBrush>> {
308        self.get_value(Self::stroke_property())
309    }
310
311    /// Sets the brush that specifies how the shape's outline is painted.
312    pub fn set_stroke(&self, value: Option<Rc<dyn IBrush>>) {
313        self.set_value(Self::stroke_property(), value)
314    }
315
316    /// Gets a collection of values that indicate the pattern of dashes and
317    /// gaps that is used to outline shapes.
318    pub fn stroke_dash_array(&self) -> Option<MediaCollection<f64>> {
319        self.get_value(Self::stroke_dash_array_property())
320    }
321
322    /// Sets a collection of values that indicate the pattern of dashes and
323    /// gaps that is used to outline shapes.
324    pub fn set_stroke_dash_array(&self, value: Option<MediaCollection<f64>>) {
325        self.set_value(Self::stroke_dash_array_property(), value)
326    }
327
328    /// Gets a value that specifies the distance within the dash pattern
329    /// where a dash begins.
330    pub fn stroke_dash_offset(&self) -> f64 {
331        self.get_value(Self::stroke_dash_offset_property())
332    }
333
334    /// Sets a value that specifies the distance within the dash pattern
335    /// where a dash begins.
336    pub fn set_stroke_dash_offset(&self, value: f64) {
337        self.set_value(Self::stroke_dash_offset_property(), value)
338    }
339
340    /// Gets the width of the shape outline.
341    pub fn stroke_thickness(&self) -> f64 {
342        self.get_value(Self::stroke_thickness_property())
343    }
344
345    /// Sets the width of the shape outline.
346    pub fn set_stroke_thickness(&self, value: f64) {
347        self.set_value(Self::stroke_thickness_property(), value)
348    }
349
350    /// Gets a [`PenLineCap`] value that describes the shape at the ends of a
351    /// line.
352    pub fn stroke_line_cap(&self) -> PenLineCap {
353        self.get_value(Self::stroke_line_cap_property())
354    }
355
356    /// Sets a [`PenLineCap`] value that describes the shape at the ends of a
357    /// line.
358    pub fn set_stroke_line_cap(&self, value: PenLineCap) {
359        self.set_value(Self::stroke_line_cap_property(), value)
360    }
361
362    /// Gets a [`PenLineJoin`] value that specifies the type of join that is
363    /// used at the vertices of a shape.
364    pub fn stroke_join(&self) -> PenLineJoin {
365        self.get_value(Self::stroke_join_property())
366    }
367
368    /// Sets a [`PenLineJoin`] value that specifies the type of join that is
369    /// used at the vertices of a shape.
370    pub fn set_stroke_join(&self, value: PenLineJoin) {
371        self.set_value(Self::stroke_join_property(), value)
372    }
373
374    /// Gets the limit on the ratio of the miter length to half the
375    /// `StrokeThickness` of the pen.
376    pub fn stroke_miter_limit(&self) -> f64 {
377        self.get_value(Self::stroke_miter_limit_property())
378    }
379
380    /// Sets the limit on the ratio of the miter length to half the
381    /// `StrokeThickness` of the pen.
382    pub fn set_stroke_miter_limit(&self, value: f64) {
383        self.set_value(Self::stroke_miter_limit_property(), value)
384    }
385
386    /// Marks a property as affecting the shape's geometry.
387    ///
388    /// After a call to this method in a control's class initialisation, any
389    /// change to the property on an object of class `T` causes
390    /// [`invalidate_geometry`](Self::invalidate_geometry) to be called on
391    /// the element.
392    pub fn affects_geometry<T: ObjectType + Upcast<Shape>>(properties: &[&'static FerroProperty]) {
393        for property in properties {
394            property.changed().subscribe(|e| {
395                if let Some(sender) = e.sender().downcast_ref::<T>() {
396                    let shape: &Shape = sender.upcast();
397                    Shape::affects_geometry_invalidate(shape, e);
398                }
399            });
400        }
401    }
402
403    /// Invalidates the geometry of this shape.
404    pub fn invalidate_geometry(&self) {
405        if self.defining_geometry.borrow().is_some() {
406            self.unsubscribe_from_geometry();
407        }
408
409        *self.rendered_geometry.borrow_mut() = None;
410        *self.defining_geometry.borrow_mut() = None;
411
412        self.invalidate_measure();
413    }
414
415    /// Calculates the size of a shape with the given bounds in the available
416    /// space, and the transform which stretches the shape to that size.
417    pub(crate) fn calculate_size_and_transform(
418        available_size: Size,
419        shape_bounds: Rect,
420        stretch: Stretch,
421    ) -> (Size, Matrix) {
422        let mut shape_size = Size::new(shape_bounds.right(), shape_bounds.bottom());
423        let mut translate = Matrix::IDENTITY;
424        let mut desired_x = available_size.width;
425        let mut desired_y = available_size.height;
426        let mut sx = 0.0;
427        let mut sy = 0.0;
428
429        if stretch != Stretch::None {
430            shape_size = shape_bounds.size();
431            translate = Matrix::create_translation_vector(-Vector::from(shape_bounds.position()));
432        }
433
434        if available_size.width.is_infinite() {
435            desired_x = shape_size.width;
436        }
437
438        if available_size.height.is_infinite() {
439            desired_y = shape_size.height;
440        }
441
442        if shape_bounds.width > 0.0 {
443            sx = desired_x / shape_size.width;
444        }
445
446        if shape_bounds.height > 0.0 {
447            sy = desired_y / shape_size.height;
448        }
449
450        if available_size.width.is_infinite() {
451            sx = sy;
452        }
453
454        if available_size.height.is_infinite() {
455            sy = sx;
456        }
457
458        match stretch {
459            Stretch::Uniform => {
460                sx = f64::min(sx, sy);
461                sy = sx;
462            }
463            Stretch::UniformToFill => {
464                sx = f64::max(sx, sy);
465                sy = sx;
466            }
467            Stretch::Fill => {
468                if available_size.width.is_infinite() {
469                    sx = 1.0;
470                }
471
472                if available_size.height.is_infinite() {
473                    sy = 1.0;
474                }
475            }
476            Stretch::None => {
477                sx = 1.0;
478                sy = 1.0;
479            }
480        }
481
482        let transform = translate * Matrix::create_scale(sx, sy);
483        let size = Size::new(shape_size.width * sx, shape_size.height * sy);
484        (size, transform)
485    }
486
487    fn affects_geometry_invalidate(control: &Shape, e: &FerroPropertyChangedEventArgs<'_>) {
488        // If the geometry is invalidated when the bounds change, only
489        // invalidate when the size portion changes.
490        if e.property() == Visual::bounds_property().as_property() {
491            let (old_bounds, new_bounds) = e.get_old_and_new_value::<Rect>();
492
493            if old_bounds.size() == new_bounds.size() {
494                return;
495            }
496        }
497
498        control.invalidate_geometry();
499    }
500
501    /// Listens to changes of the defining geometry. At most one subscription
502    /// is kept.
503    fn subscribe_to_geometry(&self, geometry: &Ref<Geometry>) {
504        let weak = self.to_ref().downgrade();
505        let subscription = geometry.changed(move || {
506            if let Some(this) = weak.upgrade() {
507                this.on_geometry_changed();
508            }
509        });
510
511        let previous = self.geometry_changed_subscription.replace(Some(subscription));
512        if let Some(previous) = previous {
513            previous.dispose();
514        }
515    }
516
517    fn unsubscribe_from_geometry(&self) {
518        let subscription = self.geometry_changed_subscription.take();
519        if let Some(subscription) = subscription {
520            subscription.dispose();
521        }
522    }
523}