1use crate::media::{
7 BrushImpl, Color, MaterialPolicy, XamlCompositionBrushBase, XamlCompositionBrushBaseImpl,
8};
9use crate::numerics::Vector2;
10use crate::reactive::IDisposable;
11use crate::rendering::composition::effects::{
12 BlendEffect, BlendEffectMode, BorderEffect, CanvasComposite, CanvasEdgeBehavior, ColorSourceEffect,
13 CompositeEffect, CompositionEffectFactory, CompositionEffectSourceParameter, CrossFadeEffect, EffectBorderMode,
14 GaussianBlurEffect, GraphicsEffect, GraphicsEffectSource, OpacityEffect,
15};
16use crate::rendering::composition::{
17 CompositionBatchTypes, CompositionBrush, CompositionScopedBatch, Compositor, ICompositionObjectAnimations,
18};
19use crate::{
20 ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
21 FerroPropertyChangedEventArgs, Ref, StyledProperty, StyledPropertyOptions,
22};
23use std::cell::RefCell;
24use std::rc::{Rc, Weak};
25use std::time::Duration;
26
27pub const ACRYLIC_BLUR_AMOUNT: f32 = 30.0;
29pub const ACRYLIC_NOISE_OPACITY: f32 = 0.02;
31pub const ACRYLIC_FALLBACK_CROSS_FADE: Duration = Duration::from_millis(167);
34
35const TINT_COLOR_COLOR: &str = "TintColor.Color";
36const LUMINOSITY_COLOR_COLOR: &str = "LuminosityColor.Color";
37const FALLBACK_COLOR_COLOR: &str = "FallbackColor.Color";
38const OPAQUE_FALLBACK_COLOR_COLOR: &str = "OpaqueFallbackColor.Color";
39const FADE_IN_OUT_WEIGHT: &str = "FadeInOut.Weight";
40
41#[derive(Clone, Copy, Debug, PartialEq)]
45struct Hsv {
46 h: f64,
47 s: f64,
48 v: f64,
49}
50
51fn rgb_to_hsv(r: f64, g: f64, b: f64) -> Hsv {
52 let max = r.max(g).max(b);
53 let min = r.min(g).min(b);
54 let delta = max - min;
55 let h = if delta <= 0.0 {
56 0.0
57 } else if max == r {
58 60.0 * (((g - b) / delta).rem_euclid(6.0))
59 } else if max == g {
60 60.0 * ((b - r) / delta + 2.0)
61 } else {
62 60.0 * ((r - g) / delta + 4.0)
63 };
64 let s = if max <= 0.0 { 0.0 } else { delta / max };
65 Hsv { h, s, v: max }
66}
67
68fn hsv_to_rgb(hsv: Hsv) -> (f64, f64, f64) {
69 let c = hsv.v * hsv.s;
70 let h = hsv.h.rem_euclid(360.0) / 60.0;
71 let x = c * (1.0 - (h.rem_euclid(2.0) - 1.0).abs());
72 let (r, g, b) = match h as u32 {
73 0 => (c, x, 0.0),
74 1 => (x, c, 0.0),
75 2 => (0.0, c, x),
76 3 => (0.0, x, c),
77 4 => (x, 0.0, c),
78 _ => (c, 0.0, x),
79 };
80 let m = hsv.v - c;
81 (r + m, g + m, b + m)
82}
83
84fn channels(color: Color) -> (f64, f64, f64) {
85 (color.r as f64 / 255.0, color.g as f64 / 255.0, color.b as f64 / 255.0)
86}
87
88fn channel(value: f64) -> u8 {
89 (value * 255.0).round().clamp(0.0, 255.0) as u8
90}
91
92pub fn tint_opacity_modifier(tint_color: Color) -> f64 {
97 const MID_POINT: f64 = 0.50;
98 const WHITE_MAX_OPACITY: f64 = 0.45;
99 const MID_POINT_MAX_OPACITY: f64 = 0.90;
100 const BLACK_MAX_OPACITY: f64 = 0.85;
101
102 let (r, g, b) = channels(tint_color);
103 let hsv = rgb_to_hsv(r, g, b);
104 let mut opacity_modifier = MID_POINT_MAX_OPACITY;
105 if hsv.v != MID_POINT {
106 let mut lowest_max_opacity = MID_POINT_MAX_OPACITY;
107 let mut max_deviation = MID_POINT;
108 if hsv.v > MID_POINT {
109 lowest_max_opacity = WHITE_MAX_OPACITY;
110 max_deviation = 1.0 - max_deviation;
111 } else if hsv.v < MID_POINT {
112 lowest_max_opacity = BLACK_MAX_OPACITY;
113 }
114 let mut max_opacity_suppression = MID_POINT_MAX_OPACITY - lowest_max_opacity;
115 let deviation = (hsv.v - MID_POINT).abs();
116 let normalized_deviation = deviation / max_deviation;
117 if hsv.s > 0.0 {
118 max_opacity_suppression *= (1.0 - hsv.s * 2.0).max(0.0);
119 }
120 let opacity_suppression = max_opacity_suppression * normalized_deviation;
121 opacity_modifier = MID_POINT_MAX_OPACITY - opacity_suppression;
122 }
123 opacity_modifier
124}
125
126pub fn luminosity_color(tint_color: Color, luminosity_opacity: Option<f64>) -> Color {
132 let (r, g, b) = channels(tint_color);
133 if let Some(opacity) = luminosity_opacity {
134 return Color::from_argb(channel(opacity.clamp(0.0, 1.0)), tint_color.r, tint_color.g, tint_color.b);
135 }
136 const MIN_HSV_V: f64 = 0.125;
137 const MAX_HSV_V: f64 = 0.965;
138 let hsv = rgb_to_hsv(r, g, b);
139 let (lr, lg, lb) = hsv_to_rgb(Hsv { v: hsv.v.clamp(MIN_HSV_V, MAX_HSV_V), ..hsv });
140 const MIN_LUMINOSITY_OPACITY: f64 = 0.15;
141 const MAX_LUMINOSITY_OPACITY: f64 = 1.03;
142 let range = MAX_LUMINOSITY_OPACITY - MIN_LUMINOSITY_OPACITY;
143 let mapped = (tint_color.a as f64 / 255.0) * range + MIN_LUMINOSITY_OPACITY;
144 Color::from_argb(channel(mapped.min(1.0)), channel(lr), channel(lg), channel(lb))
145}
146
147fn parameter(name: &str) -> Option<GraphicsEffectSource> {
150 Some(CompositionEffectSourceParameter::new(name).into())
151}
152
153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
156#[repr(i32)]
157pub enum AcrylicBrushBackgroundSource {
158 HostBackdrop = 0,
160 #[default]
162 Backdrop = 1,
163}
164
165crate::ferro_markup_enum!(AcrylicBrushBackgroundSource { HostBackdrop, Backdrop }, { namespace: "FerroUI.Media" });
166
167pub fn acrylic_effect(
174 should_brush_be_opaque: bool,
175 use_cross_fade: bool,
176 tint_color: Color,
177 luminosity_color: Color,
178 fallback_color: Color,
179) -> (GraphicsEffect, Vec<&'static str>) {
180 acrylic_effect_with_source(
181 should_brush_be_opaque,
182 use_cross_fade,
183 AcrylicBrushBackgroundSource::Backdrop,
184 tint_color,
185 luminosity_color,
186 fallback_color,
187 )
188}
189
190pub fn acrylic_effect_with_source(
197 should_brush_be_opaque: bool,
198 use_cross_fade: bool,
199 source: AcrylicBrushBackgroundSource,
200 tint_color: Color,
201 luminosity_color: Color,
202 fallback_color: Color,
203) -> (GraphicsEffect, Vec<&'static str>) {
204 let tint: GraphicsEffect = ColorSourceEffect { name: "TintColor".into(), color: tint_color }.into();
205 let mut animatable = vec![TINT_COLOR_COLOR];
206
207 let tint_output: GraphicsEffect = if should_brush_be_opaque {
208 tint
209 } else if source == AcrylicBrushBackgroundSource::HostBackdrop {
210 animatable.push(LUMINOSITY_COLOR_COLOR);
211 CompositeEffect {
212 name: String::new(),
213 mode: CanvasComposite::SourceOver,
214 sources: vec![
215 CompositionEffectSourceParameter::new("Backdrop").into(),
216 ColorSourceEffect { name: "LuminosityColor".into(), color: luminosity_color }.into(),
217 tint.into(),
218 ],
219 }
220 .into()
221 } else {
222 let opaque_fallback = Color { a: 255, ..fallback_color };
225 animatable.push(OPAQUE_FALLBACK_COLOR_COLOR);
226 let blended_backdrop = CompositeEffect {
227 name: String::new(),
228 mode: CanvasComposite::SourceOver,
229 sources: vec![
230 ColorSourceEffect { name: "OpaqueFallbackColor".into(), color: opaque_fallback }.into(),
231 CompositionEffectSourceParameter::new("Backdrop").into(),
232 ],
233 };
234 let blur = GaussianBlurEffect {
235 name: "Blur".into(),
236 blur_amount: ACRYLIC_BLUR_AMOUNT,
237 border_mode: EffectBorderMode::Hard,
238 source: Some(blended_backdrop.into()),
239 ..Default::default()
240 };
241 animatable.push(LUMINOSITY_COLOR_COLOR);
242 let luminosity_blend = BlendEffect {
250 name: String::new(),
251 mode: BlendEffectMode::Luminosity,
252 background: Some(blur.into()),
253 foreground: Some(ColorSourceEffect { name: "LuminosityColor".into(), color: luminosity_color }.into()),
254 };
255 BlendEffect {
258 name: String::new(),
259 mode: BlendEffectMode::Color,
260 background: Some(luminosity_blend.into()),
261 foreground: Some(tint.into()),
262 }
263 .into()
264 };
265
266 let noise = OpacityEffect {
268 name: "NoiseOpacity".into(),
269 opacity: ACRYLIC_NOISE_OPACITY,
270 source: Some(
271 BorderEffect {
272 name: String::new(),
273 extend_x: CanvasEdgeBehavior::Wrap,
274 extend_y: CanvasEdgeBehavior::Wrap,
275 source: parameter("Noise"),
276 }
277 .into(),
278 ),
279 };
280 let outer: GraphicsEffect = CompositeEffect {
281 name: String::new(),
282 mode: CanvasComposite::SourceOver,
283 sources: vec![tint_output.into(), noise.into()],
284 }
285 .into();
286
287 if !use_cross_fade {
288 return (outer, animatable);
289 }
290 animatable.push(FALLBACK_COLOR_COLOR);
291 animatable.push(FADE_IN_OUT_WEIGHT);
292 let fade = CrossFadeEffect {
293 name: "FadeInOut".into(),
294 weight: 1.0,
295 source1: Some(ColorSourceEffect { name: "FallbackColor".into(), color: fallback_color }.into()),
296 source2: Some(outer.into()),
297 };
298 (fade.into(), animatable)
299}
300
301crate::ferro_thread_local! {
302 static FACTORIES: RefCell<Vec<(Weak<Compositor>, bool, bool, AcrylicBrushBackgroundSource, Rc<CompositionEffectFactory>)>> = const { RefCell::new(Vec::new()) };
305}
306
307fn factory(
308 compositor: &Rc<Compositor>,
309 opaque: bool,
310 cross_fade: bool,
311 source: AcrylicBrushBackgroundSource,
312 tint: Color,
313 luminosity: Color,
314 fallback: Color,
315) -> Rc<CompositionEffectFactory> {
316 let cached = FACTORIES.with(|factories| {
317 let mut factories = factories.borrow_mut();
318 factories.retain(|(owner, ..)| owner.strong_count() > 0);
319 factories
320 .iter()
321 .find(|(owner, o, c, s, _)| {
322 *o == opaque && *c == cross_fade && *s == source && owner.upgrade().is_some_and(|owner| Rc::ptr_eq(&owner, compositor))
323 })
324 .map(|(.., factory)| factory.clone())
325 });
326 if let Some(cached) = cached {
327 return cached;
328 }
329 let (effect, animatable) = acrylic_effect_with_source(opaque, cross_fade, source, tint, luminosity, fallback);
330 let factory = compositor.create_effect_factory_with_properties(effect, &animatable);
331 FACTORIES.with(|factories| factories.borrow_mut().push((Rc::downgrade(compositor), opaque, cross_fade, source, factory.clone())));
332 factory
333}
334
335#[derive(Default)]
338struct AcrylicState {
339 brush: Option<Rc<CompositionBrush>>,
342 is_using_acrylic: bool,
343 is_using_opaque: bool,
344 is_using_host_backdrop: bool,
346 is_disabled_by_policy: bool,
347 waiting_for_fallback: Option<(Rc<CompositionScopedBatch>, Rc<dyn IDisposable>)>,
350 policy_subscription: Option<Rc<dyn IDisposable>>,
351 activation_subscription: Option<Rc<dyn IDisposable>>,
352}
353
354#[repr(C)]
370pub struct AcrylicBrush {
371 base: XamlCompositionBrushBase,
372 state: RefCell<AcrylicState>,
373}
374
375ferro_class!(AcrylicBrush: XamlCompositionBrushBase);
376crate::ferro_class_info!(AcrylicBrush {
377 new: AcrylicBrush::new,
378 markup: {
379 namespace: "FerroUI.Media",
380 },
381});
382ferro_impl_classes!(AcrylicBrush: BrushImpl);
383
384impl FerroObjectImpl for AcrylicBrush {
385 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
386 Self::parent_on_property_changed(this, change);
387 let property = change.property();
388 if property == Self::tint_color_property().as_property()
389 || property == Self::tint_opacity_property().as_property()
390 || property == Self::tint_luminosity_opacity_property().as_property()
391 {
392 this.on_tint_changed(property == Self::tint_luminosity_opacity_property().as_property());
393 } else if property == Self::always_use_fallback_property().as_property()
394 || property == Self::background_source_property().as_property()
395 {
396 this.update_acrylic_brush();
397 } else if property == XamlCompositionBrushBase::fallback_color_property().as_property() {
398 this.on_fallback_color_changed();
399 }
400 }
401}
402
403impl XamlCompositionBrushBaseImpl for AcrylicBrush {
404 fn on_connected(this: &Self) {
405 let weak = this.to_ref().downgrade();
406 let subscription = MaterialPolicy::policy_changed(move |disabled| {
409 if let Some(this) = weak.upgrade() {
410 this.state.borrow_mut().is_disabled_by_policy = disabled;
411 if this.state.borrow().policy_subscription.is_some() {
412 this.update_acrylic_brush();
413 }
414 }
415 });
416 this.state.borrow_mut().policy_subscription = Some(subscription);
417 let weak = this.to_ref().downgrade();
418 let activation = MaterialPolicy::window_activation_changed(move |_| {
419 if let Some(this) = weak.upgrade() {
420 if this.background_source() == AcrylicBrushBackgroundSource::HostBackdrop {
421 this.update_acrylic_brush();
422 }
423 }
424 });
425 this.state.borrow_mut().activation_subscription = Some(activation);
426 this.update_acrylic_brush();
427 }
428
429 fn on_disconnected(this: &Self) {
430 this.cancel_fallback_animation_complete_wait();
431 let (subscriptions, brush) = {
432 let mut state = this.state.borrow_mut();
433 state.is_using_acrylic = false;
434 ([state.policy_subscription.take(), state.activation_subscription.take()], state.brush.take())
435 };
436 for subscription in subscriptions.into_iter().flatten() {
437 subscription.dispose();
438 }
439 if let Some(brush) = brush {
440 brush.object().dispose();
441 }
442 this.set_composition_brush(None);
443 }
444}
445
446crate::ferro_properties! { impl AcrylicBrush {
447 ferro_property!(
448 pub fn tint_color_property() -> StyledProperty<Color> {
450 FerroProperty::register::<AcrylicBrush, _>("TintColor", Color::from_argb(204, 255, 255, 255))
451 }
452 );
453
454 ferro_property!(
455 pub fn tint_opacity_property() -> StyledProperty<f64> {
457 FerroProperty::register_with::<AcrylicBrush, _>(
458 "TintOpacity",
459 StyledPropertyOptions::new(1.0).coerce(|_, value: f64| if value.is_nan() { value } else { value.clamp(0.0, 1.0) }),
460 )
461 }
462 );
463
464 ferro_property!(
465 pub fn tint_luminosity_opacity_property() -> StyledProperty<Option<f64>> {
467 FerroProperty::register_with::<AcrylicBrush, _>(
468 "TintLuminosityOpacity",
469 StyledPropertyOptions::new(None).coerce(|_, value: Option<f64>| {
470 value.map(|value| if value.is_nan() { value } else { value.clamp(0.0, 1.0) })
471 }),
472 )
473 }
474 );
475
476 ferro_property!(
477 pub fn tint_transition_duration_property() -> StyledProperty<Duration> {
480 FerroProperty::register::<AcrylicBrush, _>("TintTransitionDuration", Duration::from_millis(500))
481 }
482 );
483
484 ferro_property!(
485 pub fn background_source_property() -> StyledProperty<AcrylicBrushBackgroundSource> {
488 FerroProperty::register::<AcrylicBrush, _>("BackgroundSource", AcrylicBrushBackgroundSource::Backdrop)
489 }
490 );
491
492 ferro_property!(
493 pub fn always_use_fallback_property() -> StyledProperty<bool> {
495 FerroProperty::register::<AcrylicBrush, _>("AlwaysUseFallback", false)
496 }
497 );
498} }
499
500impl AcrylicBrush {
501 pub fn construct() -> Self {
503 Self { base: XamlCompositionBrushBase::construct(), state: RefCell::new(AcrylicState::default()) }
504 }
505
506 pub fn new() -> Ref<Self> {
507 instantiate(Self::construct())
508 }
509
510 pub fn tint_color(&self) -> Color {
511 self.get_value(Self::tint_color_property())
512 }
513
514 pub fn set_tint_color(&self, value: Color) {
515 self.set_value(Self::tint_color_property(), value)
516 }
517
518 pub fn tint_opacity(&self) -> f64 {
519 self.get_value(Self::tint_opacity_property())
520 }
521
522 pub fn set_tint_opacity(&self, value: f64) {
523 self.set_value(Self::tint_opacity_property(), value)
524 }
525
526 pub fn tint_luminosity_opacity(&self) -> Option<f64> {
527 self.get_value(Self::tint_luminosity_opacity_property())
528 }
529
530 pub fn set_tint_luminosity_opacity(&self, value: Option<f64>) {
531 self.set_value(Self::tint_luminosity_opacity_property(), value)
532 }
533
534 pub fn tint_transition_duration(&self) -> Duration {
535 self.get_value(Self::tint_transition_duration_property())
536 }
537
538 pub fn set_tint_transition_duration(&self, value: Duration) {
539 self.set_value(Self::tint_transition_duration_property(), value)
540 }
541
542 pub fn background_source(&self) -> AcrylicBrushBackgroundSource {
543 self.get_value(Self::background_source_property())
544 }
545
546 pub fn set_background_source(&self, value: AcrylicBrushBackgroundSource) {
547 self.set_value(Self::background_source_property(), value)
548 }
549
550 pub fn always_use_fallback(&self) -> bool {
551 self.get_value(Self::always_use_fallback_property())
552 }
553
554 pub fn set_always_use_fallback(&self, value: bool) {
555 self.set_value(Self::always_use_fallback_property(), value)
556 }
557
558 pub fn effective_tint_color(&self) -> Color {
562 let mut tint = self.tint_color();
563 let opacity = self.tint_opacity();
564 let modifier = if self.tint_luminosity_opacity().is_some() { 1.0 } else { tint_opacity_modifier(tint) };
565 tint.a = (tint.a as f64 * opacity * modifier).round().clamp(0.0, 255.0) as u8;
566 tint
567 }
568
569 pub fn effective_luminosity_color(&self) -> Color {
572 let mut tint = self.tint_color();
573 tint.a = (tint.a as f64 * self.tint_opacity()).round().clamp(0.0, 255.0) as u8;
574 luminosity_color(tint, self.tint_luminosity_opacity())
575 }
576
577 pub fn is_using_acrylic_brush(&self) -> bool {
580 self.state.borrow().is_using_acrylic
581 }
582
583 pub fn is_waiting_for_fallback_animation(&self) -> bool {
586 self.state.borrow().waiting_for_fallback.is_some()
587 }
588
589 fn on_tint_changed(&self, luminosity_only: bool) {
590 let should_use_opaque = self.effective_tint_color().a == 255;
591 if self.state.borrow().is_using_opaque != should_use_opaque {
592 self.update_acrylic_brush();
593 }
594 let (brush, animate) = {
595 let state = self.state.borrow();
596 (state.brush.clone(), state.is_using_acrylic || state.waiting_for_fallback.is_some())
597 };
598 let Some(brush) = brush.filter(|_| animate) else { return };
599 let duration = self.tint_transition_duration();
600 if !luminosity_only {
601 animate_color(&brush, TINT_COLOR_COLOR, self.effective_tint_color(), duration);
602 }
603 if !should_use_opaque {
604 animate_color(&brush, LUMINOSITY_COLOR_COLOR, self.effective_luminosity_color(), duration);
605 }
606 }
607
608 fn on_fallback_color_changed(&self) {
609 let (brush, using_opaque, using_acrylic, waiting) = {
610 let state = self.state.borrow();
611 (state.brush.clone(), state.is_using_opaque, state.is_using_acrylic, state.waiting_for_fallback.is_some())
612 };
613 let Some(brush) = brush else { return };
614 let fallback = self.fallback_color();
615 if !using_opaque && (using_acrylic || waiting) {
616 brush.object().properties().insert_color(OPAQUE_FALLBACK_COLOR_COLOR, Color { a: 255, ..fallback });
617 }
618 if using_acrylic && !waiting {
619 return;
621 }
622 let name = if using_acrylic || waiting { FALLBACK_COLOR_COLOR } else { "Color" };
623 animate_color(&brush, name, fallback, self.tint_transition_duration());
624 }
625
626 fn update_acrylic_brush(&self) {
631 if !self.is_connected() {
632 return;
633 }
634 let use_host_backdrop = self.background_source() == AcrylicBrushBackgroundSource::HostBackdrop;
635 let is_using_acrylic = !(self.state.borrow().is_disabled_by_policy
636 || self.always_use_fallback()
637 || (use_host_backdrop && !MaterialPolicy::is_window_active()));
638 let should_use_opaque = self.effective_tint_color().a == 255;
639 let (has_brush, using_opaque, currently_acrylic, using_host_backdrop) = {
640 let state = self.state.borrow();
641 (state.brush.is_some(), state.is_using_opaque, state.is_using_acrylic, state.is_using_host_backdrop)
642 };
643 self.set_needs_transparent_window(use_host_backdrop);
644 if !has_brush || using_opaque != should_use_opaque || using_host_backdrop != use_host_backdrop {
645 self.state.borrow_mut().is_using_host_backdrop = use_host_backdrop;
646 self.state.borrow_mut().is_using_acrylic = is_using_acrylic;
647 self.create_acrylic_brush(false, false);
648 return;
649 }
650 if currently_acrylic == is_using_acrylic {
651 return;
652 }
653 self.cancel_fallback_animation_complete_wait();
654 self.create_acrylic_brush(true, true);
657 self.state.borrow_mut().is_using_acrylic = is_using_acrylic;
658 let (start, end) = if is_using_acrylic { (0.0, 1.0) } else { (1.0, 0.0) };
659 let Some(brush) = self.state.borrow().brush.clone() else { return };
660 let compositor = brush.compositor().clone();
661 let batch = compositor.create_scoped_batch(CompositionBatchTypes::ANIMATION);
662 let animation = compositor.create_scalar_key_frame_animation();
663 let easing = compositor.create_cubic_bezier_easing_function(Vector2::new(0.5, 0.0), Vector2::new(0.0, 0.9));
664 animation.insert_key_frame(0.0, start);
665 animation.insert_key_frame_with_easing(1.0, end, easing);
666 animation.set_duration(ACRYLIC_FALLBACK_CROSS_FADE);
667 brush.start_animation(FADE_IN_OUT_WEIGHT, &*animation);
668 batch.end();
669 let weak = self.to_ref().downgrade();
670 let subscription = batch.completed(move |_| {
671 if let Some(this) = weak.upgrade() {
672 this.cancel_fallback_animation_complete_wait();
673 this.create_acrylic_brush(false, false);
676 }
677 });
678 self.state.borrow_mut().waiting_for_fallback = Some((batch, subscription));
679 }
680
681 fn cancel_fallback_animation_complete_wait(&self) {
682 let waiting = self.state.borrow_mut().waiting_for_fallback.take();
683 if let Some((_, subscription)) = waiting {
684 subscription.dispose();
685 }
686 }
687
688 fn create_acrylic_brush(&self, use_cross_fade: bool, force: bool) {
692 self.cancel_fallback_animation_complete_wait();
693 let Some(compositor) = self.compositor() else { return };
694 let fallback = self.fallback_color();
695 let using_acrylic = self.state.borrow().is_using_acrylic;
696 let brush: Rc<CompositionBrush> = if force || using_acrylic {
697 let tint = self.effective_tint_color();
698 let luminosity = self.effective_luminosity_color();
699 let opaque = tint.a == 255;
700 self.state.borrow_mut().is_using_opaque = opaque;
701 let host = self.background_source() == AcrylicBrushBackgroundSource::HostBackdrop;
702 let source = if host { AcrylicBrushBackgroundSource::HostBackdrop } else { AcrylicBrushBackgroundSource::Backdrop };
703 let effect_brush = factory(&compositor, opaque, use_cross_fade, source, tint, luminosity, fallback).create_brush();
704 effect_brush.set_source_parameter("Noise", Some((*MaterialPolicy::noise_brush(&compositor)).clone()));
705 if !opaque {
706 let backdrop: Rc<CompositionBrush> = if host {
707 (*compositor.create_host_backdrop_brush()).clone()
708 } else {
709 (*compositor.create_backdrop_brush()).clone()
710 };
711 effect_brush.set_source_parameter("Backdrop", Some(backdrop));
712 }
713 let properties = effect_brush.properties();
714 properties.insert_color(TINT_COLOR_COLOR, tint);
715 if !opaque {
716 properties.insert_color(LUMINOSITY_COLOR_COLOR, luminosity);
717 if !host {
718 properties.insert_color(OPAQUE_FALLBACK_COLOR_COLOR, Color { a: 255, ..fallback });
719 }
720 }
721 if use_cross_fade {
722 properties.insert_color(FALLBACK_COLOR_COLOR, fallback);
723 }
724 effect_brush.brush().clone()
725 } else {
726 (*compositor.create_color_brush_with_color(fallback)).clone()
727 };
728 let old = self.state.borrow_mut().brush.replace(brush.clone());
729 self.set_composition_brush(Some(brush));
730 if let Some(old) = old {
731 old.object().dispose();
732 }
733 }
734}
735
736fn animate_color(brush: &Rc<CompositionBrush>, name: &str, color: Color, duration: Duration) {
739 let compositor = brush.compositor().clone();
740 let animation = compositor.create_color_key_frame_animation();
741 animation.insert_key_frame(1.0, color);
742 animation.set_duration(if duration.is_zero() { Duration::from_millis(1) } else { duration });
743 brush.start_animation(name, &*animation);
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 #[test]
751 fn the_tint_opacity_modifier_follows_winui() {
752 assert!((tint_opacity_modifier(Color::from_rgb(255, 255, 255)) - 0.45).abs() < 1e-9);
754 assert!((tint_opacity_modifier(Color::from_rgb(0, 0, 0)) - 0.85).abs() < 1e-9);
755 assert!((tint_opacity_modifier(Color::from_rgb(255, 0, 0)) - 0.90).abs() < 1e-9);
757 }
758
759 #[test]
760 fn the_luminosity_colour_follows_winui() {
761 let color = luminosity_color(Color::from_argb(38, 44, 44, 44), Some(0.96));
763 assert_eq!((color.a, color.r, color.g, color.b), (245, 44, 44, 44));
764 let color = luminosity_color(Color::from_argb(0, 0, 0, 0), None);
767 assert_eq!(color.r, 32);
768 assert_eq!(color.a, (0.15f64 * 255.0).round() as u8);
769 let opaque = luminosity_color(Color::from_argb(255, 255, 255, 255), None);
770 assert_eq!(opaque.a, 255);
771 assert_eq!(opaque.r, (0.965f64 * 255.0).round() as u8);
772 }
773
774 #[test]
775 fn the_effective_colours_of_the_theme_brushes() {
776 let brush = AcrylicBrush::new();
778 brush.set_tint_color(Color::from_rgb(0x2c, 0x2c, 0x2c));
779 brush.set_tint_opacity(0.15);
780 brush.set_tint_luminosity_opacity(Some(0.96));
781 assert_eq!(brush.effective_tint_color().a, (255.0f64 * 0.15).round() as u8);
782 assert_eq!(brush.effective_luminosity_color().a, (0.96f64 * 255.0).round() as u8);
783 brush.set_tint_opacity(3.0);
785 assert_eq!(brush.tint_opacity(), 1.0);
786 brush.set_tint_luminosity_opacity(Some(-1.0));
787 assert_eq!(brush.tint_luminosity_opacity(), Some(0.0));
788 }
789
790 #[test]
791 fn the_effect_has_the_animatable_properties_of_winui() {
792 let (_, translucent) = acrylic_effect(false, false, Color::default(), Color::default(), Color::default());
793 assert_eq!(translucent, vec![TINT_COLOR_COLOR, OPAQUE_FALLBACK_COLOR_COLOR, LUMINOSITY_COLOR_COLOR]);
794 let (_, opaque_fading) = acrylic_effect(true, true, Color::default(), Color::default(), Color::default());
795 assert_eq!(opaque_fading, vec![TINT_COLOR_COLOR, FALLBACK_COLOR_COLOR, FADE_IN_OUT_WEIGHT]);
796 }
797}