Skip to main content

ferroui_base/media/
drawing.rs

1use crate::media::effects::{Effect, IEffect};
2use crate::media::{DrawingContext, IImage, Transform};
3use crate::reactive::{Disposable, IDisposable};
4use crate::utilities::HandlerList;
5use crate::{
6    ferro_class, FerroObject, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Rect,
7    Ref,
8};
9use std::any::Any;
10use std::cell::RefCell;
11use std::rc::Rc;
12
13type ValueSubscription = (&'static FerroProperty, Rc<dyn IDisposable>);
14
15/// Abstract base class of the objects describing drawn content.
16#[repr(C)]
17pub struct Drawing {
18    base: FerroObject,
19    invalidated: HandlerList<dyn Fn()>,
20    value_subscriptions: RefCell<Vec<ValueSubscription>>,
21}
22
23ferro_class! {
24    Drawing: FerroObject, virtuals DrawingImpl: FerroObjectImpl {
25        /// Draws the content to a drawing context; see [`Drawing::draw`].
26        fn draw_core(this, context: &mut DrawingContext);
27        /// The bounds of the drawn content.
28        fn get_bounds(this) -> Rect;
29    }
30}
31
32impl FerroObjectImpl for Drawing {
33    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
34        Self::parent_on_property_changed(this, change);
35
36        // Every property of a drawing is baked into the recorded drawing
37        // commands or selects which resource is used, so any change requires
38        // a re-record. Keep subscriptions to nested values whose own changes
39        // are also baked in sync.
40        this.update_value_subscription(change.property(), None);
41        this.update_value_subscription(change.property(), Some(change.new_value()));
42        this.raise_invalidated();
43    }
44}
45
46impl DrawingImpl for Drawing {
47    fn draw_core(_this: &Self, _context: &mut DrawingContext) {
48        panic!("Drawing is abstract: 'draw_core' must be implemented by the deriving class")
49    }
50
51    fn get_bounds(_this: &Self) -> Rect {
52        panic!("Drawing is abstract: 'get_bounds' must be implemented by the deriving class")
53    }
54}
55
56impl Drawing {
57    /// Creates the class data; see [`FerroObject::construct`].
58    pub fn construct() -> Self {
59        Self {
60            base: FerroObject::construct(),
61            invalidated: HandlerList::new(),
62            value_subscriptions: RefCell::new(Vec::new()),
63        }
64    }
65
66    /// Draws this drawing to the given drawing context.
67    pub fn draw(&self, context: &mut DrawingContext<'_>) {
68        self.draw_core(context)
69    }
70
71    /// Raised when the drawing changed in a way that requires its content to
72    /// be recorded again.
73    pub(crate) fn invalidated(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
74        let token = self.invalidated.add(Rc::new(handler));
75        let weak = self.to_ref().downgrade();
76        Disposable::create(move || {
77            if let Some(this) = weak.upgrade() {
78                this.invalidated.remove(token);
79            }
80        })
81    }
82
83    pub(crate) fn raise_invalidated(&self) {
84        if self.invalidated.is_empty() {
85            return;
86        }
87        for (_, handler) in self.invalidated.snapshot().iter() {
88            handler();
89        }
90    }
91
92    /// Subscribes to (`Some`) or unsubscribes from (`None`) the changes of
93    /// the value of `property`.
94    fn update_value_subscription(&self, property: &'static FerroProperty, value: Option<&dyn Any>) {
95        let Some(value) = value else {
96            let position = self.value_subscriptions.borrow().iter().position(|(p, _)| *p == property);
97            if let Some(position) = position {
98                let (_, subscription) = self.value_subscriptions.borrow_mut().swap_remove(position);
99                subscription.dispose();
100            }
101            return;
102        };
103
104        let weak = self.to_ref().downgrade();
105        let value_invalidated = move || {
106            if let Some(this) = weak.upgrade() {
107                this.raise_invalidated();
108            }
109        };
110
111        let subscription = if let Some(value) = value.downcast_ref::<Option<Ref<Transform>>>() {
112            // A transform's matrix is baked into the recorded commands, so
113            // its value changes (not just its replacement) require a
114            // re-record.
115            value.as_ref().map(|transform| transform.changed(value_invalidated))
116        } else if let Some(value) = value.downcast_ref::<Option<Rc<dyn IEffect>>>() {
117            // Effects are baked as immutable, and nested image sources (e.g.
118            // a drawing image) are inlined, so their visual changes require a
119            // re-record too.
120            value
121                .as_ref()
122                .and_then(|effect| effect.as_object())
123                .and_then(|object| object.downcast_ref::<Effect>())
124                .map(|effect| effect.invalidated(value_invalidated))
125        } else if let Some(value) = value.downcast_ref::<Option<Rc<dyn IImage>>>() {
126            value
127                .as_ref()
128                .and_then(|image| image.as_affects_render())
129                .map(|affects_render| affects_render.invalidated(Rc::new(value_invalidated)))
130        } else {
131            None
132        };
133
134        if let Some(subscription) = subscription {
135            self.value_subscriptions.borrow_mut().push((property, subscription));
136        }
137    }
138}