Skip to main content

ferroui_base/media/
drawing_brush.rs

1use crate::media::ref_adapter::RefAdapter;
2use crate::media::{BrushImpl, BrushImplExt, Drawing, DrawingContext, ISceneBrushContent, ServerBrushFactory, TileBrush};
3use crate::reactive::IDisposable;
4use crate::rendering::composition::drawing::{
5    CompositionRenderData, CompositionRenderDataSceneBrushContentProperties, RenderDataDrawingContext,
6};
7use crate::rendering::composition::server::ServerCompositionSimpleContentBrush;
8use crate::rendering::composition::transport::BatchStreamWriter;
9use crate::rendering::composition::Compositor;
10use crate::{
11    ferro_class, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
12    FerroPropertyChangedEventArgs, Nullable, Ref, StyledProperty,
13};
14use std::cell::{Cell, RefCell};
15use std::rc::{Rc, Weak};
16
17/// The content of the brush recorded for one compositor.
18struct RenderDataItem {
19    data: Rc<CompositionRenderData>,
20    is_dirty: Cell<bool>,
21}
22
23impl RenderDataItem {
24    fn dispose(&self) {
25        self.data.dispose();
26    }
27}
28
29/// Paints an area with a [`Drawing`].
30#[repr(C)]
31pub struct DrawingBrush {
32    base: TileBrush,
33    drawing_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
34    /// The recorded content per compositor the brush is used with; `None`
35    /// for a compositor the content of which is empty.
36    render_data_dictionary: RefCell<Vec<(Weak<Compositor>, Option<Rc<RenderDataItem>>)>>,
37}
38
39ferro_class!(DrawingBrush: TileBrush);
40crate::ferro_class_info!(DrawingBrush { new: DrawingBrush::new });
41
42impl FerroObjectImpl for DrawingBrush {
43    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
44        // Replacing the drawing, or a structural change inside it (e.g. a replaced brush, pen or geometry) changes
45        // the recorded content; mark it dirty so it's re-recorded. Mutations _within_ a referenced compositor-aware
46        // resource (a brush's color, a geometry's shape) are propagated by the server without re-recording.
47        if change.property() == Self::drawing_property().as_property() {
48            let new_value = change.get_new_value::<Option<Ref<Drawing>>>();
49            if let Some(subscription) = this.drawing_subscription.take() {
50                subscription.dispose();
51            }
52            if let Some(new_value) = new_value {
53                let weak = this.to_ref().downgrade();
54                let subscription = new_value.invalidated(move || {
55                    if let Some(this) = weak.upgrade() {
56                        this.invalidate_content();
57                    }
58                });
59                *this.drawing_subscription.borrow_mut() = Some(subscription);
60            }
61            this.invalidate_content();
62        }
63
64        Self::parent_on_property_changed(this, change);
65    }
66}
67
68impl BrushImpl for DrawingBrush {
69    fn factory(_this: &Self) -> Option<ServerBrushFactory> {
70        Some(|c| ServerCompositionSimpleContentBrush::new(c))
71    }
72
73    fn on_unreferenced_from_compositor(this: &Self, c: &Rc<Compositor>) {
74        let removed = {
75            let mut dictionary = this.render_data_dictionary.borrow_mut();
76            dictionary.iter().position(|(key, _)| is_compositor(key, c)).map(|index| dictionary.remove(index).1)
77        };
78        if let Some(Some(content)) = removed {
79            content.dispose();
80        }
81        Self::parent_on_unreferenced_from_compositor(this, c);
82    }
83
84    /// The first half of the serialization of upstream: records the content
85    /// again when there is none or it is out of date. The render data is a
86    /// new server object, which has to be created before the changes of the
87    /// brush refer to it.
88    fn prepare_serialization(this: &Self, c: &Rc<Compositor>) {
89        Self::parent_prepare_serialization(this, c);
90        // Should always be true here, but just in case do this check
91        if this.is_on_compositor(c) {
92            let data = this.render_data(c);
93            if data.as_ref().is_none_or(|data| data.is_dirty.get()) {
94                let created = this.create_server_content(c);
95                // Dispose the old render list _after_ creating a new one to avoid unnecessary detach/attach
96                // sequence for referenced resources
97                if let Some(data) = data {
98                    data.dispose();
99                }
100                this.set_render_data(c, created);
101            }
102        }
103    }
104
105    fn serialize_changes(this: &Self, c: &Compositor, writer: &mut BatchStreamWriter<'_>) {
106        Self::parent_serialize_changes(this, c, writer);
107
108        let mut content = None;
109        // Should always be true here, but just in case do this check
110        if this.is_on_compositor(c) {
111            if let Some(data) = this.render_data(c) {
112                content = Some((data.data.server(), None, true));
113            }
114        }
115
116        CompositionRenderDataSceneBrushContentProperties::serialize(writer, content);
117    }
118}
119
120fn is_compositor(key: &Weak<Compositor>, c: &Compositor) -> bool {
121    std::ptr::eq(key.as_ptr(), c)
122}
123
124crate::ferro_properties! { impl DrawingBrush {
125    ferro_property!(pub fn drawing_property() -> StyledProperty<Option<Ref<Drawing>>> {
126        FerroProperty::register::<DrawingBrush, _>("Drawing", None)
127    });
128} }
129
130impl DrawingBrush {
131    /// Creates the class data.
132    pub fn construct() -> Self {
133        Self {
134            base: TileBrush::construct(),
135            drawing_subscription: RefCell::new(None),
136            render_data_dictionary: RefCell::new(Vec::new()),
137        }
138    }
139
140    pub fn new() -> Ref<Self> {
141        instantiate(Self::construct())
142    }
143
144    /// Creates a brush that draws `drawing`.
145    pub fn with_drawing(drawing: impl Into<Nullable<Drawing>>) -> Ref<Self> {
146        let result = Self::new();
147        result.set_drawing(drawing);
148        result
149    }
150
151    /// The drawing to draw.
152    pub fn drawing(&self) -> Option<Ref<Drawing>> {
153        self.get_value(Self::drawing_property())
154    }
155
156    pub fn set_drawing(&self, value: impl Into<Nullable<Drawing>>) {
157        self.set_value(Self::drawing_property(), value.into().0)
158    }
159
160    /// Records the current content of the brush: what the drawing draws.
161    /// `None` when there is no drawing or it draws nothing.
162    pub fn create_content(&self) -> Option<Rc<dyn ISceneBrushContent>> {
163        let drawing = self.drawing()?;
164
165        let mut recorder = RenderDataDrawingContext::new(None);
166        {
167            let mut context = DrawingContext::new(&mut recorder);
168            drawing.draw(&mut context);
169        }
170        let content = recorder.get_immediate_scene_brush_content(Rc::new(RefAdapter(self.to_ref())), None, true);
171        recorder.reset();
172        content.map(|content| content as Rc<dyn ISceneBrushContent>)
173    }
174
175    /// The content recorded for compositor `c`, if there is any.
176    fn render_data(&self, c: &Compositor) -> Option<Rc<RenderDataItem>> {
177        self.render_data_dictionary.borrow().iter().find(|(key, _)| is_compositor(key, c)).and_then(|(_, item)| item.clone())
178    }
179
180    fn set_render_data(&self, c: &Rc<Compositor>, item: Option<Rc<RenderDataItem>>) {
181        let mut dictionary = self.render_data_dictionary.borrow_mut();
182        match dictionary.iter_mut().find(|(key, _)| is_compositor(key, c)) {
183            Some(entry) => entry.1 = item,
184            None => dictionary.push((Rc::downgrade(c), item)),
185        }
186    }
187
188    /// Marks the recorded content of the brush as out of date.
189    fn invalidate_content(&self) {
190        for (_, item) in self.render_data_dictionary.borrow().iter() {
191            if let Some(item) = item {
192                item.is_dirty.set(true);
193            }
194        }
195
196        self.register_for_serialization();
197        self.raise_invalidated();
198    }
199
200    fn create_server_content(&self, c: &Rc<Compositor>) -> Option<Rc<RenderDataItem>> {
201        let drawing = self.drawing()?;
202
203        let mut recorder = RenderDataDrawingContext::new(Some(c.clone()));
204        {
205            let mut context = DrawingContext::new(&mut recorder);
206            drawing.draw(&mut context);
207        }
208        let render_data = recorder.get_render_results();
209        recorder.reset();
210        Some(Rc::new(RenderDataItem { data: render_data?, is_dirty: Cell::new(false) }))
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    // Not from upstream: the upstream tests of this class need the compositor.
217    use super::*;
218    use crate::media::{Brushes, GeometryDrawing};
219    use std::cell::Cell;
220
221    #[test]
222    fn drawing_changes_raise_invalidated() {
223        let target = DrawingBrush::new();
224        let raised = Rc::new(Cell::new(0));
225        let r = raised.clone();
226        target.invalidated(move || r.set(r.get() + 1));
227
228        let drawing = GeometryDrawing::new();
229        target.set_drawing(&drawing);
230        assert!(raised.get() >= 1);
231
232        let before = raised.get();
233        drawing.set_brush(Some(Brushes::red()));
234        assert_eq!(before + 1, raised.get());
235
236        target.set_drawing(None);
237        let before = raised.get();
238        drawing.set_brush(Some(Brushes::blue()));
239        assert_eq!(before, raised.get());
240    }
241
242    #[test]
243    fn create_content_records_the_drawing() {
244        use crate::media::{ISceneBrush, RectangleGeometry, Stretch};
245        use crate::rendering::testing::{DrawingLog, MockDrawingContextImpl, MockPlatformRenderInterface};
246        use crate::{Matrix, Rect};
247
248        let (scope, _) = MockPlatformRenderInterface::install();
249        let target = DrawingBrush::new();
250        assert!(target.create_content().is_none());
251
252        let drawing = GeometryDrawing::new();
253        target.set_drawing(&drawing);
254        // A drawing that draws nothing has no content.
255        assert!(target.create_content().is_none());
256
257        drawing.set_brush(Some(Brushes::red()));
258        drawing.set_geometry(RectangleGeometry::with_rect(Rect::new(1.5, 2.5, 10.0, 20.0)).upcast::<crate::media::Geometry>());
259        target.set_stretch(Stretch::Fill);
260        target.set_opacity(0.25);
261
262        // Through the scene brush interface of the class handle.
263        let adapter = RefAdapter(target.clone());
264        let content = ISceneBrush::create_content(&adapter).expect("the drawing draws something");
265        // The bounds of what was drawn, rounded outwards to whole units.
266        assert_eq!(Rect::new(1.0, 2.0, 11.0, 21.0), content.rect());
267        assert!(content.use_scalable_rasterization());
268        assert_eq!(Stretch::Fill, content.brush().stretch());
269        assert_eq!(0.25, content.opacity());
270
271        let log = DrawingLog::new();
272        let mut context = MockDrawingContextImpl::new(log.clone());
273        content.render(&mut context, None);
274        assert_eq!(log.entries(), ["DrawGeometry Red none 1.5, 2.5, 10, 20"]);
275
276        // With a transform: applied around the replay and restored after.
277        log.clear();
278        let transform = Matrix::create_scale(2.0, 2.0);
279        content.render(&mut context, Some(transform));
280        assert_eq!(
281            log.entries(),
282            [
283                format!("SetTransform {transform}"),
284                "DrawGeometry Red none 1.5, 2.5, 10, 20".to_string(),
285                format!("SetTransform {}", Matrix::IDENTITY),
286            ]
287        );
288
289        // Disposed content draws nothing; disposing twice is fine.
290        content.dispose();
291        content.dispose();
292        log.clear();
293        content.render(&mut context, None);
294        assert!(log.entries().is_empty());
295
296        // The content is a snapshot: it is recorded again on every call.
297        drawing.set_brush(Some(Brushes::blue()));
298        let content = target.create_content().unwrap();
299        content.render(&mut context, None);
300        assert_eq!(log.entries(), ["DrawGeometry Blue none 1.5, 2.5, 10, 20"]);
301        scope.dispose();
302    }
303}