Skip to main content

ferroui_base/media/
drawing_group.rs

1use crate::collections::NotifyCollectionChangedAction;
2use crate::media::effects::{EffectExtensions, IEffect};
3use crate::media::imaging::BitmapBlendingMode;
4use crate::media::{
5    BoxShadows, Drawing, DrawingCollection, DrawingContext, DrawingImpl, Geometry, GeometryDrawing, GlyphRun,
6    GlyphRunDrawing, IBrush, IDrawingContextCore, IPen, MatrixTransform, PlatformGeometry, PushedState,
7    RectangleGeometry, RenderOptions, TextOptions, Transform,
8};
9use crate::platform::{self, IGeometryImpl, IPlatformRenderInterface};
10use crate::reactive::IDisposable;
11use crate::rendering::scene_graph::ICustomDrawOperation;
12use crate::{
13    ferro_class, ferro_property, instantiate, DirectProperty, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
14    Matrix, Nullable, Point, Rect, Ref, RoundedRect, StyledProperty,
15};
16use std::cell::{Cell, RefCell};
17use std::rc::Rc;
18use std::sync::Arc;
19
20type ChildSubscription = (Ref<Drawing>, Rc<dyn IDisposable>);
21
22/// Represents a collection of drawings that can be operated upon as a single
23/// drawing.
24#[repr(C)]
25pub struct DrawingGroup {
26    base: Drawing,
27    children: RefCell<DrawingCollection>,
28    children_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
29    child_subscriptions: RefCell<Vec<ChildSubscription>>,
30    render_options: Cell<Option<RenderOptions>>,
31    text_options: Cell<Option<TextOptions>>,
32    effect_bounds: Cell<Option<Rect>>,
33    compositing_layer: Cell<Option<(BitmapBlendingMode, f64, Rect)>>,
34}
35
36ferro_class!(DrawingGroup: Drawing);
37crate::ferro_class_info!(DrawingGroup { new: DrawingGroup::new });
38
39impl FerroObjectImpl for DrawingGroup {
40    fn constructed(this: &Self) {
41        Self::parent_constructed(this);
42        let children = this.children();
43        this.subscribe_collection(&children);
44    }
45}
46
47impl DrawingImpl for DrawingGroup {
48    fn draw_core(this: &Self, context: &mut DrawingContext) {
49        // Compute local bounds from children only when the effect bounds
50        // are not explicitly set. The effect bounds store content bounds
51        // (pre-inflation); pushing the effect handles inflation.
52        let children = this.children();
53        let content_bounds = match this.effect_bounds() {
54            Some(bounds) => bounds,
55            None => {
56                let mut bounds = Rect::default();
57                for drawing in children.iter() {
58                    bounds = bounds.union(drawing.get_bounds());
59                }
60                bounds
61            }
62        };
63
64        let effect = this.effect();
65        let effect_bounds = match &effect {
66            Some(effect) => {
67                content_bounds.inflate_thickness(EffectExtensions::get_effect_output_padding(Some(&**effect)))
68            }
69            None => content_bounds,
70        };
71
72        let s_transform =
73            context.push_transform(this.transform().map(|transform| transform.value()).unwrap_or(Matrix::IDENTITY));
74        // A group recorded for a backend with compositing layers can be drawn into a context
75        // without them (another backend, a recording of its own): the content is then drawn with
76        // the opacity of the layer and without its blend, rather than not at all.
77        let s_compositing = match this.compositing_layer.get() {
78            Some((blend, opacity, bounds)) => context
79                .try_push_compositing_layer(blend, opacity, bounds)
80                .unwrap_or_else(|| context.push_opacity(opacity)),
81            None => PushedState::NONE,
82        };
83        let s_opacity = context.push_opacity(this.opacity());
84        let s_clip = match this.clip_geometry() {
85            Some(clip) => context.push_geometry_clip(&clip),
86            None => PushedState::NONE,
87        };
88        let s_opacity_mask = match this.opacity_mask() {
89            Some(mask) => context.push_opacity_mask(&mask, effect_bounds),
90            None => PushedState::NONE,
91        };
92        let s_render_options = match this.render_options() {
93            Some(render_options) => context.push_render_options(render_options),
94            None => PushedState::NONE,
95        };
96        let s_text_options = match this.text_options() {
97            Some(text_options) => context.push_text_options(text_options),
98            None => PushedState::NONE,
99        };
100        let s_effect = match &effect {
101            Some(effect) => context.push_effect(effect, content_bounds),
102            None => PushedState::NONE,
103        };
104
105        for drawing in children.iter() {
106            drawing.draw(context);
107        }
108
109        context.pop(s_effect);
110        context.pop(s_text_options);
111        context.pop(s_render_options);
112        context.pop(s_opacity_mask);
113        context.pop(s_clip);
114        context.pop(s_opacity);
115        context.pop(s_compositing);
116        context.pop(s_transform);
117    }
118
119    fn get_bounds(this: &Self) -> Rect {
120        let mut rect = Rect::default();
121
122        for drawing in this.children().iter() {
123            rect = rect.union(drawing.get_bounds());
124        }
125
126        // Note: the effect is intentionally not reflected here. The bounds
127        // are the content/geometric bounds only. Effects render additively
128        // outside the content bounds and must not shift the coordinate
129        // origin (e.g. when used inside a drawing image).
130        if let Some(transform) = this.transform() {
131            rect = rect.transform_to_aabb(transform.value());
132        }
133
134        rect
135    }
136}
137
138crate::ferro_properties! { impl DrawingGroup {
139    ferro_property!(
140        /// Defines the `Opacity` property.
141        pub fn opacity_property() -> StyledProperty<f64> {
142            FerroProperty::register::<DrawingGroup, _>("Opacity", 1.0)
143        }
144    );
145
146    ferro_property!(
147        /// Defines the `Transform` property.
148        pub fn transform_property() -> StyledProperty<Option<Ref<Transform>>> {
149            FerroProperty::register::<DrawingGroup, _>("Transform", None)
150        }
151    );
152
153    ferro_property!(
154        /// Defines the `ClipGeometry` property.
155        pub fn clip_geometry_property() -> StyledProperty<Option<Ref<Geometry>>> {
156            FerroProperty::register::<DrawingGroup, _>("ClipGeometry", None)
157        }
158    );
159
160    ferro_property!(
161        /// Defines the `OpacityMask` property.
162        pub fn opacity_mask_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
163            FerroProperty::register::<DrawingGroup, _>("OpacityMask", None)
164        }
165    );
166
167    ferro_property!(
168        /// Defines the `Effect` property.
169        pub fn effect_property() -> StyledProperty<Option<Rc<dyn IEffect>>> {
170            FerroProperty::register::<DrawingGroup, _>("Effect", None)
171        }
172    );
173
174    ferro_property!(
175        /// Defines the `Children` property.
176        pub fn children_property() -> DirectProperty<DrawingGroup, DrawingCollection> {
177            FerroProperty::register_direct::<DrawingGroup, _>(
178                "Children",
179                |o| o.children(),
180                Some(|o, v| o.set_children(v)),
181                DrawingCollection::new(),
182            )
183        }
184    );
185} }
186
187impl DrawingGroup {
188    /// Creates the class data.
189    pub fn construct() -> Self {
190        Self {
191            base: Drawing::construct(),
192            children: RefCell::new(DrawingCollection::new()),
193            children_subscription: RefCell::new(None),
194            child_subscriptions: RefCell::new(Vec::new()),
195            render_options: Cell::new(None),
196            text_options: Cell::new(None),
197            effect_bounds: Cell::new(None),
198            compositing_layer: Cell::new(None),
199        }
200    }
201
202    pub fn new() -> Ref<Self> {
203        instantiate(Self::construct())
204    }
205
206    /// The opacity the children are drawn with.
207    pub fn opacity(&self) -> f64 {
208        self.get_value(Self::opacity_property())
209    }
210
211    pub fn set_opacity(&self, value: f64) {
212        self.set_value(Self::opacity_property(), value)
213    }
214
215    /// The transform applied to the children.
216    pub fn transform(&self) -> Option<Ref<Transform>> {
217        self.get_value(Self::transform_property())
218    }
219
220    pub fn set_transform(&self, value: impl Into<Nullable<Transform>>) {
221        self.set_value(Self::transform_property(), value.into().0)
222    }
223
224    /// The geometry the children are clipped to.
225    pub fn clip_geometry(&self) -> Option<Ref<Geometry>> {
226        self.get_value(Self::clip_geometry_property())
227    }
228
229    pub fn set_clip_geometry(&self, value: impl Into<Nullable<Geometry>>) {
230        self.set_value(Self::clip_geometry_property(), value.into().0)
231    }
232
233    /// The opacity mask the children are drawn through.
234    pub fn opacity_mask(&self) -> Option<Rc<dyn IBrush>> {
235        self.get_value(Self::opacity_mask_property())
236    }
237
238    pub fn set_opacity_mask(&self, value: Option<Rc<dyn IBrush>>) {
239        self.set_value(Self::opacity_mask_property(), value)
240    }
241
242    /// The effect applied to the children.
243    pub fn effect(&self) -> Option<Rc<dyn IEffect>> {
244        self.get_value(Self::effect_property())
245    }
246
247    pub fn set_effect(&self, value: Option<Rc<dyn IEffect>>) {
248        self.set_value(Self::effect_property(), value)
249    }
250
251    /// The render options the children are drawn with. Set by the drawing
252    /// context that records into a group.
253    pub(crate) fn render_options(&self) -> Option<RenderOptions> {
254        self.render_options.get()
255    }
256
257    pub(crate) fn set_render_options(&self, value: Option<RenderOptions>) {
258        self.render_options.set(value)
259    }
260
261    /// The text options the children are drawn with. Set by the drawing
262    /// context that records into a group.
263    pub(crate) fn text_options(&self) -> Option<TextOptions> {
264        self.text_options.get()
265    }
266
267    pub(crate) fn set_text_options(&self, value: Option<TextOptions>) {
268        self.text_options.set(value)
269    }
270
271    /// The bounds of the content the effect applies to; computed from the
272    /// children when not set. Set by the drawing context that records into a
273    /// group.
274    pub(crate) fn effect_bounds(&self) -> Option<Rect> {
275        self.effect_bounds.get()
276    }
277
278    pub(crate) fn set_effect_bounds(&self, value: Option<Rect>) {
279        self.effect_bounds.set(value)
280    }
281
282    /// The collection that contains the child drawings.
283    pub fn children(&self) -> DrawingCollection {
284        self.children.borrow().clone()
285    }
286
287    pub fn set_children(&self, value: DrawingCollection) {
288        if self.children.borrow().ptr_eq(&value) {
289            return;
290        }
291
292        if let Some(subscription) = self.children_subscription.take() {
293            subscription.dispose();
294        }
295        let old_subscriptions = std::mem::take(&mut *self.child_subscriptions.borrow_mut());
296        for (_, subscription) in old_subscriptions {
297            subscription.dispose();
298        }
299
300        self.set_and_raise(Self::children_property(), &self.children, value.clone());
301
302        self.subscribe_collection(&value);
303        for child in value.iter() {
304            self.subscribe_child(&child);
305        }
306    }
307
308    /// Opens the group for populating: what is drawn on the returned
309    /// context replaces the children of the group when the context is
310    /// disposed (or dropped).
311    ///
312    /// Panics when no platform render interface is registered.
313    pub fn open(&self) -> DrawingContext<'static> {
314        DrawingContext::owned(Box::new(DrawingGroupDrawingContext::new(self.to_ref())))
315    }
316
317    fn subscribe_collection(&self, children: &DrawingCollection) {
318        let weak = self.to_ref().downgrade();
319        let subscription = children.collection_changed(move |e| {
320            let Some(this) = weak.upgrade() else { return };
321
322            if e.action == NotifyCollectionChangedAction::Reset {
323                panic!("Collection reset is not supported.");
324            }
325
326            for child in e.old_items {
327                this.unsubscribe_child(child);
328            }
329
330            for child in e.new_items {
331                this.subscribe_child(child);
332            }
333
334            this.raise_invalidated();
335        });
336        *self.children_subscription.borrow_mut() = Some(subscription);
337    }
338
339    fn subscribe_child(&self, child: &Ref<Drawing>) {
340        let weak = self.to_ref().downgrade();
341        let subscription = child.invalidated(move || {
342            if let Some(this) = weak.upgrade() {
343                this.raise_invalidated();
344            }
345        });
346        self.child_subscriptions.borrow_mut().push((child.clone(), subscription));
347    }
348
349    fn unsubscribe_child(&self, child: &Ref<Drawing>) {
350        let position = self.child_subscriptions.borrow().iter().position(|(c, _)| c.ptr_eq(child));
351        if let Some(position) = position {
352            let (_, subscription) = self.child_subscriptions.borrow_mut().remove(position);
353            subscription.dispose();
354        }
355    }
356}
357
358/// The drawing context core that records what is drawn as the children of
359/// a [`DrawingGroup`].
360struct DrawingGroupDrawingContext {
361    drawing_group: Ref<DrawingGroup>,
362    platform_render_interface: Rc<dyn IPlatformRenderInterface>,
363
364    disposed: bool,
365
366    // Root drawing created by this drawing context.
367    //
368    // If there is only a single child of the root drawing group,
369    // `root_drawing` references the single child, and the root
370    // `current_drawing_group` value is `None`. Otherwise, `root_drawing`
371    // references the root drawing group, and is the same value as the root
372    // `current_drawing_group`.
373    //
374    // Either way, `root_drawing` always references the root drawing.
375    root_drawing: Option<Ref<Drawing>>,
376
377    // Current drawing group that new children are added to.
378    current_drawing_group: Option<Ref<DrawingGroup>>,
379
380    // Previous values of `current_drawing_group`.
381    previous_drawing_group_stack: Option<Vec<Option<Ref<DrawingGroup>>>>,
382}
383
384impl DrawingGroupDrawingContext {
385    fn new(drawing_group: Ref<DrawingGroup>) -> Self {
386        Self {
387            drawing_group,
388            platform_render_interface: platform::render_interface(),
389            disposed: false,
390            root_drawing: None,
391            current_drawing_group: None,
392            previous_drawing_group_stack: None,
393        }
394    }
395
396    fn pop(&mut self) {
397        // Verify that pop hasn't been called too many times.
398        let Some(previous) = self.previous_drawing_group_stack.as_mut().and_then(Vec::pop) else {
399            panic!("DrawingGroupStack count missmatch.");
400        };
401
402        // Restore the previous value of the current drawing group.
403        self.current_drawing_group = previous;
404    }
405
406    fn push_new_drawing_group(&mut self) -> Ref<DrawingGroup> {
407        // Instantiate a new drawing group.
408        let drawing_group = DrawingGroup::new();
409
410        // Add it to the drawing graph, like any other drawing.
411        self.add_drawing(drawing_group.clone().upcast());
412
413        // Save the previous `current_drawing_group` value. The stack is
414        // allocated lazily because many uses have a depth of one.
415        //
416        // If this is the first call, the value of `current_drawing_group`
417        // is `None` because `add_drawing` doesn't create a current group
418        // for the first drawing. Having `None` on the stack is valid, and
419        // simply denotes that this new drawing group is the first child in
420        // the root drawing group. It is also possible for the first value
421        // on the stack to be `Some`, which means that the root drawing
422        // group has other children.
423        let previous = self.current_drawing_group.take();
424        self.previous_drawing_group_stack.get_or_insert_with(|| Vec::with_capacity(2)).push(previous);
425
426        // Set this drawing group as the current one so that subsequent
427        // drawings are added as its children until pop is called.
428        self.current_drawing_group = Some(drawing_group.clone());
429
430        drawing_group
431    }
432
433    fn add_new_geometry_drawing(
434        &mut self,
435        brush: Option<&Rc<dyn IBrush>>,
436        pen: Option<&Rc<dyn IPen>>,
437        geometry: Ref<Geometry>,
438    ) {
439        // Instantiate the geometry drawing.
440        let geometry_drawing = GeometryDrawing::new();
441        geometry_drawing.set_brush(brush.cloned());
442        geometry_drawing.set_pen(pen.cloned());
443        geometry_drawing.set_geometry(geometry);
444
445        // Add it to the drawing graph.
446        self.add_drawing(geometry_drawing.upcast());
447    }
448
449    fn add_drawing(&mut self, new_drawing: Ref<Drawing>) {
450        match (&self.root_drawing, &self.current_drawing_group) {
451            (None, Some(_)) => {
452                panic!("When a DrawingGroup is set, it should be made the root if a root drawing didnt exist.");
453            }
454            (None, None) => {
455                // If this is the first drawing being added, avoid creating
456                // a drawing group and set this drawing as the root drawing.
457                // This optimizes the common case where only a single child
458                // exists in the root drawing group.
459                self.root_drawing = Some(new_drawing);
460            }
461            (Some(root_drawing), None) => {
462                // When the second drawing is added at the root level, set a
463                // drawing group as the root and add both drawings to it.
464                let current_drawing_group = DrawingGroup::new();
465
466                // Add both children.
467                let children = current_drawing_group.children();
468                children.add(root_drawing.clone());
469                children.add(new_drawing);
470
471                // Set the new drawing group as the current.
472                self.root_drawing = Some(current_drawing_group.clone().upcast());
473                self.current_drawing_group = Some(current_drawing_group);
474            }
475            (Some(_), Some(current_drawing_group)) => {
476                // If there already is a current drawing group, then simply
477                // add the new drawing to it.
478                current_drawing_group.children().add(new_drawing);
479            }
480        }
481    }
482}
483
484impl IDrawingContextCore for DrawingGroupDrawingContext {
485    fn draw_line_core(&mut self, pen: &Rc<dyn IPen>, p1: Point, p2: Point) {
486        // Instantiate the geometry.
487        let geometry = self.platform_render_interface.create_line_geometry(p1, p2);
488
489        // Add the drawing to the drawing graph.
490        self.add_new_geometry_drawing(None, Some(pen), PlatformGeometry::new(geometry).upcast());
491    }
492
493    fn draw_geometry_impl_core(
494        &mut self,
495        brush: Option<&Rc<dyn IBrush>>,
496        pen: Option<&Rc<dyn IPen>>,
497        geometry: &Arc<dyn IGeometryImpl>,
498    ) {
499        if brush.is_none() && pen.is_none() {
500            return;
501        }
502
503        self.add_new_geometry_drawing(brush, pen, PlatformGeometry::new(geometry.clone()).upcast());
504    }
505
506    fn draw_rectangle_core(
507        &mut self,
508        brush: Option<&Rc<dyn IBrush>>,
509        pen: Option<&Rc<dyn IPen>>,
510        rrect: RoundedRect,
511        _box_shadows: &BoxShadows,
512    ) {
513        // Instantiate the geometry.
514        let geometry = self.platform_render_interface.create_rectangle_geometry(rrect.rect);
515
516        // Add the drawing to the drawing graph.
517        self.add_new_geometry_drawing(brush, pen, PlatformGeometry::new(geometry).upcast());
518    }
519
520    fn draw_ellipse_core(&mut self, brush: Option<&Rc<dyn IBrush>>, pen: Option<&Rc<dyn IPen>>, rect: Rect) {
521        if brush.is_none() && pen.is_none() {
522            return;
523        }
524
525        // Instantiate the geometry.
526        let geometry = self.platform_render_interface.create_ellipse_geometry(rect);
527
528        // Add the drawing to the drawing graph.
529        self.add_new_geometry_drawing(brush, pen, PlatformGeometry::new(geometry).upcast());
530    }
531
532    fn draw_bitmap(&mut self, _source: &crate::utilities::RefCounted<crate::platform::SharedBitmapImpl>, _opacity: f64, _source_rect: Rect, _dest_rect: Rect) {
533        panic!("Drawing a bitmap into a DrawingGroup is not implemented.");
534    }
535
536    fn custom(&mut self, _custom: &std::sync::Arc<dyn ICustomDrawOperation>) {
537        panic!("Custom draw operations are not supported when drawing into a DrawingGroup.");
538    }
539
540    fn draw_glyph_run(&mut self, foreground: Option<&Rc<dyn IBrush>>, glyph_run: &Rc<GlyphRun>) {
541        let Some(foreground) = foreground else {
542            return;
543        };
544
545        let glyph_run_drawing = GlyphRunDrawing::new();
546        glyph_run_drawing.set_foreground(Some(foreground.clone()));
547        glyph_run_drawing.set_glyph_run(Some(glyph_run.clone()));
548
549        // Add the drawing to the drawing graph.
550        self.add_drawing(glyph_run_drawing.upcast());
551    }
552
553    fn push_clip_core(&mut self, rect: Rect) {
554        let drawing_group = self.push_new_drawing_group();
555
556        drawing_group.set_clip_geometry(RectangleGeometry::with_rect(rect).upcast::<Geometry>());
557    }
558
559    fn push_rounded_clip_core(&mut self, _rect: RoundedRect) {
560        panic!("Pushing a rounded clip into a DrawingGroup is not implemented.");
561    }
562
563    fn push_geometry_clip_core(&mut self, clip: &Ref<Geometry>) {
564        let drawing_group = self.push_new_drawing_group();
565
566        drawing_group.set_clip_geometry(clip.clone());
567    }
568
569    fn push_opacity_core(&mut self, opacity: f64) {
570        let drawing_group = self.push_new_drawing_group();
571
572        drawing_group.set_opacity(opacity);
573    }
574
575    fn try_push_compositing_layer_core(&mut self, blend: BitmapBlendingMode, opacity: f64, bounds: Rect) -> bool {
576        if !self.platform_render_interface.supports_compositing_layers() {
577            return false;
578        }
579        self.push_new_drawing_group().compositing_layer.set(Some((blend, opacity, bounds)));
580        true
581    }
582
583    fn pop_compositing_layer_core(&mut self) {
584        self.pop();
585    }
586
587    fn push_opacity_mask_core(&mut self, mask: &Rc<dyn IBrush>, _bounds: Rect) {
588        let drawing_group = self.push_new_drawing_group();
589
590        drawing_group.set_opacity_mask(Some(mask.clone()));
591    }
592
593    fn push_transform_core(&mut self, matrix: Matrix) {
594        // Instantiate a new drawing group and set it as the current one.
595        let drawing_group = self.push_new_drawing_group();
596
597        // Set the transform on the new drawing group.
598        drawing_group.set_transform(MatrixTransform::with_matrix(matrix).upcast::<Transform>());
599    }
600
601    fn push_render_options_core(&mut self, render_options: RenderOptions) {
602        // Instantiate a new drawing group and set it as the current one.
603        let drawing_group = self.push_new_drawing_group();
604
605        // Set the render options on the new drawing group.
606        drawing_group.set_render_options(Some(render_options));
607    }
608
609    fn push_text_options_core(&mut self, text_options: TextOptions) {
610        // Instantiate a new drawing group and set it as the current one.
611        let drawing_group = self.push_new_drawing_group();
612
613        // Set the text options on the new drawing group.
614        drawing_group.set_text_options(Some(text_options));
615    }
616
617    fn push_effect_core(&mut self, effect: &Rc<dyn IEffect>, bounds: Rect) {
618        // Instantiate a new drawing group and set it as the current one.
619        let drawing_group = self.push_new_drawing_group();
620
621        // Set the effect on the new drawing group.
622        drawing_group.set_effect(Some(effect.clone()));
623        drawing_group.set_effect_bounds(Some(bounds));
624    }
625
626    fn pop_clip_core(&mut self) {
627        self.pop();
628    }
629
630    fn pop_geometry_clip_core(&mut self) {
631        self.pop();
632    }
633
634    fn pop_opacity_core(&mut self) {
635        self.pop();
636    }
637
638    fn pop_opacity_mask_core(&mut self) {
639        self.pop();
640    }
641
642    fn pop_transform_core(&mut self) {
643        self.pop();
644    }
645
646    fn pop_render_options_core(&mut self) {
647        self.pop();
648    }
649
650    fn pop_text_options_core(&mut self) {
651        self.pop();
652    }
653
654    fn pop_effect_core(&mut self) {
655        self.pop();
656    }
657
658    fn dispose_core(&mut self) {
659        // Dispose may be called multiple times without failing.
660        if self.disposed {
661            return;
662        }
663
664        // Match any outstanding push calls with a pop.
665        let stack_count = self.previous_drawing_group_stack.as_ref().map_or(0, Vec::len);
666        for _ in 0..stack_count {
667            self.pop();
668        }
669
670        // Close with the root drawing group's children.
671        let root_children = match &self.current_drawing_group {
672            // If a root drawing group was created because multiple elements
673            // exist at the root level, provide its children collection
674            // directly.
675            Some(current_drawing_group) => current_drawing_group.children(),
676            // Create a new collection if no root drawing group was created
677            // because the root level only contained a single child. The
678            // collection is needed because opening a group always replaces
679            // its children collection.
680            None => {
681                let root_children = DrawingCollection::new();
682                if let Some(root_drawing) = &self.root_drawing {
683                    root_children.add(root_drawing.clone());
684                }
685                root_children
686            }
687        };
688
689        self.drawing_group.set_children(root_children);
690
691        self.disposed = true;
692    }
693}
694
695#[cfg(test)]
696mod tests {
697    // Not from upstream: the upstream tests of this class record through a
698    // drawing context.
699    use super::*;
700    use crate::media::effects::BlurEffect;
701    use crate::media::{Brushes, DrawingImage, GeometryDrawing, IImage, ImageDrawing, TranslateTransform};
702    use crate::{PixelRect, Size};
703
704    fn counter(drawing: &Drawing) -> Rc<Cell<i32>> {
705        let count = Rc::new(Cell::new(0));
706        let c = count.clone();
707        drawing.invalidated(move || c.set(c.get() + 1));
708        count
709    }
710
711    #[test]
712    fn compositing_layer_drawing_group_open_preserves_the_recorded_scope() {
713        use crate::media::immutable::ImmutableSolidColorBrush;
714        use crate::media::{Colors, PlatformDrawingContext};
715        use crate::rendering::testing::{DrawingLog, MockDrawingContextImpl, MockPlatformRenderInterface};
716
717        let (scope, backend) = MockPlatformRenderInterface::install();
718        backend.set_supports_compositing_layers(true);
719        let group = DrawingGroup::new();
720        let brush: Rc<dyn IBrush> = Rc::new(ImmutableSolidColorBrush::new(Colors::RED));
721        {
722            let mut context = group.open();
723            let _layer = context.try_push_compositing_layer(
724                BitmapBlendingMode::Multiply, 0.5, Rect::new(0.0, 0.0, 1.0, 1.0),
725            ).unwrap();
726            context.fill_rectangle(&brush, Rect::new(5.0, 6.0, 10.0, 20.0), 0.0);
727        }
728        assert_eq!(group.get_bounds(), Rect::new(5.0, 6.0, 10.0, 20.0));
729        let log = DrawingLog::new();
730        let mut implementation = MockDrawingContextImpl::new(log.clone());
731        implementation.supports_compositing_layers = true;
732        implementation.log_transforms = false;
733        {
734            let mut core = PlatformDrawingContext::borrowed(&mut implementation);
735            let mut context = DrawingContext::new(&mut core);
736            group.draw(&mut context);
737        }
738        let relevant: Vec<_> = log.entries().into_iter().filter(|entry| {
739            entry.contains("CompositingLayer") || entry.starts_with("Draw")
740        }).collect();
741        assert_eq!(relevant, [
742            "PushCompositingLayer Multiply 0.5 0, 0, 1, 1",
743            "DrawGeometry Red none 5, 6, 10, 20",
744            "PopCompositingLayer",
745        ]);
746        scope.dispose();
747    }
748
749    #[test]
750    fn children_changes_and_child_changes_raise_invalidated() {
751        let group = DrawingGroup::new();
752        let count = counter(&group);
753
754        let child = ImageDrawing::new();
755        group.children().add(child.clone().upcast());
756        assert_eq!(1, count.get());
757
758        child.set_rect(Rect::new(1.0, 2.0, 3.0, 4.0));
759        assert_eq!(2, count.get());
760        assert_eq!(Rect::new(1.0, 2.0, 3.0, 4.0), group.get_bounds());
761
762        group.children().remove_at(0);
763        assert_eq!(3, count.get());
764        child.set_rect(Rect::default());
765        assert_eq!(3, count.get());
766    }
767
768    #[test]
769    fn replaced_children_collection_is_tracked() {
770        let group = DrawingGroup::new();
771        let old = group.children();
772        let child = ImageDrawing::new();
773        let replacement = DrawingCollection::from_items([child.clone().upcast()]);
774        let count = counter(&group);
775
776        group.set_children(replacement.clone());
777        assert_eq!(1, count.get());
778        assert!(group.get_direct_value(DrawingGroup::children_property()).ptr_eq(&replacement));
779
780        child.set_rect(Rect::new(0.0, 0.0, 5.0, 5.0));
781        assert_eq!(2, count.get());
782
783        old.add(ImageDrawing::new().upcast());
784        assert_eq!(2, count.get());
785
786        group.set_children(replacement);
787        assert_eq!(2, count.get());
788    }
789
790    #[test]
791    #[should_panic(expected = "Collection reset is not supported.")]
792    fn resetting_the_children_is_not_supported() {
793        let group = DrawingGroup::new();
794        let children = group.children();
795        children.set_reset_behavior(crate::media::ResetBehavior::Reset);
796        children.add(ImageDrawing::new().upcast());
797        children.clear();
798    }
799
800    #[test]
801    fn nested_value_changes_raise_invalidated() {
802        let group = DrawingGroup::new();
803        let transform = TranslateTransform::new();
804        let effect = BlurEffect::new();
805        group.set_transform(transform.clone().upcast::<Transform>());
806        group.set_effect(Some(effect.clone().into()));
807        let count = counter(&group);
808
809        transform.set_x(10.0);
810        assert_eq!(1, count.get());
811        effect.set_radius(1.0);
812        assert_eq!(2, count.get());
813
814        group.set_transform(None);
815        group.set_effect(None);
816        assert_eq!(4, count.get());
817        transform.set_x(20.0);
818        effect.set_radius(2.0);
819        assert_eq!(4, count.get());
820
821        // A brush is not baked into the recording: only its replacement
822        // invalidates the drawing.
823        let drawing = GeometryDrawing::new();
824        let count = counter(&drawing);
825        let brush = crate::media::SolidColorBrush::new();
826        drawing.set_brush(Some(brush.clone().into()));
827        assert_eq!(1, count.get());
828        brush.set_opacity(0.5);
829        assert_eq!(1, count.get());
830        drawing.set_brush(Some(Brushes::red() as Rc<dyn IBrush>));
831        assert_eq!(2, count.get());
832    }
833
834    #[test]
835    fn bounds_apply_the_transform() {
836        let group = DrawingGroup::new();
837        let a = ImageDrawing::new();
838        a.set_rect(Rect::new(0.0, 0.0, 10.0, 10.0));
839        let b = ImageDrawing::new();
840        b.set_rect(Rect::new(20.0, 20.0, 10.0, 10.0));
841        group.children().add_range([a.upcast(), b.upcast()]);
842        assert_eq!(Rect::new(0.0, 0.0, 30.0, 30.0), group.get_bounds());
843
844        let transform = TranslateTransform::new();
845        transform.set_x(5.0);
846        transform.set_y(7.0);
847        group.set_transform(transform.upcast::<Transform>());
848        assert_eq!(Rect::new(5.0, 7.0, 30.0, 30.0), group.get_bounds());
849        assert_eq!(None, group.effect_bounds());
850        assert_eq!(None, group.render_options());
851    }
852
853    #[test]
854    fn drawing_image_follows_its_drawing() {
855        let drawing = ImageDrawing::new();
856        drawing.set_rect(Rect::new(0.0, 0.0, 16.0, 8.0));
857        let image = DrawingImage::with_drawing(drawing.clone().upcast::<Drawing>());
858        assert_eq!(Size::new(16.0, 8.0), image.size());
859
860        let count = Rc::new(Cell::new(0));
861        let c = count.clone();
862        image.invalidated(move || c.set(c.get() + 1));
863
864        drawing.set_rect(Rect::new(0.0, 0.0, 4.0, 4.0));
865        assert_eq!(1, count.get());
866        assert_eq!(Size::new(4.0, 4.0), image.size());
867
868        image.set_viewbox(Some(Rect::new(1.0, 1.0, 2.0, 3.0)));
869        assert_eq!(2, count.get());
870        assert_eq!(Size::new(2.0, 3.0), image.size());
871
872        // A nested drawing image invalidates the drawing that draws it.
873        let outer = ImageDrawing::new();
874        let handle: Rc<dyn IImage> = image.clone().into();
875        outer.set_image_source(Some(handle.clone()));
876        let outer_count = counter(&outer);
877        image.set_viewbox(None);
878        assert_eq!(1, outer_count.get());
879        assert!(handle.as_bitmap().is_none());
880        assert!(handle.as_object().unwrap().is::<DrawingImage>());
881
882        image.set_drawing(None);
883        let before = count.get();
884        drawing.set_rect(Rect::default());
885        assert_eq!(before, count.get());
886        assert_eq!(Size::default(), image.size());
887        let _ = PixelRect::default();
888    }
889
890    // --- from upstream: DrawingGroupTests --------------------------------
891
892    use crate::media::immutable::ImmutableSolidColorBrush;
893    use crate::media::{Colors, PlatformDrawingContext};
894    use crate::rendering::testing::{DrawingLog, MockDrawingContextImpl, MockPlatformRenderInterface};
895
896    fn solid(color: crate::media::Color) -> Rc<dyn IBrush> {
897        Rc::new(ImmutableSolidColorBrush::new(color))
898    }
899
900    fn blur(radius: f64) -> Rc<dyn IEffect> {
901        let effect = BlurEffect::new();
902        effect.set_radius(radius);
903        effect.into()
904    }
905
906    fn rectangle_drawing(brush: Rc<dyn IBrush>, rect: Rect) -> Ref<Drawing> {
907        let drawing = GeometryDrawing::new();
908        drawing.set_brush(Some(brush));
909        drawing.set_geometry(RectangleGeometry::with_rect(rect).upcast::<Geometry>());
910        drawing.upcast()
911    }
912
913    /// A drawing context core that captures the bounds of the pushed effect
914    /// and opacity mask.
915    #[derive(Default)]
916    struct MockDrawingContext {
917        effect: Option<Rc<dyn IEffect>>,
918        bounds: Rect,
919        opacity_mask_bounds: Rect,
920    }
921
922    impl IDrawingContextCore for MockDrawingContext {
923        fn draw_line_core(&mut self, _: &Rc<dyn IPen>, _: Point, _: Point) {}
924        fn draw_geometry_impl_core(&mut self, _: Option<&Rc<dyn IBrush>>, _: Option<&Rc<dyn IPen>>, _: &Arc<dyn IGeometryImpl>) {
925        }
926        fn draw_rectangle_core(&mut self, _: Option<&Rc<dyn IBrush>>, _: Option<&Rc<dyn IPen>>, _: RoundedRect, _: &BoxShadows) {
927        }
928        fn draw_ellipse_core(&mut self, _: Option<&Rc<dyn IBrush>>, _: Option<&Rc<dyn IPen>>, _: Rect) {}
929        fn draw_bitmap(&mut self, _: &crate::utilities::RefCounted<crate::platform::SharedBitmapImpl>, _: f64, _: Rect, _: Rect) {}
930        fn custom(&mut self, _: &std::sync::Arc<dyn ICustomDrawOperation>) {}
931        fn draw_glyph_run(&mut self, _: Option<&Rc<dyn IBrush>>, _: &Rc<GlyphRun>) {}
932        fn push_clip_core(&mut self, _: Rect) {}
933        fn push_rounded_clip_core(&mut self, _: RoundedRect) {}
934        fn push_geometry_clip_core(&mut self, _: &Ref<Geometry>) {}
935        fn push_opacity_core(&mut self, _: f64) {}
936        fn push_opacity_mask_core(&mut self, _: &Rc<dyn IBrush>, bounds: Rect) {
937            self.opacity_mask_bounds = bounds;
938        }
939        fn push_transform_core(&mut self, _: Matrix) {}
940        fn push_render_options_core(&mut self, _: RenderOptions) {}
941        fn push_text_options_core(&mut self, _: TextOptions) {}
942        fn push_effect_core(&mut self, effect: &Rc<dyn IEffect>, bounds: Rect) {
943            self.effect = Some(effect.clone());
944            self.bounds = bounds;
945        }
946        fn pop_clip_core(&mut self) {}
947        fn pop_geometry_clip_core(&mut self) {}
948        fn pop_opacity_core(&mut self) {}
949        fn pop_opacity_mask_core(&mut self) {}
950        fn pop_transform_core(&mut self) {}
951        fn pop_render_options_core(&mut self) {}
952        fn pop_text_options_core(&mut self) {}
953        fn pop_effect_core(&mut self) {}
954        fn dispose_core(&mut self) {}
955    }
956
957    #[test]
958    fn push_effect_should_store_provided_bounds() {
959        let (scope, _) = MockPlatformRenderInterface::install();
960        let group = DrawingGroup::new();
961        let effect = blur(10.0);
962        let bounds = Rect::new(10.0, 10.0, 100.0, 100.0);
963
964        {
965            let mut context = group.open();
966            let state = context.push_effect(&effect, bounds);
967            context.draw_rectangle(
968                Some(&solid(Colors::RED)),
969                None,
970                Rect::new(20.0, 20.0, 50.0, 50.0),
971                0.0,
972                0.0,
973                &BoxShadows::default(),
974            );
975            context.pop(state);
976        }
977
978        // Opening the group adds a child drawing group to the root group
979        // when an effect is pushed.
980        assert_eq!(1, group.children().len());
981        let child_group = group.children().get(0).cast::<DrawingGroup>().expect("a drawing group");
982        assert!(child_group.effect() == Some(effect));
983        assert_eq!(Some(bounds), child_group.effect_bounds());
984        scope.dispose();
985    }
986
987    #[test]
988    fn drawing_with_effect_should_use_stored_bounds() {
989        let (scope, _) = MockPlatformRenderInterface::install();
990        let effect = blur(10.0);
991        let bounds = Rect::new(10.0, 10.0, 100.0, 100.0);
992        let group = DrawingGroup::new();
993        group.set_effect(Some(effect.clone()));
994        group.set_effect_bounds(Some(bounds));
995        group.children().add(rectangle_drawing(solid(Colors::RED), Rect::new(20.0, 20.0, 50.0, 50.0)));
996
997        let mut mock_context = MockDrawingContext::default();
998        {
999            let mut context = DrawingContext::new(&mut mock_context);
1000            group.draw(&mut context);
1001        }
1002
1003        assert!(mock_context.effect == Some(effect));
1004        assert_eq!(bounds, mock_context.bounds);
1005        scope.dispose();
1006    }
1007
1008    /// Regression test: the platform drawing context was passing the raw
1009    /// content bounds to the platform effect API without inflating them by
1010    /// the effect output padding, causing effects to be clipped.
1011    #[test]
1012    fn platform_drawing_context_push_effect_should_inflate_bounds_for_platform_api() {
1013        let log = DrawingLog::new();
1014        let mut effect_impl = MockDrawingContextImpl::new(log.clone());
1015        effect_impl.supports_effects = true;
1016        let mut core = PlatformDrawingContext::borrowed(&mut effect_impl);
1017        let mut context = DrawingContext::new(&mut core);
1018
1019        let effect = blur(10.0);
1020        let content_bounds = Rect::new(10.0, 10.0, 100.0, 100.0);
1021        let expected_inflated_bounds =
1022            content_bounds.inflate_thickness(EffectExtensions::get_effect_output_padding(Some(&*effect)));
1023
1024        let state = context.push_effect(&effect, content_bounds);
1025        context.pop(state);
1026        context.dispose();
1027
1028        // The platform API received the inflated (output) bounds, not the
1029        // raw content bounds.
1030        assert_eq!(1, log.count("PushEffect"));
1031        assert_eq!(log.entries()[0], format!("PushEffect {expected_inflated_bounds}"));
1032    }
1033
1034    /// Regression test: drawing a group was passing uninflated local bounds
1035    /// to the opacity mask when an effect was also set, meaning the opacity
1036    /// mask didn't cover the effect output region.
1037    #[test]
1038    fn draw_core_with_effect_and_opacity_mask_should_use_effect_bounds_for_opacity_mask() {
1039        let (scope, _) = MockPlatformRenderInterface::install();
1040        let effect = blur(10.0);
1041        let content_bounds = Rect::new(10.0, 10.0, 100.0, 100.0);
1042        let expected_effect_bounds =
1043            content_bounds.inflate_thickness(EffectExtensions::get_effect_output_padding(Some(&*effect)));
1044
1045        let group = DrawingGroup::new();
1046        group.set_effect(Some(effect));
1047        group.set_effect_bounds(Some(content_bounds));
1048        group.set_opacity_mask(Some(solid(Colors::RED)));
1049        group.children().add(rectangle_drawing(solid(Colors::BLUE), Rect::new(20.0, 20.0, 50.0, 50.0)));
1050
1051        let mut mock_context = MockDrawingContext::default();
1052        {
1053            let mut context = DrawingContext::new(&mut mock_context);
1054            group.draw(&mut context);
1055        }
1056
1057        assert_eq!(expected_effect_bounds, mock_context.opacity_mask_bounds);
1058        scope.dispose();
1059    }
1060
1061    #[test]
1062    fn invalidated_is_raised_when_child_is_added_or_removed() {
1063        let group = DrawingGroup::new();
1064        let child: Ref<Drawing> = GeometryDrawing::new().upcast();
1065        let count = counter(&group);
1066
1067        group.children().add(child.clone());
1068        assert_eq!(1, count.get());
1069
1070        group.children().remove(&child);
1071        assert_eq!(2, count.get());
1072    }
1073
1074    #[test]
1075    fn invalidated_is_raised_when_child_changes() {
1076        let child = GeometryDrawing::new();
1077        let group = DrawingGroup::new();
1078        group.children().add(child.clone().upcast());
1079
1080        let count = counter(&group);
1081
1082        child.set_brush(Some(Brushes::red()));
1083        assert!(count.get() > 0);
1084    }
1085
1086    #[test]
1087    fn removed_child_is_no_longer_tracked() {
1088        let child = GeometryDrawing::new();
1089        let group = DrawingGroup::new();
1090        let handle: Ref<Drawing> = child.clone().upcast();
1091        group.children().add(handle.clone());
1092        group.children().remove(&handle);
1093
1094        let count = counter(&group);
1095
1096        child.set_brush(Some(Brushes::red()));
1097        assert_eq!(0, count.get());
1098    }
1099
1100    // --- not from upstream: recording into a group and drawing it ---------
1101
1102    fn replay(drawing: &Drawing) -> Vec<String> {
1103        let log = DrawingLog::new();
1104        let mut platform_impl = MockDrawingContextImpl::new(log.clone());
1105        platform_impl.log_transforms = false;
1106        platform_impl.supports_effects = true;
1107        {
1108            let mut core = PlatformDrawingContext::borrowed(&mut platform_impl);
1109            let mut context = DrawingContext::new(&mut core);
1110            drawing.draw(&mut context);
1111        }
1112        log.entries()
1113    }
1114
1115    #[test]
1116    fn open_with_a_single_drawing_makes_it_the_only_child() {
1117        let (scope, _) = MockPlatformRenderInterface::install();
1118        let group = DrawingGroup::new();
1119        let old_children = group.children();
1120        let count = counter(&group);
1121        {
1122            let mut context = group.open();
1123            context.draw_ellipse(Some(&solid(Colors::RED)), None, Rect::new(1.0, 2.0, 3.0, 4.0));
1124            // Nothing changes before the context is closed.
1125            assert_eq!(0, group.children().len());
1126        }
1127        assert!(!group.children().ptr_eq(&old_children));
1128        assert_eq!(1, group.children().len());
1129        assert_eq!(1, count.get());
1130        let child = group.children().get(0).cast::<GeometryDrawing>().expect("a geometry drawing");
1131        assert!(child.pen().is_none());
1132        assert_eq!(Rect::new(1.0, 2.0, 3.0, 4.0), group.get_bounds());
1133
1134        // Opening again replaces the content; an empty recording clears it.
1135        group.open().dispose();
1136        assert_eq!(0, group.children().len());
1137        scope.dispose();
1138    }
1139
1140    #[test]
1141    fn open_records_states_as_nested_groups_and_draws_back_the_same_content() {
1142        let (scope, _) = MockPlatformRenderInterface::install();
1143        let group = DrawingGroup::new();
1144        let pen: Rc<dyn IPen> = Rc::new(crate::media::immutable::ImmutablePen::with_brush(
1145                Some(Rc::new(ImmutableSolidColorBrush::new(Colors::BLUE))),
1146                2.0,
1147            ));
1148        {
1149            let mut context = group.open();
1150            context.draw_line(&pen, Point::new(0.0, 0.0), Point::new(10.0, 10.0));
1151            let transform = context.push_transform(Matrix::create_translation(5.0, 5.0));
1152            let opacity = context.push_opacity(0.5);
1153            context.draw_rectangle(
1154                Some(&solid(Colors::RED)),
1155                None,
1156                Rect::new(0.0, 0.0, 4.0, 4.0),
1157                0.0,
1158                0.0,
1159                &BoxShadows::default(),
1160            );
1161            context.pop(opacity);
1162            context.pop(transform);
1163            let clip = context.push_clip(Rect::new(0.0, 0.0, 20.0, 20.0));
1164            let options = context.push_render_options(RenderOptions::default());
1165            let geometry: Ref<Geometry> = RectangleGeometry::with_rect(Rect::new(1.0, 1.0, 2.0, 2.0)).upcast();
1166            let geometry_clip = context.push_geometry_clip(&geometry);
1167            let mask = context.push_opacity_mask(&solid(Colors::GREEN), Rect::new(0.0, 0.0, 9.0, 9.0));
1168            context.draw_geometry(Some(&solid(Colors::RED)), Some(&pen), &geometry);
1169            // The states still pushed are closed with the context.
1170            let _ = (clip, options, geometry_clip, mask);
1171        }
1172
1173        // Root: the line, the transform group and the clip group.
1174        let children = group.children();
1175        assert_eq!(3, children.len());
1176        assert!(children.get(0).is::<GeometryDrawing>());
1177        let transform_group = children.get(1).cast::<DrawingGroup>().unwrap();
1178        assert_eq!(Matrix::create_translation(5.0, 5.0), transform_group.transform().unwrap().value());
1179        let opacity_group = transform_group.children().get(0).cast::<DrawingGroup>().unwrap();
1180        assert_eq!(0.5, opacity_group.opacity());
1181        assert!(opacity_group.children().get(0).is::<GeometryDrawing>());
1182        let clip_group = children.get(2).cast::<DrawingGroup>().unwrap();
1183        assert_eq!(Rect::new(0.0, 0.0, 20.0, 20.0), clip_group.clip_geometry().unwrap().bounds());
1184        let options_group = clip_group.children().get(0).cast::<DrawingGroup>().unwrap();
1185        assert_eq!(Some(RenderOptions::default()), options_group.render_options());
1186
1187        assert_eq!(
1188            replay(&group),
1189            [
1190                "PushOpacity 1",
1191                "DrawGeometry none Blue@2 0, 0, 10, 10",
1192                "PushOpacity 1",
1193                "PushOpacity 0.5",
1194                "DrawGeometry Red none 0, 0, 4, 4",
1195                "PopOpacity",
1196                "PopOpacity",
1197                "PushOpacity 1",
1198                "PushGeometryClip 0, 0, 20, 20",
1199                "PushOpacity 1",
1200                "PushRenderOptions",
1201                "PushOpacity 1",
1202                "PushGeometryClip 1, 1, 2, 2",
1203                "PushOpacity 1",
1204                // The bounds of the masked content, including the stroke.
1205                "PushOpacityMask Green 0, 0, 4, 4",
1206                "DrawGeometry Red Blue@2 1, 1, 2, 2",
1207                "PopOpacityMask",
1208                "PopOpacity",
1209                "PopGeometryClip",
1210                "PopOpacity",
1211                "PopRenderOptions",
1212                "PopOpacity",
1213                "PopGeometryClip",
1214                "PopOpacity",
1215                "PopOpacity",
1216            ]
1217        );
1218        scope.dispose();
1219    }
1220
1221    #[test]
1222    fn open_records_glyph_runs_and_text_options_and_draws_them_back() {
1223        use crate::media::text_formatting::testing::{utf16, TextTestScope};
1224        use crate::media::{TextHintingMode, Typeface};
1225
1226        let _scope = TextTestScope::new();
1227        let glyph_typeface = Typeface::default_typeface().glyph_typeface();
1228        let glyphs: Vec<u16> =
1229            "ab".chars().map(|c| glyph_typeface.character_to_glyph_map().get_glyph(c as i32)).collect();
1230        let glyph_run = GlyphRun::from_glyph_indices(glyph_typeface, 12.0, utf16("ab"), &glyphs, None, 0);
1231        let text_options = TextOptions { text_hinting_mode: TextHintingMode::Strong, ..Default::default() };
1232
1233        let group = DrawingGroup::new();
1234        {
1235            let mut context = group.open();
1236            let options = context.push_text_options(text_options);
1237            context.draw_glyph_run(Some(&solid(Colors::RED)), &glyph_run);
1238            // Without a foreground nothing is recorded.
1239            context.draw_glyph_run(None, &glyph_run);
1240            context.pop(options);
1241        }
1242
1243        // The pushed state is the only root drawing: a group with the text
1244        // options whose only child is the glyph run drawing.
1245        let children = group.children();
1246        assert_eq!(1, children.len());
1247        let options_group = children.get(0).cast::<DrawingGroup>().expect("a group for the text options");
1248        assert_eq!(Some(text_options), options_group.text_options());
1249        assert_eq!(None, group.text_options());
1250        let group_children = options_group.children();
1251        assert_eq!(1, group_children.len());
1252        let drawing = group_children.get(0).cast::<GlyphRunDrawing>().expect("a glyph run drawing");
1253        assert!(Rc::ptr_eq(&glyph_run, &drawing.glyph_run().unwrap()));
1254        assert!(*drawing.foreground().unwrap() == *solid(Colors::RED));
1255        assert_eq!(glyph_run.bounds(), drawing.get_bounds());
1256        assert_eq!(glyph_run.bounds(), group.get_bounds());
1257
1258        let glyph_bounds = glyph_run.platform_impl().bounds();
1259        assert_eq!(
1260            replay(&group),
1261            [
1262                "PushOpacity 1".to_string(),
1263                "PushOpacity 1".to_string(),
1264                "PushTextOptions Unspecified Strong Unspecified".to_string(),
1265                format!("DrawGlyphRun Red {glyph_bounds}"),
1266                "PopTextOptions".to_string(),
1267                "PopOpacity".to_string(),
1268                "PopOpacity".to_string(),
1269            ]
1270        );
1271    }
1272
1273    #[test]
1274    fn glyph_run_drawing_without_a_glyph_run_draws_nothing() {
1275        let drawing = GlyphRunDrawing::new();
1276        drawing.set_foreground(Some(solid(Colors::RED)));
1277        assert_eq!(Rect::default(), drawing.get_bounds());
1278        assert!(replay(&drawing).is_empty());
1279    }
1280
1281    #[test]
1282    fn drawing_a_group_pushes_its_effect_with_the_children_bounds() {
1283        let (scope, _) = MockPlatformRenderInterface::install();
1284        let effect = blur(10.0);
1285        let content = Rect::new(20.0, 20.0, 50.0, 50.0);
1286        let padding = EffectExtensions::get_effect_output_padding(Some(&*effect));
1287        let group = DrawingGroup::new();
1288        group.set_effect(Some(effect));
1289        group.children().add(rectangle_drawing(solid(Colors::RED), content));
1290
1291        assert_eq!(
1292            replay(&group),
1293            [
1294                "PushOpacity 1".to_string(),
1295                format!("PushEffect {}", content.inflate_thickness(padding)),
1296                "DrawGeometry Red none 20, 20, 50, 50".to_string(),
1297                "PopEffect".to_string(),
1298                "PopOpacity".to_string(),
1299            ]
1300        );
1301        scope.dispose();
1302    }
1303
1304    #[test]
1305    #[should_panic(expected = "Pushing a rounded clip into a DrawingGroup is not implemented.")]
1306    fn open_does_not_record_rounded_clips() {
1307        let (_scope, _) = MockPlatformRenderInterface::install();
1308        let group = DrawingGroup::new();
1309        let mut context = group.open();
1310        let _ = context.push_rounded_clip(RoundedRect::from_rect(Rect::new(0.0, 0.0, 1.0, 1.0)));
1311    }
1312
1313    #[test]
1314    #[should_panic(expected = "Drawing a bitmap into a DrawingGroup is not implemented.")]
1315    fn open_does_not_record_bitmaps() {
1316        let (_scope, render_interface) = MockPlatformRenderInterface::install();
1317        let bitmap_impl: std::sync::Arc<crate::platform::SharedBitmapImpl> = std::sync::Arc::new(crate::rendering::testing::MockDrawingContextLayerImpl::new(
1318            render_interface.log().clone(),
1319            crate::PixelSize::new(1, 1),
1320        ));
1321        let bitmap = crate::utilities::RefCountable::create(bitmap_impl, || {});
1322        let group = DrawingGroup::new();
1323        let mut context = group.open();
1324        context.draw_bitmap(&bitmap, 1.0, Rect::default(), Rect::default());
1325    }
1326}