Skip to main content

ferroui_base/media/
experimental_acrylic_material.rs

1use crate::media::ref_adapter::RefAdapter;
2use crate::media::{AcrylicBackgroundSource, Color, IExperimentalAcrylicMaterial, ImmutableExperimentalAcrylicMaterial};
3use crate::reactive::{Disposable, IDisposable};
4use crate::utilities::HandlerList;
5use crate::{
6    ferro_class, ferro_property, instantiate, FerroObject, FerroObjectImpl, FerroProperty,
7    ObjectType, Ref, StyledProperty, Upcast,
8};
9use std::cell::Cell;
10use std::rc::Rc;
11
12/// An experimental acrylic-like material.
13#[repr(C)]
14pub struct ExperimentalAcrylicMaterial {
15    base: FerroObject,
16    effective_tint_color: Cell<Color>,
17    effective_luminosity_color: Cell<Color>,
18    invalidated: HandlerList<dyn Fn()>,
19}
20
21ferro_class!(ExperimentalAcrylicMaterial: FerroObject);
22crate::ferro_class_info!(ExperimentalAcrylicMaterial { new: ExperimentalAcrylicMaterial::new, interfaces: [std::rc::Rc<dyn crate::media::IExperimentalAcrylicMaterial>] });
23
24crate::ferro_impl_classes!(ExperimentalAcrylicMaterial: FerroObjectImpl);
25
26crate::ferro_properties! { impl ExperimentalAcrylicMaterial {
27    ferro_property!(
28        /// Defines the `TintColor` property.
29        pub fn tint_color_property() -> StyledProperty<Color> {
30            FerroProperty::register::<ExperimentalAcrylicMaterial, _>("TintColor", Color::default())
31        }
32    );
33
34    ferro_property!(
35        /// Defines the `BackgroundSource` property.
36        pub fn background_source_property() -> StyledProperty<AcrylicBackgroundSource> {
37            FerroProperty::register::<ExperimentalAcrylicMaterial, _>("BackgroundSource", AcrylicBackgroundSource::None)
38        }
39    );
40
41    ferro_property!(
42        /// Defines the `TintOpacity` property.
43        pub fn tint_opacity_property() -> StyledProperty<f64> {
44            FerroProperty::register::<ExperimentalAcrylicMaterial, _>("TintOpacity", 0.8)
45        }
46    );
47
48    ferro_property!(
49        /// Defines the `MaterialOpacity` property.
50        pub fn material_opacity_property() -> StyledProperty<f64> {
51            FerroProperty::register::<ExperimentalAcrylicMaterial, _>("MaterialOpacity", 0.5)
52        }
53    );
54
55    ferro_property!(
56        /// Defines the `PlatformTransparencyCompensationLevel` property.
57        pub fn platform_transparency_compensation_level_property() -> StyledProperty<f64> {
58            FerroProperty::register::<ExperimentalAcrylicMaterial, _>("PlatformTransparencyCompensationLevel", 0.0)
59        }
60    );
61
62    ferro_property!(
63        /// Defines the `FallbackColor` property.
64        pub fn fallback_color_property() -> StyledProperty<Color> {
65            FerroProperty::register::<ExperimentalAcrylicMaterial, _>("FallbackColor", Color::default())
66        }
67    );
68} }
69
70impl ExperimentalAcrylicMaterial {
71    fn static_constructor() {
72        Self::affects_render::<ExperimentalAcrylicMaterial>(&[
73            Self::tint_color_property().as_property(),
74            Self::background_source_property().as_property(),
75            Self::tint_opacity_property().as_property(),
76            Self::material_opacity_property().as_property(),
77            Self::platform_transparency_compensation_level_property().as_property(),
78        ]);
79
80        for property in [
81            Self::tint_color_property().as_property(),
82            Self::tint_opacity_property().as_property(),
83            Self::material_opacity_property().as_property(),
84            Self::platform_transparency_compensation_level_property().as_property(),
85        ] {
86            property.changed().add_class_handler::<ExperimentalAcrylicMaterial>(|b, _| {
87                b.effective_tint_color.set(Self::get_effective_tint_color(b.tint_color(), b.tint_opacity()));
88                b.effective_luminosity_color.set(b.get_effective_luminosity_color());
89            });
90        }
91    }
92
93    /// Creates the class data.
94    pub fn construct() -> Self {
95        Self {
96            base: FerroObject::construct(),
97            effective_tint_color: Cell::new(Color::default()),
98            effective_luminosity_color: Cell::new(Color::default()),
99            invalidated: HandlerList::new(),
100        }
101    }
102
103    pub fn new() -> Ref<Self> {
104        instantiate(Self::construct())
105    }
106
107    /// Raised when the material changes.
108    pub fn invalidated(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
109        let token = self.invalidated.add(Rc::new(handler));
110        let weak = self.to_ref().downgrade();
111        Disposable::create(move || {
112            if let Some(this) = weak.upgrade() {
113                this.invalidated.remove(token);
114            }
115        })
116    }
117
118    /// The background source mode.
119    pub fn background_source(&self) -> AcrylicBackgroundSource {
120        self.get_value(Self::background_source_property())
121    }
122
123    pub fn set_background_source(&self, value: AcrylicBackgroundSource) {
124        self.set_value(Self::background_source_property(), value)
125    }
126
127    /// The tint color of the material.
128    pub fn tint_color(&self) -> Color {
129        self.get_value(Self::tint_color_property())
130    }
131
132    pub fn set_tint_color(&self, value: Color) {
133        self.set_value(Self::tint_color_property(), value)
134    }
135
136    /// The tint opacity of the material.
137    pub fn tint_opacity(&self) -> f64 {
138        self.get_value(Self::tint_opacity_property())
139    }
140
141    pub fn set_tint_opacity(&self, value: f64) {
142        self.set_value(Self::tint_opacity_property(), value)
143    }
144
145    /// The fallback color if acrylic is unavailable.
146    pub fn fallback_color(&self) -> Color {
147        self.get_value(Self::fallback_color_property())
148    }
149
150    pub fn set_fallback_color(&self, value: Color) {
151        self.set_value(Self::fallback_color_property(), value)
152    }
153
154    /// The opacity of the material layer.
155    pub fn material_opacity(&self) -> f64 {
156        self.get_value(Self::material_opacity_property())
157    }
158
159    pub fn set_material_opacity(&self, value: f64) {
160        self.set_value(Self::material_opacity_property(), value)
161    }
162
163    /// How much of the platform's own transparency is compensated for.
164    pub fn platform_transparency_compensation_level(&self) -> f64 {
165        self.get_value(Self::platform_transparency_compensation_level_property())
166    }
167
168    pub fn set_platform_transparency_compensation_level(&self, value: f64) {
169        self.set_value(Self::platform_transparency_compensation_level_property(), value)
170    }
171
172    /// The material color reported through [`IExperimentalAcrylicMaterial`].
173    pub(crate) fn effective_luminosity_color(&self) -> Color {
174        self.effective_luminosity_color.get()
175    }
176
177    /// The tint color reported through [`IExperimentalAcrylicMaterial`].
178    pub(crate) fn effective_tint_color(&self) -> Color {
179        self.effective_tint_color.get()
180    }
181
182    fn get_effective_tint_color(tint_color: Color, tint_opacity: f64) -> Color {
183        // Update the tint color's alpha with the combined opacity value.
184        let tint_opacity_modifier = Self::get_tint_opacity_modifier(tint_color);
185
186        Color::new(
187            (255.0 * ((255.0 / tint_color.a as f64) * tint_opacity) * tint_opacity_modifier) as u8,
188            tint_color.r,
189            tint_color.g,
190            tint_color.b,
191        )
192    }
193
194    fn get_tint_opacity_modifier(tint_color: Color) -> f64 {
195        // This method suppresses the maximum allowable tint opacity depending
196        // on the luminosity and saturation of a color by compressing the range
197        // of allowable values - for example, a user-defined value of 100% will
198        // be mapped to 45% for pure white (100% luminosity), 85% for pure
199        // black (0% luminosity), and 90% for pure gray (50% luminosity). The
200        // intensity of the effect increases linearly as luminosity deviates
201        // from 50%. After this effect is calculated, we cancel it out linearly
202        // as saturation increases from zero.
203
204        // Mid point of the HsvV range that these calculations are based on.
205        // This is here for easy tuning.
206        const MID_POINT: f64 = 0.5;
207
208        const WHITE_MAX_OPACITY: f64 = 0.2; // 100% luminosity
209        const MID_POINT_MAX_OPACITY: f64 = 0.45; // 50% luminosity
210        const BLACK_MAX_OPACITY: f64 = 0.45; // 0% luminosity
211
212        let hsv = tint_color.to_hsv();
213
214        let mut opacity_modifier = MID_POINT_MAX_OPACITY;
215
216        if hsv.v != MID_POINT {
217            // Determine maximum suppression amount
218            let mut lowest_max_opacity = MID_POINT_MAX_OPACITY;
219            let mut max_deviation = MID_POINT;
220
221            if hsv.v > MID_POINT {
222                lowest_max_opacity = WHITE_MAX_OPACITY; // At white (100% hsvV)
223                max_deviation = 1.0 - max_deviation;
224            } else if hsv.v < MID_POINT {
225                lowest_max_opacity = BLACK_MAX_OPACITY; // At black (0% hsvV)
226            }
227
228            let mut max_opacity_suppression = MID_POINT_MAX_OPACITY - lowest_max_opacity;
229
230            // Determine normalized deviation from the midpoint
231            let deviation = (hsv.v - MID_POINT).abs();
232            let normalized_deviation = deviation / max_deviation;
233
234            // If we have saturation, reduce opacity suppression to allow that
235            // color to come through more
236            if hsv.s > 0.0 {
237                // Dampen opacity suppression based on how much saturation
238                // there is
239                max_opacity_suppression *= f64::max(1.0 - (hsv.s * 2.0), 0.0);
240            }
241
242            let opacity_suppression = max_opacity_suppression * normalized_deviation;
243
244            opacity_modifier = MID_POINT_MAX_OPACITY - opacity_suppression;
245        }
246
247        opacity_modifier
248    }
249
250    fn get_effective_luminosity_color(&self) -> Color {
251        let luminosity_opacity = Some(self.material_opacity());
252
253        self.get_luminosity_color(luminosity_opacity)
254    }
255
256    fn trim(value: f64) -> u8 {
257        let value = f64::min((value * 256.0).floor(), 255.0);
258
259        if value < 0.0 {
260            return 0;
261        } else if value > 255.0 {
262            return 255;
263        }
264
265        value as u8
266    }
267
268    fn rgb_max(color: Color) -> f32 {
269        if color.r > color.g {
270            if color.r > color.b {
271                color.r as f32
272            } else {
273                color.b as f32
274            }
275        } else if color.g > color.b {
276            color.g as f32
277        } else {
278            color.b as f32
279        }
280    }
281
282    fn rgb_min(color: Color) -> f32 {
283        if color.r < color.g {
284            if color.r < color.b {
285                color.r as f32
286            } else {
287                color.b as f32
288            }
289        } else if color.g < color.b {
290            color.g as f32
291        } else {
292            color.b as f32
293        }
294    }
295
296    // The tint color used here should be the original, unmodified color
297    // created using user values for TintColor + TintOpacity.
298    fn get_luminosity_color(&self, luminosity_opacity: Option<f64>) -> Color {
299        let tint_color = self.tint_color();
300
301        // Calculate the HSL lightness value of the color.
302        let max = Self::rgb_max(tint_color) / 255.0f32;
303        let min = Self::rgb_min(tint_color) / 255.0f32;
304
305        let mut lightness = ((max + min) as f64) / 2.0;
306
307        lightness = 1.0 - ((1.0 - lightness) * luminosity_opacity.unwrap_or(1.0));
308
309        lightness = 0.13 + (lightness * 0.74);
310
311        let luminosity_color = Color::new(255, Self::trim(lightness), Self::trim(lightness), Self::trim(lightness));
312
313        let compensation_level = self.platform_transparency_compensation_level();
314        let compensation_multiplier = 1.0 - compensation_level;
315        Color::new(
316            (255.0
317                * f64::max(
318                    f64::min(compensation_level + (luminosity_opacity.unwrap_or(1.0) * compensation_multiplier), 1.0),
319                    0.0,
320                )) as u8,
321            luminosity_color.r,
322            luminosity_color.g,
323            luminosity_color.b,
324        )
325    }
326
327    /// Marks a property on a class deriving from this one as affecting the
328    /// material's visual appearance: a change raises
329    /// [`invalidated`](Self::invalidated).
330    pub fn affects_render<T: ObjectType + Upcast<ExperimentalAcrylicMaterial>>(properties: &[&'static FerroProperty]) {
331        for property in properties {
332            property.changed().subscribe(|e| {
333                if let Some(sender) = e.sender().downcast_ref::<T>() {
334                    let material: &ExperimentalAcrylicMaterial = sender.upcast();
335                    material.raise_invalidated();
336                }
337            });
338        }
339    }
340
341    /// Raises the invalidated notification.
342    pub fn raise_invalidated(&self) {
343        if self.invalidated.is_empty() {
344            return;
345        }
346        for (_, handler) in self.invalidated.snapshot().iter() {
347            handler();
348        }
349    }
350
351    /// Creates an immutable clone of the material.
352    pub fn to_immutable(&self) -> Rc<dyn IExperimentalAcrylicMaterial> {
353        Rc::new(ImmutableExperimentalAcrylicMaterial::new(&RefAdapter(self.to_ref())))
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    // Not from upstream: there are no upstream unit tests for this class.
360    use super::*;
361    use crate::media::{Colors, MaterialExtensions};
362
363    #[test]
364    fn changing_properties_raises_invalidated_and_updates_effective_colors() {
365        let target = ExperimentalAcrylicMaterial::new();
366        let count = Rc::new(Cell::new(0));
367        let c = count.clone();
368        target.invalidated(move || c.set(c.get() + 1));
369
370        target.set_tint_color(Colors::WHITE);
371        assert_eq!(1, count.get());
372        target.set_tint_opacity(1.0);
373        target.set_material_opacity(0.65);
374        target.set_background_source(AcrylicBackgroundSource::Digger);
375        assert_eq!(4, count.get());
376        // The fallback color does not affect rendering.
377        target.set_fallback_color(Colors::RED);
378        assert_eq!(4, count.get());
379
380        let material: Rc<dyn IExperimentalAcrylicMaterial> = target.clone().into();
381        // White: the tint opacity is compressed to 20%.
382        assert_eq!(Color::new(51, 255, 255, 255), material.tint_color());
383        // Lightness 1 -> 0.13 + 0.74 = 0.87 -> floor(0.87 * 256) = 222;
384        // alpha = 255 * 0.65 = 165.
385        assert_eq!(Color::new(165, 222, 222, 222), material.material_color());
386        assert_eq!(1.0, material.tint_opacity());
387        assert_eq!(Colors::RED, material.fallback_color());
388        assert_eq!(AcrylicBackgroundSource::Digger, material.background_source());
389
390        let immutable = MaterialExtensions::to_immutable(&material);
391        assert!(immutable.as_mutable_material().is_none());
392        assert_eq!(Color::new(51, 255, 255, 255), immutable.tint_color());
393        let again = MaterialExtensions::to_immutable(&immutable);
394        assert!(*again == *immutable);
395
396        let a = immutable.as_any().downcast_ref::<ImmutableExperimentalAcrylicMaterial>().unwrap();
397        target.set_platform_transparency_compensation_level(1.0);
398        let b = target.to_immutable();
399        let b = b.as_any().downcast_ref::<ImmutableExperimentalAcrylicMaterial>().unwrap();
400        assert_eq!(255, b.material_color().a);
401        assert!(a != b);
402        assert!(a == &a.clone());
403    }
404}