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
22pub enum StrokeDashArray<'a> {
24 List(&'a [f64]),
26 Observable(&'a MediaCollection<f64>),
29}
30
31type DashStyleSubscription = (Ref<DashStyle>, Rc<dyn IDisposable>);
32
33#[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 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 pub fn new() -> Ref<Self> {
104 instantiate(Self::construct())
105 }
106
107 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 pub fn from_uint32(color: u32, thickness: f64) -> Ref<Self> {
115 Self::with_brush(Some(SolidColorBrush::from_uint32(color).into()), thickness)
116 }
117
118 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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}