Skip to main content

ferroui_base/media/
brush.rs

1use crate::animation::Animatable;
2use crate::media::immutable::ImmutableSolidColorBrush;
3use crate::media::ref_adapter::RefAdapter;
4use crate::media::{Color, IBrush, ITransform, KnownColors};
5use crate::reactive::{Disposable, IDisposable};
6use crate::rendering::composition::drawing::{
7    transform_get_server, CompositorResourceHolder, ICompositionRenderResource,
8};
9use crate::rendering::composition::generated::ServerCompositionSimpleBrushProps;
10use crate::rendering::composition::server::{IServerObject, ServerCompositor, ServerObjectId};
11use crate::rendering::composition::transport::BatchStreamWriter;
12use crate::rendering::composition::{Compositor, ICompositorSerializable};
13use crate::utilities::{FormatError, HandlerList};
14use crate::{
15    ferro_class, ferro_property, FerroObject, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
16    FerroPropertyChangedEventArgs, RelativePoint, StyledProperty, Upcast,
17};
18use std::rc::Rc;
19
20/// Describes how an area is painted. Abstract base class of the mutable
21/// brushes.
22#[repr(C)]
23pub struct Brush {
24    base: Animatable,
25    invalidated: HandlerList<dyn Fn()>,
26    resource: CompositorResourceHolder,
27}
28
29/// Creates the server-side counterpart of a brush on the render thread.
30pub type ServerBrushFactory = fn(&Rc<ServerCompositor>) -> Rc<dyn IServerObject>;
31
32ferro_class! {
33    Brush: Animatable, virtuals BrushImpl: FerroObjectImpl {
34        /// The factory of the server-side counterpart of the brush. `None`
35        /// for the abstract brush classes only: every brush class that can
36        /// be instantiated has a server-side counterpart.
37        fn factory(this) -> Option<ServerBrushFactory>;
38
39        /// Called when the brush got its counterpart on a compositor.
40        fn on_referenced_from_compositor(this, c: &Rc<Compositor>);
41
42        /// Called when the brush lost its counterpart on a compositor.
43        fn on_unreferenced_from_compositor(this, c: &Rc<Compositor>);
44
45        /// Creates the server objects the state of the brush refers to,
46        /// before the state is written for its counterpart on a compositor
47        /// (see [`ICompositorSerializable::prepare_serialization`]).
48        fn prepare_serialization(this, c: &Rc<Compositor>);
49
50        /// Writes the state of the brush for its counterpart on a
51        /// compositor.
52        fn serialize_changes(this, c: &Compositor, writer: &mut BatchStreamWriter<'_>);
53    }
54}
55crate::ferro_class_info!(Brush { interfaces: [std::rc::Rc<dyn crate::media::IBrush>] });
56
57impl FerroObjectImpl for Brush {
58    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
59        if change.property() == Self::transform_property().as_property()
60            || change.property() == Self::relative_transform_property().as_property()
61        {
62            let (old_value, new_value) = change.get_old_and_new_value::<Option<Rc<dyn ITransform>>>();
63            this.resource.process_property_change_notification(
64                old_value.as_ref().and_then(|t| t.as_composition_render_resource()),
65                new_value.as_ref().and_then(|t| t.as_composition_render_resource()),
66            );
67        }
68
69        this.register_for_serialization();
70        this.raise_invalidated();
71
72        Self::parent_on_property_changed(this, change);
73    }
74}
75
76impl BrushImpl for Brush {
77    fn factory(_this: &Self) -> Option<ServerBrushFactory> {
78        None
79    }
80
81    fn on_referenced_from_compositor(this: &Self, c: &Rc<Compositor>) {
82        if let Some(transform) = this.transform() {
83            if let Some(transform) = transform.as_composition_render_resource() {
84                transform.add_ref_on_compositor(c);
85            }
86        }
87        if let Some(relative_transform) = this.relative_transform() {
88            if let Some(relative_transform) = relative_transform.as_composition_render_resource() {
89                relative_transform.add_ref_on_compositor(c);
90            }
91        }
92    }
93
94    fn on_unreferenced_from_compositor(this: &Self, c: &Rc<Compositor>) {
95        if let Some(transform) = this.transform() {
96            if let Some(transform) = transform.as_composition_render_resource() {
97                transform.release_on_compositor(c);
98            }
99        }
100        if let Some(relative_transform) = this.relative_transform() {
101            if let Some(relative_transform) = relative_transform.as_composition_render_resource() {
102                relative_transform.release_on_compositor(c);
103            }
104        }
105    }
106
107    fn prepare_serialization(_this: &Self, _c: &Rc<Compositor>) {}
108
109    fn serialize_changes(this: &Self, c: &Compositor, writer: &mut BatchStreamWriter<'_>) {
110        ServerCompositionSimpleBrushProps::serialize_all_changes(
111            writer,
112            this.opacity(),
113            this.transform_origin(),
114            transform_get_server(this.transform().as_ref(), Some(c)),
115            transform_get_server(this.relative_transform().as_ref(), Some(c)),
116        );
117    }
118}
119
120impl ICompositionRenderResource for Brush {
121    fn add_ref_on_compositor(&self, c: &Rc<Compositor>) {
122        let Some(factory) = self.factory() else {
123            panic!("{} is not compatible with composition", self.get_type().name());
124        };
125        let owner = self.as_compositor_serializable();
126        let (_, created) = self
127            .resource
128            .create_or_add_ref(c, Some(owner), |c| c.create_server_object(move |server, _| factory(server)));
129        if created {
130            self.on_referenced_from_compositor(c);
131        }
132    }
133
134    fn release_on_compositor(&self, c: &Rc<Compositor>) {
135        if self.resource.release(c) {
136            self.on_unreferenced_from_compositor(c);
137        }
138    }
139
140    fn get_for_compositor(&self, c: &Compositor) -> ServerObjectId {
141        self.resource.get_for_compositor(c)
142    }
143}
144
145impl ICompositorSerializable for RefAdapter<Brush> {
146    fn try_get_server(&self, c: &Compositor) -> Option<ServerObjectId> {
147        self.0.resource.try_get_for_compositor(c)
148    }
149
150    fn serialization_key(&self) -> *const () {
151        RefAdapter::reference_id(self)
152    }
153
154    fn prepare_serialization(&self, c: &Compositor) {
155        self.0.prepare_serialization(&c.this_handle());
156    }
157
158    fn serialize_changes(&self, c: &Compositor, writer: &mut BatchStreamWriter<'_>) {
159        self.0.serialize_changes(c, writer);
160    }
161}
162
163crate::ferro_properties! { impl Brush {
164    ferro_property!(pub fn opacity_property() -> StyledProperty<f64> {
165        FerroProperty::register::<Brush, _>("Opacity", 1.0)
166    });
167
168    ferro_property!(pub fn transform_property() -> StyledProperty<Option<Rc<dyn ITransform>>> {
169        FerroProperty::register::<Brush, _>("Transform", None)
170    });
171
172    ferro_property!(pub fn transform_origin_property() -> StyledProperty<RelativePoint> {
173        FerroProperty::register::<Brush, _>("TransformOrigin", RelativePoint::default())
174    });
175
176    ferro_property!(pub fn relative_transform_property() -> StyledProperty<Option<Rc<dyn ITransform>>> {
177        FerroProperty::register::<Brush, _>("RelativeTransform", None)
178    });
179} }
180
181impl Brush {
182    /// Creates the class data; see [`FerroObject::construct`].
183    pub fn construct() -> Self {
184        Self { base: Animatable::construct(), invalidated: HandlerList::new(), resource: CompositorResourceHolder::new() }
185    }
186
187    /// The opacity of the brush.
188    pub fn opacity(&self) -> f64 {
189        self.get_value(Self::opacity_property())
190    }
191
192    pub fn set_opacity(&self, value: f64) {
193        self.set_value(Self::opacity_property(), value)
194    }
195
196    /// The transform of the brush.
197    pub fn transform(&self) -> Option<Rc<dyn ITransform>> {
198        self.get_value(Self::transform_property())
199    }
200
201    pub fn set_transform(&self, value: Option<Rc<dyn ITransform>>) {
202        self.set_value(Self::transform_property(), value)
203    }
204
205    /// The origin of the brush [`transform`](Self::transform).
206    pub fn transform_origin(&self) -> RelativePoint {
207        self.get_value(Self::transform_origin_property())
208    }
209
210    pub fn set_transform_origin(&self, value: RelativePoint) {
211        self.set_value(Self::transform_origin_property(), value)
212    }
213
214    /// The transform of the brush, relative to the bounds of the painted
215    /// area.
216    pub fn relative_transform(&self) -> Option<Rc<dyn ITransform>> {
217        self.get_value(Self::relative_transform_property())
218    }
219
220    pub fn set_relative_transform(&self, value: Option<Rc<dyn ITransform>>) {
221        self.set_value(Self::relative_transform_property(), value)
222    }
223
224    /// Parses a brush string: a known color name or any color format
225    /// accepted by [`Color::parse`].
226    pub fn parse(s: &str) -> Result<Rc<dyn IBrush>, FormatError> {
227        if !s.is_empty() {
228            // Attempt to get a cached known brush first
229            // This is a performance optimization for known colors
230            if let Some(brush) = KnownColors::get_known_brush(s) {
231                return Ok(brush);
232            }
233
234            if let Some(color) = Color::try_parse(s) {
235                return Ok(Rc::new(ImmutableSolidColorBrush::new(color)));
236            }
237        }
238
239        Err(FormatError::from_string(format!("Invalid brush string: '{s}'.")))
240    }
241
242    /// The object as something a compositor serializes: the adapter keeps
243    /// the object alive while it is queued.
244    pub(crate) fn as_compositor_serializable(&self) -> Rc<dyn ICompositorSerializable> {
245        Rc::new(RefAdapter(self.to_ref()))
246    }
247
248    /// Queues the brush for serialization on every compositor it has a
249    /// server-side counterpart on.
250    pub(crate) fn register_for_serialization(&self) {
251        if self.resource.is_attached() {
252            let serializable = self.as_compositor_serializable();
253            self.resource.register_for_invalidation_on_all_compositors(&serializable);
254        }
255    }
256
257    /// Whether the brush has a server-side counterpart on compositor `c`.
258    pub(crate) fn is_on_compositor(&self, c: &Compositor) -> bool {
259        self.resource.try_get_for_compositor(c).is_some()
260    }
261
262    /// Subscribes to invalidation of the brush: raised whenever a change
263    /// requires the brush to be redrawn. Disposing the returned handle
264    /// unsubscribes.
265    pub fn invalidated(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
266        let token = self.invalidated.add(Rc::new(handler));
267        let object: &FerroObject = self.upcast();
268        let weak = object.to_weak();
269        Disposable::create(move || {
270            if let Some(this) = weak.upgrade() {
271                if let Some(this) = this.downcast_ref::<Brush>() {
272                    this.invalidated.remove(token);
273                }
274            }
275        })
276    }
277
278    /// Raises the invalidated notification.
279    pub(crate) fn raise_invalidated(&self) {
280        if self.invalidated.is_empty() {
281            return;
282        }
283        for (_, handler) in self.invalidated.snapshot().iter() {
284            handler();
285        }
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use crate::media::{BrushExtensions, Colors, ISolidColorBrush, SolidColorBrush};
293    use std::cell::Cell;
294
295    fn solid(brush: &Rc<dyn IBrush>) -> &dyn ISolidColorBrush {
296        brush.as_solid_color_brush().expect("solid color brush")
297    }
298
299    #[test]
300    fn parse_parses_rgb_hash_brush() {
301        let result = Brush::parse("#ff8844").unwrap();
302        let color = solid(&result).color();
303        assert_eq!((0xff, 0x88, 0x44, 0xff), (color.r, color.g, color.b, color.a));
304    }
305
306    #[test]
307    fn parse_parses_argb_hash_brush() {
308        let result = Brush::parse("#40ff8844").unwrap();
309        let color = solid(&result).color();
310        assert_eq!((0xff, 0x88, 0x44, 0x40), (color.r, color.g, color.b, color.a));
311    }
312
313    #[test]
314    fn parse_parses_named_brush_lowercase() {
315        let result = Brush::parse("red").unwrap();
316        let color = solid(&result).color();
317        assert_eq!((0xff, 0x00, 0x00, 0xff), (color.r, color.g, color.b, color.a));
318    }
319
320    #[test]
321    fn parse_parses_named_brush_uppercase() {
322        let result = Brush::parse("RED").unwrap();
323        let color = solid(&result).color();
324        assert_eq!((0xff, 0x00, 0x00, 0xff), (color.r, color.g, color.b, color.a));
325    }
326
327    #[test]
328    fn parse_to_string_named_brush_roundtrip() {
329        let brush = Brush::parse("Red").unwrap();
330        let brush = brush.as_any().downcast_ref::<ImmutableSolidColorBrush>().unwrap();
331        assert_eq!("Red", brush.to_string());
332    }
333
334    #[test]
335    fn parse_hex_value_doesnt_accept_too_few_chars() {
336        assert!(Brush::parse("#ff").is_err());
337    }
338
339    #[test]
340    fn parse_hex_value_doesnt_accept_too_many_chars() {
341        assert!(Brush::parse("#ff5555555").is_err());
342    }
343
344    #[test]
345    fn parse_hex_value_doesnt_accept_invalid_number() {
346        assert!(Brush::parse("#ff808g80").is_err());
347    }
348
349    #[test]
350    fn parse_parses_all_color_format_brushes() {
351        for input in [
352            "rgb(255, 128, 64)",
353            "rgba(255, 128, 64, 0.5)",
354            "hsl(120, 100%, 50%)",
355            "hsla(120, 100%, 50%, 0.5)",
356            "hsv(300, 100%, 25%)",
357            "hsva(300, 100%, 25%, 0.75)",
358            "#40ff8844",
359            "Green",
360        ] {
361            let brush = Brush::parse(input).unwrap();
362            // The color tests already validate all color formats are parsed properly.
363            let expected = Color::parse(input).unwrap();
364            assert_eq!(Some(expected), brush.as_solid_color_brush().map(|b| b.color()));
365        }
366    }
367
368    #[test]
369    fn parse_rejects_empty_string() {
370        assert!(Brush::parse("").is_err());
371    }
372
373    #[test]
374    fn changing_opacity_raises_invalidated() {
375        let target = SolidColorBrush::new();
376        let raised = Rc::new(Cell::new(false));
377        let r = raised.clone();
378        target.invalidated(move || r.set(true));
379        target.set_opacity(0.5);
380        assert!(raised.get());
381    }
382
383    #[test]
384    fn brush_property_value_accepts_mutable_and_immutable_brushes() {
385        let mutable = SolidColorBrush::with_color(Colors::RED);
386        let a: Rc<dyn IBrush> = mutable.clone().into();
387        let b: Rc<dyn IBrush> = (&mutable).into();
388        // Two handles of the same mutable brush are equal; mutable brushes
389        // compare by reference.
390        assert!(*a == *b);
391        let other: Rc<dyn IBrush> = SolidColorBrush::with_color(Colors::RED).into();
392        assert!(*a != *other);
393
394        // Immutable solid color brushes compare structurally.
395        let i1: Rc<dyn IBrush> = Rc::new(ImmutableSolidColorBrush::new(Colors::RED));
396        let i2: Rc<dyn IBrush> = Rc::new(ImmutableSolidColorBrush::new(Colors::RED));
397        assert!(*i1 == *i2);
398        assert!(*i1 != *a);
399        assert!(*a != *i1);
400
401        // Interface casts.
402        assert_eq!(Some(Colors::RED), a.as_solid_color_brush().map(|b| b.color()));
403        assert!(a.as_gradient_brush().is_none());
404        assert!(a.as_mutable_brush().is_some());
405        assert!(i1.as_mutable_brush().is_none());
406        assert!(a.as_object().unwrap().is::<SolidColorBrush>());
407
408        // Conversion to immutable.
409        let immutable = BrushExtensions::to_immutable(&a);
410        assert_eq!(Some(Colors::RED), immutable.as_solid_color_brush().map(|b| b.color()));
411        let same = BrushExtensions::to_immutable(&i1);
412        assert!(same.reference_id() == i1.reference_id());
413
414        // A base-class handle keeps its interfaces.
415        let base: crate::Ref<Brush> = mutable.upcast();
416        let c: Rc<dyn IBrush> = base.into();
417        assert!(c.as_solid_color_brush().is_some());
418        assert!(*c == *a);
419    }
420
421    #[test]
422    fn brush_handles_convert_to_the_brush_interface_as_untyped_values() {
423        use crate::data::core::{ValueType, ValueTypes};
424        use crate::{BoxedValue, Ref, TypeInfo};
425
426        // Created through the type metadata, without naming the class.
427        let object = SolidColorBrush::TYPE.create_instance().expect("default constructor");
428        let brush = object.cast::<SolidColorBrush>().expect("a solid color brush");
429        brush.set_color(Colors::RED);
430        let boxed: BoxedValue = Rc::new(brush);
431
432        let target = ValueType::of::<Option<Rc<dyn IBrush>>>();
433        assert!(ValueTypes::is_assignable(ValueType::of::<Ref<SolidColorBrush>>(), target));
434        let converted = ValueTypes::try_convert(Some(&boxed), target).flatten().expect("converted");
435        let converted = converted.downcast_ref::<Option<Rc<dyn IBrush>>>().unwrap().clone().unwrap();
436        assert_eq!(solid(&converted).color(), Colors::RED);
437
438        // The abstract base class has no default constructor.
439        assert!(Brush::TYPE.default_constructor().is_none());
440        assert!(TypeInfo::find_by_name("SolidColorBrush").is_some());
441    }
442}