Skip to main content

ferroui_base/rendering/composition/server/
server_composition_shape.rs

1//! The server-side counterparts of the WinUI shapes
2//! (`CompositionSpriteShape`, `CompositionContainerShape`) and of
3//! `CompositionShapeCollection`, the pen of a sprite shape, and the drawing
4//! of a tree of shapes.
5
6use super::{
7    composition_transform, impl_server_render_resource, paint_composition_brush, platform_geometry_of,
8    CompositionProperty, IAnimatedServerObject, IServerObject, IServerRenderResource, IServerRenderResourceObserver,
9    ServerCompositor, ServerRenderResource,
10};
11use crate::media::immutable::{ImmutableDashStyle, ImmutablePen, ImmutableSolidColorBrush};
12use crate::media::{IBrush, IDashStyle, IImmutableBrush, IPen, PenLineCap, PenLineJoin};
13use crate::platform::{IDrawingContextImpl, IGeometryImpl};
14use crate::rendering::composition::generated::{
15    ServerCompositionContainerShapeHooks, ServerCompositionContainerShapeProps, ServerCompositionShapeCollectionHooks,
16    ServerCompositionShapeCollectionProps, ServerCompositionShapeHooks, ServerCompositionShapeProps,
17    ServerCompositionSpriteShapeHooks, ServerCompositionSpriteShapeProps,
18};
19use crate::rendering::composition::transport::BatchStreamReader;
20use crate::{Matrix, Vector};
21use std::any::{Any, TypeId};
22use std::rc::{Rc, Weak};
23use std::sync::Arc;
24use std::time::Duration;
25
26/// How deep container shapes may nest before the drawing stops: a
27/// container that contains itself would recurse for ever.
28const MAX_SHAPE_DEPTH: usize = 256;
29
30// --- the shape collection ------------------------------------------------------------
31
32fn no_composition_property(_name: &str) -> Option<&'static CompositionProperty> {
33    None
34}
35
36/// The server-side counterpart of a
37/// [`CompositionShapeCollection`](crate::rendering::composition::CompositionShapeCollection).
38///
39/// A render resource that observes its shapes: a change of a shape (or of
40/// what the shape shows) invalidates the collection, and so the shape
41/// visual or container shape that shows it.
42pub struct ServerCompositionShapeCollection {
43    this: Weak<ServerCompositionShapeCollection>,
44    base: ServerRenderResource,
45    props: ServerCompositionShapeCollectionProps,
46}
47
48impl ServerCompositionShapeCollection {
49    pub fn new(compositor: &Rc<ServerCompositor>) -> Rc<ServerCompositionShapeCollection> {
50        let collection = Rc::new_cyclic(|this: &Weak<ServerCompositionShapeCollection>| {
51            ServerCompositionShapeCollection {
52                this: this.clone(),
53                base: ServerRenderResource::new(
54                    compositor,
55                    this.clone() as Weak<dyn IAnimatedServerObject>,
56                    this.clone() as Weak<dyn IServerRenderResource>,
57                ),
58                props: ServerCompositionShapeCollectionProps::new(),
59            }
60        });
61        collection.base.object().activate();
62        collection
63    }
64
65    /// The shapes, in drawing order: server-side sprite and container
66    /// shapes.
67    pub fn shapes(&self) -> Vec<Rc<dyn IServerObject>> {
68        self.props.list().list()
69    }
70
71    pub fn count(&self) -> usize {
72        self.props.list().count()
73    }
74
75    pub fn is_disposed(&self) -> bool {
76        self.base.is_disposed()
77    }
78
79    /// The collection behind `object`, if it is one.
80    pub fn of(object: &Rc<dyn IServerObject>) -> Option<&ServerCompositionShapeCollection> {
81        object.as_any().downcast_ref::<ServerCompositionShapeCollection>()
82    }
83
84    /// Starts or stops observing the shapes.
85    fn observe(&self, shapes: &[Rc<dyn IServerObject>], observe: bool) {
86        let Some(this) = self.this.upgrade() else { return };
87        let observer: Rc<dyn IServerRenderResourceObserver> = this;
88        for shape in shapes {
89            if let Some(resource) = shape.clone().as_render_resource() {
90                if observe {
91                    resource.add_observer(&observer);
92                } else {
93                    resource.remove_observer(&observer);
94                }
95            }
96        }
97    }
98}
99
100impl_server_render_resource!(ServerCompositionShapeCollection, base, composition_property: no_composition_property);
101
102impl ServerCompositionShapeCollectionHooks for ServerCompositionShapeCollection {}
103
104impl IServerObject for ServerCompositionShapeCollection {
105    fn deserialize_changes_core(&self, reader: &mut BatchStreamReader<'_>, committed_at: Duration) {
106        let old = self.shapes();
107        self.props.deserialize_changes_core(self, reader, committed_at);
108        let new = self.shapes();
109        self.observe(&old, false);
110        self.observe(&new, true);
111    }
112
113    fn values_invalidated(&self) {
114        self.base.core().invalidated(self);
115    }
116
117    fn dispose(&self) {
118        self.observe(&self.shapes(), false);
119        self.props.list().clear();
120        self.base.core().dispose();
121    }
122
123    fn as_render_resource(self: Rc<Self>) -> Option<Rc<dyn IServerRenderResource>> {
124        Some(self)
125    }
126
127    fn as_animated(self: Rc<Self>) -> Option<Rc<dyn IAnimatedServerObject>> {
128        Some(self)
129    }
130
131    fn get_props(&self, type_id: TypeId) -> Option<&dyn Any> {
132        self.props.find_props(type_id)
133    }
134
135    fn as_any(&self) -> &dyn Any {
136        self
137    }
138
139    fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any> {
140        self
141    }
142}
143
144// --- the pen -------------------------------------------------------------------------
145
146/// The pen a sprite shape strokes its geometry with, made from the stroke
147/// members of the shape when it is drawn.
148#[derive(Clone)]
149pub struct CompositionShapePen {
150    brush: Option<Rc<dyn IBrush>>,
151    thickness: f64,
152    dash_style: Option<Rc<ImmutableDashStyle>>,
153    line_cap: PenLineCap,
154    line_join: PenLineJoin,
155    miter_limit: f64,
156}
157
158impl IPen for CompositionShapePen {
159    fn brush(&self) -> Option<Rc<dyn IBrush>> {
160        self.brush.clone()
161    }
162
163    fn dash_style(&self) -> Option<Rc<dyn IDashStyle>> {
164        self.dash_style.clone().map(|style| style as Rc<dyn IDashStyle>)
165    }
166
167    fn line_cap(&self) -> PenLineCap {
168        self.line_cap
169    }
170
171    fn line_join(&self) -> PenLineJoin {
172        self.line_join
173    }
174
175    fn miter_limit(&self) -> f64 {
176        self.miter_limit
177    }
178
179    fn thickness(&self) -> f64 {
180        self.thickness
181    }
182
183    fn as_any(&self) -> &dyn Any {
184        self
185    }
186
187    fn into_immutable_pen(self: Rc<Self>) -> Rc<ImmutablePen> {
188        // As the server-side simple pen: an immutable brush is taken as it
189        // is, a server-side solid colour brush by value.
190        let brush: Option<Rc<dyn IImmutableBrush>> = self.brush.clone().and_then(|brush| {
191            if let Some(solid) = brush.as_solid_color_brush() {
192                if brush.as_any().is::<ImmutableSolidColorBrush>() {
193                    return brush.clone().into_immutable_brush();
194                }
195                let snapshot: Rc<dyn IImmutableBrush> = Rc::new(ImmutableSolidColorBrush::from_brush(solid));
196                return Some(snapshot);
197            }
198            brush.into_immutable_brush()
199        });
200        Rc::new(ImmutablePen::new(
201            brush,
202            self.thickness,
203            self.dash_style.clone(),
204            self.line_cap,
205            self.line_join,
206            self.miter_limit,
207        ))
208    }
209}
210
211// --- the shapes ----------------------------------------------------------------------
212
213/// The transform of a shape in the space of its parent, from the members of
214/// `CompositionShape`; `None` for the identity.
215pub fn shape_transform(props: &ServerCompositionShapeProps) -> Option<Matrix> {
216    composition_transform(
217        Vector::new(0.0, 0.0),
218        Vector::new(0.0, 0.0),
219        props.center_point(),
220        props.transform_matrix(),
221        props.scale(),
222        props.rotation_angle(),
223        props.offset(),
224    )
225}
226
227/// Declares a server-side shape class: the struct, its constructor, the
228/// render resource contracts and `IServerObject`.
229macro_rules! server_shape {
230    ($(#[$meta:meta])* $name:ident, $props:ident, $hooks:ident) => {
231        $(#[$meta])*
232        pub struct $name {
233            base: ServerRenderResource,
234            props: $props,
235        }
236
237        impl $name {
238            /// Creates the shape, active: the UI-thread shape activates its
239            /// server object when it is created.
240            pub fn new(compositor: &Rc<ServerCompositor>) -> Rc<$name> {
241                let shape = Rc::new_cyclic(|this: &Weak<$name>| $name {
242                    base: ServerRenderResource::new(
243                        compositor,
244                        this.clone() as Weak<dyn IAnimatedServerObject>,
245                        this.clone() as Weak<dyn IServerRenderResource>,
246                    ),
247                    props: $props::new(),
248                });
249                shape.base.object().activate();
250                shape
251            }
252
253            /// The properties of the shape.
254            pub fn props(&self) -> &$props {
255                &self.props
256            }
257
258            /// The properties of `CompositionShape`.
259            pub fn shape_props(&self) -> &ServerCompositionShapeProps {
260                self.props.base()
261            }
262
263            /// The transform of the shape in the space of its parent; `None`
264            /// for the identity.
265            pub fn transform(&self) -> Option<Matrix> {
266                shape_transform(self.props.base())
267            }
268
269            pub fn is_disposed(&self) -> bool {
270                self.base.is_disposed()
271            }
272        }
273
274        impl_server_render_resource!($name, base, composition_property: $props::get_composition_property);
275
276        impl ServerCompositionShapeHooks for $name {}
277
278        impl $hooks for $name {}
279
280        impl IServerObject for $name {
281            fn deserialize_changes_core(&self, reader: &mut BatchStreamReader<'_>, committed_at: Duration) {
282                self.props.deserialize_changes_core(self, reader, committed_at);
283            }
284
285            fn values_invalidated(&self) {
286                self.base.core().invalidated(self);
287            }
288
289            fn dispose(&self) {
290                self.base.core().dispose();
291            }
292
293            fn as_render_resource(self: Rc<Self>) -> Option<Rc<dyn IServerRenderResource>> {
294                Some(self)
295            }
296
297            fn as_animated(self: Rc<Self>) -> Option<Rc<dyn IAnimatedServerObject>> {
298                Some(self)
299            }
300
301            fn get_props(&self, type_id: TypeId) -> Option<&dyn Any> {
302                self.props.find_props(type_id)
303            }
304
305            fn as_any(&self) -> &dyn Any {
306                self
307            }
308
309            fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any> {
310                self
311            }
312        }
313    };
314}
315
316server_shape!(
317    /// The server-side counterpart of a
318    /// [`CompositionSpriteShape`](crate::rendering::composition::CompositionSpriteShape).
319    ServerCompositionSpriteShape,
320    ServerCompositionSpriteShapeProps,
321    ServerCompositionSpriteShapeHooks
322);
323
324/// Whether `brush` (a server-side composition brush) is a surface brush,
325/// which is painted, not handed to the drawing context.
326/// Whether the brush is painted by the compositor rather than the drawing
327/// context: a surface brush, and the effect, backdrop, nine-grid, mask and
328/// XAML brushes (winui-materials.md).
329fn is_surface_brush(brush: &Rc<dyn IServerObject>) -> bool {
330    crate::rendering::composition::drawing::is_painted_composition_brush(&**brush)
331}
332
333impl ServerCompositionSpriteShape {
334    /// The geometry of the shape, trimmed.
335    pub fn platform_geometry(&self) -> Option<Arc<dyn IGeometryImpl>> {
336        platform_geometry_of(&self.props.geometry()?)
337    }
338
339    /// The pen the shape strokes with when its transform (with the
340    /// transforms of its parents and of the view box) is `transform`, with
341    /// `brush` as the brush of the pen. `None` when the thickness is not
342    /// positive.
343    ///
344    /// Deviation (winui-composition.md): a non-scaling stroke divides the
345    /// thickness by the square root of the determinant of `transform`,
346    /// exact for a uniform scale only; the platform pen has one cap, the
347    /// start cap of the shape; the dash array and offset are in multiples of
348    /// the thickness, as the dash style of the platform pen.
349    pub fn pen(&self, transform: Matrix, brush: Option<Rc<dyn IBrush>>) -> Option<CompositionShapePen> {
350        let props = &self.props;
351        let mut thickness = props.stroke_thickness() as f64;
352        if props.is_stroke_non_scaling() {
353            let scale = transform.get_determinant().abs().sqrt();
354            if scale > f64::EPSILON {
355                thickness /= scale;
356            }
357        }
358        if thickness.is_nan() || thickness <= 0.0 {
359            return None;
360        }
361        let dash_style = props.stroke_dash_array().filter(|dashes| !dashes.is_empty()).map(|dashes| {
362            let dashes: Vec<f64> = dashes.iter().map(|dash| *dash as f64).collect();
363            Rc::new(ImmutableDashStyle::new(Some(dashes.as_slice()), props.stroke_dash_offset() as f64))
364        });
365        Some(CompositionShapePen {
366            brush,
367            thickness,
368            dash_style,
369            line_cap: props.stroke_start_cap().to_pen_line_cap(),
370            line_join: props.stroke_line_join().to_pen_line_join(),
371            miter_limit: props.stroke_miter_limit() as f64,
372        })
373    }
374
375    /// Draws the shape: `transform` is its transform in the space of the
376    /// visual (with its parents' and the view box), `base` the transform of
377    /// the canvas for the visual. The fill first, then the stroke; a surface
378    /// brush is painted inside the geometry (fill) or the widened outline
379    /// (stroke), every other brush is a brush of the drawing context.
380    fn render(&self, canvas: &mut dyn IDrawingContextImpl, transform: Matrix, base: Matrix) {
381        let Some(geometry) = self.platform_geometry() else { return };
382        let fill = self.props.fill_brush();
383        let stroke = self.props.stroke_brush();
384        let fill_platform = fill.as_ref().and_then(|brush| brush.clone().as_brush());
385        let stroke_platform = stroke.as_ref().and_then(|brush| brush.clone().as_brush());
386        let pen = stroke_platform.and_then(|brush| self.pen(transform, Some(brush)));
387
388        canvas.set_transform(transform * base);
389        if let Some(brush) = fill.as_ref().filter(|brush| is_surface_brush(brush)) {
390            canvas.push_geometry_clip(&*geometry);
391            paint_composition_brush(canvas, brush, geometry.bounds());
392            canvas.pop_geometry_clip();
393        }
394        if fill_platform.is_some() || pen.is_some() {
395            canvas.draw_geometry(fill_platform.as_deref(), pen.as_ref().map(|pen| pen as &dyn IPen), &*geometry);
396        }
397        if let Some(brush) = stroke.as_ref().filter(|brush| is_surface_brush(brush)) {
398            if let Some(outline) = self.pen(transform, None) {
399                let widened = geometry.get_widened_geometry(&outline);
400                canvas.push_geometry_clip(&*widened);
401                paint_composition_brush(canvas, brush, widened.bounds());
402                canvas.pop_geometry_clip();
403            }
404        }
405        canvas.set_transform(base);
406    }
407}
408
409server_shape!(
410    /// The server-side counterpart of a
411    /// [`CompositionContainerShape`](crate::rendering::composition::CompositionContainerShape).
412    ServerCompositionContainerShape,
413    ServerCompositionContainerShapeProps,
414    ServerCompositionContainerShapeHooks
415);
416
417impl ServerCompositionContainerShape {
418    /// The shapes of the container, in drawing order.
419    pub fn shapes(&self) -> Vec<Rc<dyn IServerObject>> {
420        self.props
421            .shapes()
422            .as_ref()
423            .and_then(ServerCompositionShapeCollection::of)
424            .map(ServerCompositionShapeCollection::shapes)
425            .unwrap_or_default()
426    }
427}
428
429/// Draws `shapes` (server-side sprite and container shapes) in order onto
430/// `canvas`: `parent` is the transform from the space of the shapes to the
431/// space of the visual (the view box, the containers), `base` the transform
432/// of the canvas for the visual. The transform of the canvas is `base`
433/// again afterwards.
434pub fn render_shapes(canvas: &mut dyn IDrawingContextImpl, shapes: &[Rc<dyn IServerObject>], parent: Matrix, base: Matrix) {
435    render_shapes_at(canvas, shapes, parent, base, 0);
436}
437
438fn render_shapes_at(
439    canvas: &mut dyn IDrawingContextImpl,
440    shapes: &[Rc<dyn IServerObject>],
441    parent: Matrix,
442    base: Matrix,
443    depth: usize,
444) {
445    if depth > MAX_SHAPE_DEPTH {
446        return;
447    }
448    for shape in shapes {
449        let any = shape.as_any();
450        if let Some(sprite) = any.downcast_ref::<ServerCompositionSpriteShape>() {
451            let transform = sprite.transform().map_or(parent, |local| local * parent);
452            sprite.render(canvas, transform, base);
453        } else if let Some(container) = any.downcast_ref::<ServerCompositionContainerShape>() {
454            let transform = container.transform().map_or(parent, |local| local * parent);
455            render_shapes_at(canvas, &container.shapes(), transform, base, depth + 1);
456        }
457    }
458}