Skip to main content

ferroui_base/rendering/composition/
composition_shape.rs

1//! The WinUI shapes: `CompositionShape` and its subclasses
2//! `CompositionSpriteShape` and `CompositionContainerShape`, the
3//! `CompositionShapeCollection` of a shape visual or a container shape, and
4//! the stroke enums.
5//!
6//! As for the clips, every shape is one [`CompositionShape`] behind an
7//! `Rc`, which holds what the concrete class adds in a kind; the concrete
8//! classes are typed handles around the same `Rc`
9//! ([`CompositionSpriteShape`], [`CompositionContainerShape`]).
10
11use super::composition_object_impls::composition_object_impls;
12use super::generated::{
13    CompositionContainerShapeHooks, CompositionContainerShapeProps, CompositionShapeCollectionHooks,
14    CompositionShapeCollectionProps, CompositionShapeHooks, CompositionShapeProps, CompositionSpriteShapeHooks,
15    CompositionSpriteShapeProps, ServerCompositionContainerShapeProps, ServerCompositionSpriteShapeProps,
16};
17use super::server::{
18    IServerObject, ServerCompositionContainerShape, ServerCompositionShapeCollection, ServerCompositionSpriteShape,
19    ServerCompositor, ServerObjectId, ServerPropertyValue,
20};
21use super::transport::{BatchValue, BatchValueReader};
22use super::{CompositionBrush, CompositionGeometry, CompositionObject, Compositor, ICompositionObject};
23use crate::media::{PenLineCap, PenLineJoin};
24use crate::{Matrix, Vector};
25use std::ops::Deref;
26use std::rc::{Rc, Weak};
27use std::sync::Arc;
28
29// --- the stroke enums ----------------------------------------------------------------
30
31/// The shape at an end of a stroke or of a dash (WinUI
32/// `CompositionStrokeCap`).
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
34#[repr(i32)]
35pub enum CompositionStrokeCap {
36    /// The stroke ends at the end point.
37    #[default]
38    Flat = 0,
39    /// A square half the thickness long beyond the end point.
40    Square = 1,
41    /// A half circle beyond the end point.
42    Round = 2,
43    /// A triangle half the thickness high beyond the end point.
44    Triangle = 3,
45}
46
47impl CompositionStrokeCap {
48    /// The line cap of the platform pen.
49    ///
50    /// Deviation (winui-composition.md): the pens of FerroUI have no
51    /// triangle cap; `Triangle` is drawn as `Round`, the cap that comes
52    /// closest.
53    pub fn to_pen_line_cap(self) -> PenLineCap {
54        match self {
55            CompositionStrokeCap::Flat => PenLineCap::Flat,
56            CompositionStrokeCap::Square => PenLineCap::Square,
57            CompositionStrokeCap::Round | CompositionStrokeCap::Triangle => PenLineCap::Round,
58        }
59    }
60}
61
62impl BatchValue for CompositionStrokeCap {
63    fn write_to(&self, out: &mut Vec<u8>) {
64        (*self as i32).write_to(out);
65    }
66
67    fn read_from(input: &mut BatchValueReader<'_>) -> Self {
68        match i32::read_from(input) {
69            0 => Self::Flat,
70            1 => Self::Square,
71            2 => Self::Round,
72            3 => Self::Triangle,
73            value => panic!("the batch value stream holds {value}, which is not a CompositionStrokeCap"),
74        }
75    }
76}
77
78impl ServerPropertyValue for CompositionStrokeCap {
79    fn differs(&self, other: &Self) -> bool {
80        self != other
81    }
82}
83
84/// How the segments of a stroke are joined (WinUI
85/// `CompositionStrokeLineJoin`).
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
87#[repr(i32)]
88pub enum CompositionStrokeLineJoin {
89    /// A sharp corner, cut at the miter limit.
90    #[default]
91    Miter = 0,
92    /// A corner cut off at the outer ends of the segments.
93    Bevel = 1,
94    /// A rounded corner.
95    Round = 2,
96    /// A sharp corner, or a bevel beyond the miter limit.
97    MiterOrBevel = 3,
98}
99
100impl CompositionStrokeLineJoin {
101    /// The line join of the platform pen.
102    ///
103    /// Deviation (winui-composition.md): the miter join of the platform pens
104    /// becomes a bevel beyond the miter limit, so `Miter` (cut at the limit
105    /// in WinUI) and `MiterOrBevel` both draw that join.
106    pub fn to_pen_line_join(self) -> PenLineJoin {
107        match self {
108            CompositionStrokeLineJoin::Miter | CompositionStrokeLineJoin::MiterOrBevel => PenLineJoin::Miter,
109            CompositionStrokeLineJoin::Bevel => PenLineJoin::Bevel,
110            CompositionStrokeLineJoin::Round => PenLineJoin::Round,
111        }
112    }
113}
114
115impl BatchValue for CompositionStrokeLineJoin {
116    fn write_to(&self, out: &mut Vec<u8>) {
117        (*self as i32).write_to(out);
118    }
119
120    fn read_from(input: &mut BatchValueReader<'_>) -> Self {
121        match i32::read_from(input) {
122            0 => Self::Miter,
123            1 => Self::Bevel,
124            2 => Self::Round,
125            3 => Self::MiterOrBevel,
126            value => panic!("the batch value stream holds {value}, which is not a CompositionStrokeLineJoin"),
127        }
128    }
129}
130
131impl ServerPropertyValue for CompositionStrokeLineJoin {
132    fn differs(&self, other: &Self) -> bool {
133        self != other
134    }
135}
136
137// --- the shape collection ------------------------------------------------------------
138
139/// The shapes of a shape visual or of a container shape (WinUI
140/// `Microsoft.UI.Composition.CompositionShapeCollection`), drawn in order.
141pub struct CompositionShapeCollection {
142    this: Weak<CompositionShapeCollection>,
143    object: CompositionObject,
144    props: CompositionShapeCollectionProps<CompositionShape>,
145}
146
147impl CompositionShapeCollection {
148    pub(crate) fn new(compositor: &Rc<Compositor>) -> Rc<CompositionShapeCollection> {
149        let server = compositor.create_server_object(|compositor, _| ServerCompositionShapeCollection::new(compositor));
150        let collection = Rc::new_cyclic(|this: &Weak<CompositionShapeCollection>| CompositionShapeCollection {
151            this: this.clone(),
152            object: CompositionObject::new(compositor, Some(server)),
153            props: CompositionShapeCollectionProps::new(),
154        });
155        collection.props.initialize_defaults(&*collection);
156        collection
157    }
158
159    /// The id of the server-side collection.
160    pub fn server(&self) -> ServerObjectId {
161        self.object.required_server()
162    }
163
164    /// A snapshot of the shapes, for enumeration.
165    pub fn items(&self) -> Vec<Rc<CompositionShape>> {
166        self.props.items()
167    }
168
169    /// `Append`: adds a shape at the end (drawn last).
170    pub fn append(&self, item: Rc<CompositionShape>) {
171        self.add(item);
172    }
173
174    pub fn add(&self, item: Rc<CompositionShape>) {
175        self.props.add(self, item);
176    }
177
178    pub fn clear(&self) {
179        self.props.clear(self);
180    }
181
182    pub fn contains(&self, item: &Rc<CompositionShape>) -> bool {
183        self.props.contains(item)
184    }
185
186    pub fn remove(&self, item: &Rc<CompositionShape>) -> bool {
187        self.props.remove(self, item)
188    }
189
190    /// `Size` of the WinUI vector.
191    pub fn count(&self) -> usize {
192        self.props.count()
193    }
194
195    pub fn index_of(&self, item: &Rc<CompositionShape>) -> Option<usize> {
196        self.props.index_of(item)
197    }
198
199    pub fn insert(&self, index: usize, item: Rc<CompositionShape>) {
200        self.props.insert(self, index, item);
201    }
202
203    pub fn remove_at(&self, index: usize) {
204        self.props.remove_at(self, index);
205    }
206
207    pub fn get(&self, index: usize) -> Rc<CompositionShape> {
208        self.props.get(index)
209    }
210
211    pub fn set(&self, index: usize, value: Rc<CompositionShape>) {
212        self.props.set(self, index, value);
213    }
214}
215
216impl CompositionShapeCollectionHooks<CompositionShape> for CompositionShapeCollection {}
217
218composition_object_impls!(
219    CompositionShapeCollection, "CompositionShapeCollection",
220    serialize: |this, writer| this.props.serialize_changes_core(this, writer),
221    start_animation: |_this, _name, _animation, _final_value| false,
222    composition_property: |_this, _name| None,
223);
224
225// --- the shapes ----------------------------------------------------------------------
226
227/// What the concrete class of a shape adds to [`CompositionShape`].
228pub(crate) enum CompositionShapeKind {
229    Sprite(CompositionSpriteShapeProps),
230    Container(CompositionContainerShapeProps),
231}
232
233/// A shape of a shape visual (WinUI
234/// `Microsoft.UI.Composition.CompositionShape`), with its own animatable
235/// transform in the space of its parent (`CenterPoint`, `Offset`,
236/// `RotationAngle`, `Scale`, `TransformMatrix`), applied as the transform of
237/// a visual: the transform matrix, the scale and the rotation about the
238/// center point, then the offset.
239pub struct CompositionShape {
240    this: Weak<CompositionShape>,
241    object: CompositionObject,
242    kind: CompositionShapeKind,
243    /// The shapes of a container shape.
244    shapes: Option<Rc<CompositionShapeCollection>>,
245}
246
247impl CompositionShape {
248    fn create(
249        compositor: &Rc<Compositor>,
250        kind: CompositionShapeKind,
251        shapes: Option<Rc<CompositionShapeCollection>>,
252        server: impl FnOnce(&Rc<ServerCompositor>) -> Rc<dyn IServerObject> + Send + 'static,
253    ) -> Rc<CompositionShape> {
254        let server = compositor.create_server_object(move |compositor, _| server(compositor));
255        let shape = Rc::new_cyclic(|this: &Weak<CompositionShape>| CompositionShape {
256            this: this.clone(),
257            object: CompositionObject::new(compositor, Some(server)),
258            kind,
259            shapes,
260        });
261        match &shape.kind {
262            CompositionShapeKind::Sprite(props) => props.initialize_defaults(&*shape),
263            CompositionShapeKind::Container(props) => {
264                props.initialize_defaults(&*shape);
265                let shapes = shape.shapes.clone().map(|shapes| shapes as Rc<dyn ICompositionObject>);
266                props.set_shapes(&*shape, shapes);
267            }
268        }
269        shape
270    }
271
272    /// The generated properties of `CompositionShape`.
273    pub fn shape_props(&self) -> &CompositionShapeProps {
274        match &self.kind {
275            CompositionShapeKind::Sprite(props) => props.base(),
276            CompositionShapeKind::Container(props) => props.base(),
277        }
278    }
279
280    pub fn compositor(&self) -> &Rc<Compositor> {
281        self.object.compositor()
282    }
283
284    /// The embedded `CompositionObject`.
285    pub fn object(&self) -> &CompositionObject {
286        &self.object
287    }
288
289    /// The id of the server-side shape.
290    pub fn server(&self) -> ServerObjectId {
291        self.object.required_server()
292    }
293
294    pub fn is_disposed(&self) -> bool {
295        self.object.is_disposed()
296    }
297
298    pub fn dispose(&self) {
299        self.object.dispose();
300    }
301
302    /// The name of the concrete class, for messages.
303    pub fn type_name(&self) -> &'static str {
304        match &self.kind {
305            CompositionShapeKind::Sprite(_) => "CompositionSpriteShape",
306            CompositionShapeKind::Container(_) => "CompositionContainerShape",
307        }
308    }
309
310    /// The point the shape is scaled and rotated about.
311    pub fn center_point(&self) -> Vector {
312        self.shape_props().center_point()
313    }
314
315    pub fn set_center_point(&self, value: Vector) {
316        self.shape_props().set_center_point(self, value)
317    }
318
319    /// The offset of the shape in the space of its parent.
320    pub fn offset(&self) -> Vector {
321        self.shape_props().offset()
322    }
323
324    pub fn set_offset(&self, value: Vector) {
325        self.shape_props().set_offset(self, value)
326    }
327
328    /// The rotation of the shape about its center point, in radians.
329    pub fn rotation_angle(&self) -> f32 {
330        self.shape_props().rotation_angle()
331    }
332
333    pub fn set_rotation_angle(&self, value: f32) {
334        self.shape_props().set_rotation_angle(self, value)
335    }
336
337    /// The rotation of the shape in degrees: a view of
338    /// [`rotation_angle`](Self::rotation_angle).
339    ///
340    /// Deviation (winui-composition.md): not an animation target, as for the
341    /// clips.
342    pub fn rotation_angle_in_degrees(&self) -> f32 {
343        self.rotation_angle().to_degrees()
344    }
345
346    pub fn set_rotation_angle_in_degrees(&self, value: f32) {
347        self.set_rotation_angle(value.to_radians())
348    }
349
350    /// The scale of the shape about its center point (default `(1, 1)`).
351    pub fn scale(&self) -> Vector {
352        self.shape_props().scale()
353    }
354
355    pub fn set_scale(&self, value: Vector) {
356        self.shape_props().set_scale(self, value)
357    }
358
359    /// The transform matrix of the shape (WinUI `Matrix3x2`).
360    pub fn transform_matrix(&self) -> Matrix {
361        self.shape_props().transform_matrix()
362    }
363
364    pub fn set_transform_matrix(&self, value: Matrix) {
365        self.shape_props().set_transform_matrix(self, value)
366    }
367}
368
369impl CompositionShapeHooks for CompositionShape {}
370
371impl CompositionSpriteShapeHooks for CompositionShape {}
372
373impl CompositionContainerShapeHooks for CompositionShape {}
374
375composition_object_impls!(
376    CompositionShape, "CompositionShape",
377    serialize: |this, writer| match &this.kind {
378        CompositionShapeKind::Sprite(props) => props.serialize_changes_core(this, writer),
379        CompositionShapeKind::Container(props) => props.serialize_changes_core(this, writer),
380    },
381    start_animation: |this, name, animation, final_value| match &this.kind {
382        CompositionShapeKind::Sprite(props) => props.start_animation(this, name, animation, final_value),
383        CompositionShapeKind::Container(props) => props.start_animation(this, name, animation, final_value),
384    },
385    composition_property: |this, name| match &this.kind {
386        CompositionShapeKind::Sprite(_) => ServerCompositionSpriteShapeProps::get_composition_property(name),
387        CompositionShapeKind::Container(_) => ServerCompositionContainerShapeProps::get_composition_property(name),
388    },
389);
390
391/// A shape that fills and strokes a geometry (WinUI
392/// `Microsoft.UI.Composition.CompositionSpriteShape`).
393///
394/// The fill and the stroke are composition brushes: the colour and gradient
395/// brushes are brushes of the drawing context, a
396/// [`CompositionSurfaceBrush`](super::CompositionSurfaceBrush) is painted
397/// inside the geometry (the fill) or inside the widened outline (the
398/// stroke). `StrokeThickness`, `StrokeDashOffset` and `StrokeMiterLimit` are
399/// animatable.
400///
401/// Deviation (winui-composition.md): the platform pen has one line cap, so
402/// `stroke_start_cap` caps both ends of the stroke and the dashes;
403/// `stroke_end_cap` and `stroke_dash_cap` are stored only. A non-scaling
404/// stroke divides the thickness by the scale of the transforms of the shape
405/// (the square root of the determinant), which is exact for a uniform scale
406/// only.
407#[derive(Clone)]
408pub struct CompositionSpriteShape(Rc<CompositionShape>);
409
410impl Deref for CompositionSpriteShape {
411    type Target = Rc<CompositionShape>;
412
413    fn deref(&self) -> &Rc<CompositionShape> {
414        &self.0
415    }
416}
417
418impl CompositionSpriteShape {
419    pub(crate) fn new(compositor: &Rc<Compositor>) -> CompositionSpriteShape {
420        CompositionSpriteShape(CompositionShape::create(
421            compositor,
422            CompositionShapeKind::Sprite(CompositionSpriteShapeProps::new()),
423            None,
424            |c| ServerCompositionSpriteShape::new(c),
425        ))
426    }
427
428    pub(crate) fn with_geometry(
429        compositor: &Rc<Compositor>,
430        geometry: Option<Rc<CompositionGeometry>>,
431    ) -> CompositionSpriteShape {
432        let shape = Self::new(compositor);
433        shape.set_geometry(geometry);
434        shape
435    }
436
437    /// The handle of `shape` as a sprite shape, if it is one.
438    pub fn from_shape(shape: &Rc<CompositionShape>) -> Option<CompositionSpriteShape> {
439        matches!(shape.kind, CompositionShapeKind::Sprite(_)).then(|| CompositionSpriteShape(shape.clone()))
440    }
441
442    /// The shape as a [`CompositionShape`].
443    pub fn shape(&self) -> &Rc<CompositionShape> {
444        &self.0
445    }
446
447    fn props(&self) -> &CompositionSpriteShapeProps {
448        match &self.0.kind {
449            CompositionShapeKind::Sprite(props) => props,
450            _ => unreachable!("a sprite shape handle wraps a sprite shape"),
451        }
452    }
453
454    /// The brush the geometry is filled with.
455    pub fn fill_brush(&self) -> Option<Rc<CompositionBrush>> {
456        self.props().fill_brush().and_then(|brush| brush.into_any_rc().downcast::<CompositionBrush>().ok())
457    }
458
459    pub fn set_fill_brush(&self, value: Option<Rc<CompositionBrush>>) {
460        self.props().set_fill_brush(&*self.0, value.map(|brush| brush as Rc<dyn ICompositionObject>))
461    }
462
463    /// The geometry the shape draws.
464    pub fn geometry(&self) -> Option<Rc<CompositionGeometry>> {
465        self.props().geometry().and_then(|geometry| geometry.into_any_rc().downcast::<CompositionGeometry>().ok())
466    }
467
468    pub fn set_geometry(&self, value: Option<Rc<CompositionGeometry>>) {
469        self.props().set_geometry(&*self.0, value.map(|geometry| geometry as Rc<dyn ICompositionObject>))
470    }
471
472    /// Whether the thickness of the stroke ignores the transforms of the
473    /// shape (and of the view box).
474    pub fn is_stroke_non_scaling(&self) -> bool {
475        self.props().is_stroke_non_scaling()
476    }
477
478    pub fn set_is_stroke_non_scaling(&self, value: bool) {
479        self.props().set_is_stroke_non_scaling(&*self.0, value)
480    }
481
482    /// The brush the outline is stroked with.
483    pub fn stroke_brush(&self) -> Option<Rc<CompositionBrush>> {
484        self.props().stroke_brush().and_then(|brush| brush.into_any_rc().downcast::<CompositionBrush>().ok())
485    }
486
487    pub fn set_stroke_brush(&self, value: Option<Rc<CompositionBrush>>) {
488        self.props().set_stroke_brush(&*self.0, value.map(|brush| brush as Rc<dyn ICompositionObject>))
489    }
490
491    /// The lengths of the dashes and gaps of the stroke, in multiples of the
492    /// stroke thickness (WinUI `CompositionStrokeDashArray`); empty for a
493    /// solid stroke.
494    ///
495    /// Deviation (winui-composition.md): a plain list of values, replaced as a
496    /// whole, not a `CompositionObject` of its own.
497    pub fn stroke_dash_array(&self) -> Vec<f32> {
498        self.props().stroke_dash_array().map(|dashes| (*dashes).clone()).unwrap_or_default()
499    }
500
501    pub fn set_stroke_dash_array(&self, value: Vec<f32>) {
502        let value = if value.is_empty() { None } else { Some(Arc::new(value)) };
503        self.props().set_stroke_dash_array(&*self.0, value)
504    }
505
506    /// The cap of the ends of the dashes (stored only, see the type).
507    pub fn stroke_dash_cap(&self) -> CompositionStrokeCap {
508        self.props().stroke_dash_cap()
509    }
510
511    pub fn set_stroke_dash_cap(&self, value: CompositionStrokeCap) {
512        self.props().set_stroke_dash_cap(&*self.0, value)
513    }
514
515    /// How far into the dash pattern the stroke starts, in multiples of the
516    /// stroke thickness.
517    pub fn stroke_dash_offset(&self) -> f32 {
518        self.props().stroke_dash_offset()
519    }
520
521    pub fn set_stroke_dash_offset(&self, value: f32) {
522        self.props().set_stroke_dash_offset(&*self.0, value)
523    }
524
525    /// The cap of the end of the stroke (stored only, see the type).
526    pub fn stroke_end_cap(&self) -> CompositionStrokeCap {
527        self.props().stroke_end_cap()
528    }
529
530    pub fn set_stroke_end_cap(&self, value: CompositionStrokeCap) {
531        self.props().set_stroke_end_cap(&*self.0, value)
532    }
533
534    /// How the segments of the stroke are joined (default `Miter`).
535    pub fn stroke_line_join(&self) -> CompositionStrokeLineJoin {
536        self.props().stroke_line_join()
537    }
538
539    pub fn set_stroke_line_join(&self, value: CompositionStrokeLineJoin) {
540        self.props().set_stroke_line_join(&*self.0, value)
541    }
542
543    /// The limit of the ratio of the miter length to half the thickness
544    /// (default 2).
545    pub fn stroke_miter_limit(&self) -> f32 {
546        self.props().stroke_miter_limit()
547    }
548
549    pub fn set_stroke_miter_limit(&self, value: f32) {
550        self.props().set_stroke_miter_limit(&*self.0, value)
551    }
552
553    /// The cap of the start of the stroke; the platform pen caps both ends
554    /// and the dashes with it.
555    pub fn stroke_start_cap(&self) -> CompositionStrokeCap {
556        self.props().stroke_start_cap()
557    }
558
559    pub fn set_stroke_start_cap(&self, value: CompositionStrokeCap) {
560        self.props().set_stroke_start_cap(&*self.0, value)
561    }
562
563    /// The thickness of the stroke (default 1).
564    pub fn stroke_thickness(&self) -> f32 {
565        self.props().stroke_thickness()
566    }
567
568    pub fn set_stroke_thickness(&self, value: f32) {
569        self.props().set_stroke_thickness(&*self.0, value)
570    }
571}
572
573/// A shape that groups shapes under its transform (WinUI
574/// `Microsoft.UI.Composition.CompositionContainerShape`): its shapes are
575/// drawn in order, in its space.
576#[derive(Clone)]
577pub struct CompositionContainerShape(Rc<CompositionShape>);
578
579impl Deref for CompositionContainerShape {
580    type Target = Rc<CompositionShape>;
581
582    fn deref(&self) -> &Rc<CompositionShape> {
583        &self.0
584    }
585}
586
587impl CompositionContainerShape {
588    pub(crate) fn new(compositor: &Rc<Compositor>) -> CompositionContainerShape {
589        let shapes = CompositionShapeCollection::new(compositor);
590        CompositionContainerShape(CompositionShape::create(
591            compositor,
592            CompositionShapeKind::Container(CompositionContainerShapeProps::new()),
593            Some(shapes),
594            |c| ServerCompositionContainerShape::new(c),
595        ))
596    }
597
598    /// The handle of `shape` as a container shape, if it is one.
599    pub fn from_shape(shape: &Rc<CompositionShape>) -> Option<CompositionContainerShape> {
600        matches!(shape.kind, CompositionShapeKind::Container(_)).then(|| CompositionContainerShape(shape.clone()))
601    }
602
603    /// The shape as a [`CompositionShape`].
604    pub fn shape(&self) -> &Rc<CompositionShape> {
605        &self.0
606    }
607
608    /// The shapes of the container.
609    pub fn shapes(&self) -> &Rc<CompositionShapeCollection> {
610        self.0.shapes.as_ref().expect("a container shape has a shape collection")
611    }
612}