Skip to main content

ferroui_base/media/
pen.rs

1use crate::media::immutable::{ImmutableDashStyle, ImmutablePen};
2use crate::media::{
3    BrushExtensions, DashStyle, IBrush, IDashStyle, IPen, MediaCollection, PenLineCap, PenLineJoin, SolidColorBrush,
4};
5use crate::media::ref_adapter::RefAdapter;
6use crate::reactive::{Disposable, IDisposable};
7use crate::rendering::composition::drawing::{
8    brush_get_server_resource, CompositorResourceHolder, ICompositionRenderResource, ServerCompositionSimplePen,
9};
10use crate::rendering::composition::generated::ServerCompositionSimplePenProps;
11use crate::rendering::composition::server::{IServerObject, ServerObjectId};
12use crate::rendering::composition::transport::BatchStreamWriter;
13use crate::rendering::composition::{Compositor, ICompositorSerializable};
14use crate::utilities::HandlerList;
15use crate::{
16    ferro_class, ferro_property, instantiate, FerroObject, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
17    FerroPropertyChangedEventArgs, Ref, StyledProperty,
18};
19use std::cell::RefCell;
20use std::rc::Rc;
21
22/// The dashes handed to [`Pen::try_modify_or_create`].
23pub enum StrokeDashArray<'a> {
24    /// A plain list: the created dash style is immutable.
25    List(&'a [f64]),
26    /// An observable list: the created dash style is mutable and shares the
27    /// list.
28    Observable(&'a MediaCollection<f64>),
29}
30
31type DashStyleSubscription = (Ref<DashStyle>, Rc<dyn IDisposable>);
32
33/// Describes how a stroke is drawn.
34#[repr(C)]
35pub struct Pen {
36    base: FerroObject,
37    invalidated: HandlerList<dyn Fn()>,
38    subscribed_to_dashes: RefCell<Option<DashStyleSubscription>>,
39    resource: CompositorResourceHolder,
40}
41
42ferro_class!(Pen: FerroObject);
43crate::ferro_class_info!(Pen { new: Pen::new, interfaces: [std::rc::Rc<dyn crate::media::IPen>] });
44
45impl FerroObjectImpl for Pen {
46    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
47        this.register_for_serialization();
48        this.raise_invalidated();
49
50        if change.property() == Self::brush_property().as_property() {
51            let (old_value, new_value) = change.get_old_and_new_value::<Option<Rc<dyn IBrush>>>();
52            this.resource.process_property_change_notification(
53                old_value.as_ref().and_then(|b| b.as_composition_render_resource()),
54                new_value.as_ref().and_then(|b| b.as_composition_render_resource()),
55            );
56        }
57
58        if change.property() == Self::dash_style_property().as_property() {
59            this.update_dash_style_subscription();
60        }
61        Self::parent_on_property_changed(this, change);
62    }
63}
64
65crate::ferro_properties! { impl Pen {
66    ferro_property!(pub fn brush_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
67        FerroProperty::register::<Pen, _>("Brush", None)
68    });
69
70    ferro_property!(pub fn thickness_property() -> StyledProperty<f64> {
71        FerroProperty::register::<Pen, _>("Thickness", 1.0)
72    });
73
74    ferro_property!(pub fn dash_style_property() -> StyledProperty<Option<Rc<dyn IDashStyle>>> {
75        FerroProperty::register::<Pen, _>("DashStyle", None)
76    });
77
78    ferro_property!(pub fn line_cap_property() -> StyledProperty<PenLineCap> {
79        FerroProperty::register::<Pen, _>("LineCap", PenLineCap::Flat)
80    });
81
82    ferro_property!(pub fn line_join_property() -> StyledProperty<PenLineJoin> {
83        FerroProperty::register::<Pen, _>("LineJoin", PenLineJoin::Bevel)
84    });
85
86    ferro_property!(pub fn miter_limit_property() -> StyledProperty<f64> {
87        FerroProperty::register::<Pen, _>("MiterLimit", 10.0)
88    });
89} }
90
91impl Pen {
92    /// Creates the class data.
93    pub fn construct() -> Self {
94        Self {
95            base: FerroObject::construct(),
96            invalidated: HandlerList::new(),
97            subscribed_to_dashes: RefCell::new(None),
98            resource: CompositorResourceHolder::new(),
99        }
100    }
101
102    /// Creates a pen with default values.
103    pub fn new() -> Ref<Self> {
104        instantiate(Self::construct())
105    }
106
107    /// Creates a solid pen with the given brush and thickness: no dashes,
108    /// flat caps, miter joins and a miter limit of 10.
109    pub fn with_brush(brush: Option<Rc<dyn IBrush>>, thickness: f64) -> Ref<Self> {
110        Self::with_all(brush, thickness, None, PenLineCap::Flat, PenLineJoin::Miter, 10.0)
111    }
112
113    /// Creates a solid pen with the color given as an `0xAARRGGBB` value.
114    pub fn from_uint32(color: u32, thickness: f64) -> Ref<Self> {
115        Self::with_brush(Some(SolidColorBrush::from_uint32(color).into()), thickness)
116    }
117
118    /// Creates a pen.
119    pub fn with_all(
120        brush: Option<Rc<dyn IBrush>>,
121        thickness: f64,
122        dash_style: Option<Rc<dyn IDashStyle>>,
123        line_cap: PenLineCap,
124        line_join: PenLineJoin,
125        miter_limit: f64,
126    ) -> Ref<Self> {
127        let result = Self::new();
128        result.set_brush(brush);
129        result.set_thickness(thickness);
130        result.set_line_cap(line_cap);
131        result.set_line_join(line_join);
132        result.set_miter_limit(miter_limit);
133        result.set_dash_style(dash_style);
134        result
135    }
136
137    /// The brush used to draw the stroke.
138    pub fn brush(&self) -> Option<Rc<dyn IBrush>> {
139        self.get_value(Self::brush_property())
140    }
141
142    pub fn set_brush(&self, value: Option<Rc<dyn IBrush>>) {
143        self.set_value(Self::brush_property(), value)
144    }
145
146    /// The stroke thickness.
147    pub fn thickness(&self) -> f64 {
148        self.get_value(Self::thickness_property())
149    }
150
151    pub fn set_thickness(&self, value: f64) {
152        self.set_value(Self::thickness_property(), value)
153    }
154
155    /// The style of dashed lines drawn with the pen.
156    pub fn dash_style(&self) -> Option<Rc<dyn IDashStyle>> {
157        self.get_value(Self::dash_style_property())
158    }
159
160    pub fn set_dash_style(&self, value: Option<Rc<dyn IDashStyle>>) {
161        self.set_value(Self::dash_style_property(), value)
162    }
163
164    /// The type of shape to use on both ends of a line.
165    pub fn line_cap(&self) -> PenLineCap {
166        self.get_value(Self::line_cap_property())
167    }
168
169    pub fn set_line_cap(&self, value: PenLineCap) {
170        self.set_value(Self::line_cap_property(), value)
171    }
172
173    /// A value describing how to join consecutive line or curve segments.
174    pub fn line_join(&self) -> PenLineJoin {
175        self.get_value(Self::line_join_property())
176    }
177
178    pub fn set_line_join(&self, value: PenLineJoin) {
179        self.set_value(Self::line_join_property(), value)
180    }
181
182    /// The limit of the ratio of the miter length to half this pen's
183    /// thickness.
184    pub fn miter_limit(&self) -> f64 {
185        self.get_value(Self::miter_limit_property())
186    }
187
188    pub fn set_miter_limit(&self, value: f64) {
189        self.set_value(Self::miter_limit_property(), value)
190    }
191
192    /// Creates an immutable clone of the pen.
193    pub fn to_immutable(&self) -> ImmutablePen {
194        ImmutablePen::new(
195            self.brush().map(|brush| BrushExtensions::to_immutable(&brush)),
196            self.thickness(),
197            self.dash_style().map(|style| BrushExtensions::dash_style_to_immutable(&style)),
198            self.line_cap(),
199            self.line_join(),
200            self.miter_limit(),
201        )
202    }
203
204    /// Smart reuse and update of a pen.
205    ///
206    /// If `brush` is `None`, `pen` is cleared. If the brush and the dashes
207    /// are immutable, a new immutable pen is created. Otherwise the existing
208    /// mutable pen is updated, or a new mutable pen is created.
209    ///
210    /// Returns true if `pen` was created or cleared, false if the existing
211    /// pen was modified in place.
212    #[allow(clippy::too_many_arguments)]
213    pub fn try_modify_or_create(
214        pen: &mut Option<Rc<dyn IPen>>,
215        brush: Option<Rc<dyn IBrush>>,
216        thickness: f64,
217        stroke_dash_array: Option<StrokeDashArray<'_>>,
218        stroke_dash_offset: f64,
219        line_cap: PenLineCap,
220        line_join: PenLineJoin,
221        miter_limit: f64,
222    ) -> bool {
223        let previous_pen = pen.clone();
224
225        let Some(brush) = brush else {
226            *pen = None;
227            return previous_pen.is_some();
228        };
229
230        let mut dash_style: Option<Rc<dyn IDashStyle>> = None;
231        let mut immutable_dash_style: Option<Rc<ImmutableDashStyle>> = None;
232        match stroke_dash_array {
233            // The list supports notification: create a mutable dash style.
234            Some(StrokeDashArray::Observable(list)) if !list.is_empty() => {
235                dash_style = Some(DashStyle::with_dash_list(list.clone(), stroke_dash_offset).into());
236            }
237            Some(StrokeDashArray::List(list)) if !list.is_empty() => {
238                let style = Rc::new(ImmutableDashStyle::new(Some(list), stroke_dash_offset));
239                immutable_dash_style = Some(style.clone());
240                dash_style = Some(style);
241            }
242            _ => {}
243        }
244
245        if let Some(immutable_brush) = brush.clone().into_immutable_brush() {
246            if dash_style.is_none() || immutable_dash_style.is_some() {
247                *pen = Some(Rc::new(ImmutablePen::new(
248                    Some(immutable_brush),
249                    thickness,
250                    immutable_dash_style,
251                    line_cap,
252                    line_join,
253                    miter_limit,
254                )));
255                return true;
256            }
257        }
258
259        let mutable_pen = previous_pen
260            .as_ref()
261            .and_then(|p| p.as_object())
262            .and_then(|o| o.downcast_ref::<Pen>())
263            .map(Pen::to_ref)
264            .unwrap_or_else(Pen::new);
265        mutable_pen.set_brush(Some(brush));
266        mutable_pen.set_thickness(thickness);
267        mutable_pen.set_line_cap(line_cap);
268        mutable_pen.set_line_join(line_join);
269        mutable_pen.set_dash_style(dash_style);
270        mutable_pen.set_miter_limit(miter_limit);
271
272        let new_pen: Rc<dyn IPen> = mutable_pen.into();
273        let changed = match &previous_pen {
274            Some(previous) => !previous.equals(&*new_pen),
275            None => true,
276        };
277        *pen = Some(new_pen);
278        changed
279    }
280
281    /// Subscribes to invalidation of the pen: raised whenever a change
282    /// requires strokes drawn with the pen to be redrawn. Disposing the
283    /// returned handle unsubscribes.
284    pub fn invalidated(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
285        let token = self.invalidated.add(Rc::new(handler));
286        let weak = self.to_ref().downgrade();
287        Disposable::create(move || {
288            if let Some(this) = weak.upgrade() {
289                this.invalidated.remove(token);
290            }
291        })
292    }
293
294    fn raise_invalidated(&self) {
295        if self.invalidated.is_empty() {
296            return;
297        }
298        for (_, handler) in self.invalidated.snapshot().iter() {
299            handler();
300        }
301    }
302
303    /// The object as something a compositor serializes: the adapter keeps
304    /// the object alive while it is queued.
305    pub(crate) fn as_compositor_serializable(&self) -> Rc<dyn ICompositorSerializable> {
306        Rc::new(RefAdapter(self.to_ref()))
307    }
308
309    fn register_for_serialization(&self) {
310        if self.resource.is_attached() {
311            let serializable = self.as_compositor_serializable();
312            self.resource.register_for_invalidation_on_all_compositors(&serializable);
313        }
314    }
315
316    fn update_dash_style_subscription(&self) {
317        let new_value: Option<Ref<DashStyle>> = self
318            .dash_style()
319            .as_ref()
320            .and_then(|style| style.as_object())
321            .and_then(|o| o.downcast_ref::<DashStyle>())
322            .map(DashStyle::to_ref);
323
324        let same = match (&*self.subscribed_to_dashes.borrow(), &new_value) {
325            (Some((old, _)), Some(new)) => old.ptr_eq(new),
326            (None, None) => true,
327            _ => false,
328        };
329        if same {
330            return;
331        }
332
333        if let Some((_, subscription)) = self.subscribed_to_dashes.take() {
334            subscription.dispose();
335        }
336
337        if let Some(new_value) = new_value {
338            let weak = self.to_ref().downgrade();
339            let subscription = new_value.invalidated(move || {
340                if let Some(target) = weak.upgrade() {
341                    target.register_for_serialization();
342                    target.raise_invalidated();
343                }
344            });
345            *self.subscribed_to_dashes.borrow_mut() = Some((new_value, subscription));
346        }
347    }
348}
349
350impl ICompositionRenderResource for Pen {
351    fn add_ref_on_compositor(&self, c: &Rc<Compositor>) {
352        let owner = self.as_compositor_serializable();
353        let (_, created) = self.resource.create_or_add_ref(c, Some(owner), |c| {
354            c.create_server_object(|server, _| ServerCompositionSimplePen::new(server) as Rc<dyn IServerObject>)
355        });
356        if created {
357            if let Some(brush) = self.brush() {
358                if let Some(brush) = brush.as_composition_render_resource() {
359                    brush.add_ref_on_compositor(c);
360                }
361            }
362            self.update_dash_style_subscription();
363        }
364    }
365
366    fn release_on_compositor(&self, c: &Rc<Compositor>) {
367        if self.resource.release(c) {
368            if let Some(brush) = self.brush() {
369                if let Some(brush) = brush.as_composition_render_resource() {
370                    brush.release_on_compositor(c);
371                }
372            }
373            self.update_dash_style_subscription();
374        }
375    }
376
377    fn get_for_compositor(&self, c: &Compositor) -> ServerObjectId {
378        self.resource.get_for_compositor(c)
379    }
380}
381
382impl ICompositorSerializable for RefAdapter<Pen> {
383    fn try_get_server(&self, c: &Compositor) -> Option<ServerObjectId> {
384        self.0.resource.try_get_for_compositor(c)
385    }
386
387    fn serialization_key(&self) -> *const () {
388        RefAdapter::reference_id(self)
389    }
390
391    fn serialize_changes(&self, c: &Compositor, writer: &mut BatchStreamWriter<'_>) {
392        let pen = &self.0;
393        ServerCompositionSimplePenProps::serialize_all_changes(
394            writer,
395            brush_get_server_resource(pen.brush().as_ref(), Some(c)),
396            // A dash style is sent as a value of its own in a shared handle.
397            pen.dash_style().map(|style| std::sync::Arc::new((*style.into_immutable_dash_style()).clone())),
398            pen.line_cap(),
399            pen.line_join(),
400            pen.miter_limit(),
401            pen.thickness(),
402        );
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use crate::media::immutable::ImmutableSolidColorBrush;
410    use crate::media::Colors;
411    use std::cell::Cell;
412
413    fn assert_invalidated(target: &Ref<Pen>, action: impl FnOnce()) {
414        let raised = Rc::new(Cell::new(false));
415        let r = raised.clone();
416        let subscription = target.invalidated(move || r.set(true));
417        action();
418        assert!(raised.get());
419        subscription.dispose();
420    }
421
422    fn immutable_test_pen() -> Rc<dyn IPen> {
423        Rc::new(ImmutablePen::new(
424            Some(Rc::new(ImmutableSolidColorBrush::new(Colors::RED))),
425            2.0,
426            Some(Rc::new(ImmutableDashStyle::new(Some(&[0.1, 0.2]), 5.0))),
427            PenLineCap::Round,
428            PenLineJoin::Round,
429            21.0,
430        ))
431    }
432
433    fn modify(pen: &mut Option<Rc<dyn IPen>>, brush: Option<Rc<dyn IBrush>>) -> bool {
434        Pen::try_modify_or_create(pen, brush, 2.0, None, 0.0, PenLineCap::Flat, PenLineJoin::Miter, 10.0)
435    }
436
437    #[test]
438    fn changing_thickness_raises_invalidated() {
439        let target = Pen::new();
440        assert_invalidated(&target, || target.set_thickness(18.0));
441    }
442
443    #[test]
444    fn changing_dash_style_dashes_raises_invalidated() {
445        let dashes = DashStyle::new();
446        let target = Pen::new();
447        target.set_dash_style(Some((&dashes).into()));
448        assert_invalidated(&target, || dashes.set_dashes(Some(MediaCollection::from_items([0.1, 0.2]))));
449    }
450
451    #[test]
452    fn adding_dash_style_dashes_raises_invalidated() {
453        let dashes = DashStyle::new();
454        let target = Pen::new();
455        target.set_dash_style(Some((&dashes).into()));
456        assert_invalidated(&target, || dashes.set_dashes(Some(MediaCollection::from_items([0.3]))));
457    }
458
459    #[test]
460    fn adding_dash_style_dash_raises_invalidated() {
461        let dashes = DashStyle::new();
462        let target = Pen::new();
463        target.set_dash_style(Some((&dashes).into()));
464        dashes.set_dashes(Some(MediaCollection::from_items([0.3])));
465        assert_invalidated(&target, || dashes.dashes().unwrap().add_range([1.0, 2.0]));
466    }
467
468    #[test]
469    fn replaced_dash_style_no_longer_invalidates() {
470        let dashes = DashStyle::new();
471        let target = Pen::new();
472        target.set_dash_style(Some((&dashes).into()));
473        target.set_dash_style(None);
474        let raised = Rc::new(Cell::new(false));
475        let r = raised.clone();
476        target.invalidated(move || r.set(true));
477        dashes.set_offset(3.0);
478        assert!(!raised.get());
479    }
480
481    #[test]
482    fn equality_is_implemented_between_immutable_and_mutable_pens() {
483        let brush: Rc<ImmutableSolidColorBrush> = Rc::new(ImmutableSolidColorBrush::new(Colors::RED));
484        let target1: Rc<dyn IPen> = Rc::new(ImmutablePen::new(
485            Some(brush.clone()),
486            2.0,
487            Some(DashStyle::dash()),
488            PenLineCap::Round,
489            PenLineJoin::Round,
490            21.0,
491        ));
492        let target2: Rc<dyn IPen> = Pen::with_all(
493            Some(brush),
494            2.0,
495            Some(DashStyle::dash()),
496            PenLineCap::Round,
497            PenLineJoin::Round,
498            21.0,
499        )
500        .into();
501
502        assert!(target1 == target2);
503    }
504
505    #[test]
506    fn equality_is_implemented_between_mutable_and_immutable_dash_styles() {
507        let brush: Rc<ImmutableSolidColorBrush> = Rc::new(ImmutableSolidColorBrush::new(Colors::RED));
508        let target1: Rc<dyn IPen> = Rc::new(ImmutablePen::new(
509            Some(brush.clone()),
510            2.0,
511            Some(Rc::new(ImmutableDashStyle::new(Some(&[0.1, 0.2]), 5.0))),
512            PenLineCap::Round,
513            PenLineJoin::Round,
514            21.0,
515        ));
516        let target2: Rc<dyn IPen> = Pen::with_all(
517            Some(brush),
518            2.0,
519            Some(DashStyle::with_dashes(Some(&[0.1, 0.2]), 5.0).into()),
520            PenLineCap::Round,
521            PenLineJoin::Round,
522            21.0,
523        )
524        .into();
525
526        assert!(target1 == target2);
527    }
528
529    #[test]
530    fn try_modify_or_create_should_return_true_when_previous_exists_and_assign_null_when_brush_is_null() {
531        let mut target = Some(immutable_test_pen());
532        let result = modify(&mut target, None);
533        assert!(result);
534        assert!(target.is_none());
535    }
536
537    #[test]
538    fn try_modify_or_create_should_return_false_when_previous_not_exists_and_assign_null_when_brush_is_null() {
539        let mut target: Option<Rc<dyn IPen>> = None;
540        let result = modify(&mut target, None);
541        assert!(!result);
542        assert!(target.is_none());
543    }
544
545    #[test]
546    fn try_modify_or_create_should_return_true_when_previous_immutable_and_assign_mutable_when_brush_is_mutable() {
547        let mut target = Some(immutable_test_pen());
548        let result = modify(&mut target, Some(SolidColorBrush::with_color(Colors::BLUE).into()));
549        assert!(result);
550        assert!(target.unwrap().as_object().unwrap().is::<Pen>());
551    }
552
553    #[test]
554    fn try_modify_or_create_should_return_true_when_previous_immutable_and_assign_immutable_when_brush_is_immutable()
555    {
556        let mut target = Some(immutable_test_pen());
557        let result = modify(&mut target, Some(Rc::new(ImmutableSolidColorBrush::new(Colors::BLUE))));
558        assert!(result);
559        assert!(target.unwrap().as_any().is::<ImmutablePen>());
560    }
561
562    #[test]
563    fn try_modify_or_create_should_return_false_when_previous_mutable_and_modify_mutable_when_brush_is_mutable() {
564        let old_pen = Pen::with_all(
565            Some(SolidColorBrush::with_color(Colors::RED).into()),
566            2.0,
567            Some(Rc::new(ImmutableDashStyle::new(Some(&[0.1, 0.2]), 5.0))),
568            PenLineCap::Round,
569            PenLineJoin::Round,
570            21.0,
571        );
572        let mut target: Option<Rc<dyn IPen>> = Some((&old_pen).into());
573        let result = modify(&mut target, Some(SolidColorBrush::with_color(Colors::BLUE).into()));
574        assert!(!result);
575        assert!(target.unwrap().as_object().unwrap().downcast_ref::<Pen>().unwrap().to_ref().ptr_eq(&old_pen));
576    }
577
578    #[test]
579    fn try_modify_or_create_uses_dash_array_kind() {
580        let mut target: Option<Rc<dyn IPen>> = None;
581        let brush: Rc<dyn IBrush> = Rc::new(ImmutableSolidColorBrush::new(Colors::BLUE));
582        assert!(Pen::try_modify_or_create(
583            &mut target,
584            Some(brush.clone()),
585            2.0,
586            Some(StrokeDashArray::List(&[1.0, 2.0])),
587            3.0,
588            PenLineCap::Flat,
589            PenLineJoin::Miter,
590            10.0
591        ));
592        let pen = target.clone().unwrap();
593        assert!(pen.as_any().is::<ImmutablePen>());
594        assert_eq!(Some(vec![1.0, 2.0]), pen.dash_style().unwrap().dashes());
595        assert_eq!(3.0, pen.dash_style().unwrap().offset());
596
597        // The immutable pen is replaced by a mutable one. The result is false
598        // because the previous (immutable) pen compares structurally equal
599        // to the new pen.
600        let list = MediaCollection::from_items([1.0, 2.0]);
601        assert!(!Pen::try_modify_or_create(
602            &mut target,
603            Some(brush),
604            2.0,
605            Some(StrokeDashArray::Observable(&list)),
606            3.0,
607            PenLineCap::Flat,
608            PenLineJoin::Miter,
609            10.0
610        ));
611        let pen = target.unwrap();
612        assert!(pen.as_object().unwrap().is::<Pen>());
613        assert!(pen.dash_style().unwrap().as_object().unwrap().is::<DashStyle>());
614    }
615
616    #[test]
617    fn to_immutable_copies_values() {
618        let pen = Pen::with_all(
619            Some(SolidColorBrush::with_color(Colors::RED).into()),
620            3.0,
621            Some(DashStyle::with_dashes(Some(&[1.0, 2.0]), 4.0).into()),
622            PenLineCap::Square,
623            PenLineJoin::Round,
624            7.0,
625        );
626        let immutable = pen.to_immutable();
627        assert_eq!(3.0, immutable.thickness());
628        assert_eq!(PenLineCap::Square, immutable.line_cap());
629        assert_eq!(PenLineJoin::Round, immutable.line_join());
630        assert_eq!(7.0, immutable.miter_limit());
631        assert_eq!(Some(vec![1.0, 2.0]), immutable.dash_style().unwrap().dashes());
632        assert_eq!(Some(Colors::RED), immutable.brush().unwrap().as_solid_color_brush().map(|b| b.color()));
633        let as_pen: Rc<dyn IPen> = pen.into();
634        assert_eq!(3.0, BrushExtensions::pen_to_immutable(&as_pen).thickness());
635        let immutable: Rc<dyn IPen> = Rc::new(immutable);
636        assert!(Rc::ptr_eq(&BrushExtensions::pen_to_immutable(&immutable), &immutable.clone().into_immutable_pen()));
637    }
638}