@@ -11,21 +11,29 @@ namespace CommunityToolkit.WinUI.Helpers;
1111/// </summary> 
1212public  partial  class  ContrastHelper 
1313{ 
14+     // When the helper is updating the color, this flag is set to avoid feedback loops 
15+     // It has no threading issues since all updates are on the UI thread 
1416    private  static   bool  _selfUpdate  =  false ; 
1517
1618    private  static   void  OnOpponentChanged ( DependencyObject  d ,  DependencyPropertyChangedEventArgs  e ) 
1719    { 
18-         // Subscribe to brush updates  
19-         if  ( GetCallback ( d )  is  0 ) 
20+         // Subscribe to brush updates if not already  
21+         if  ( GetCallbackObject ( d )  is  null ) 
2022        { 
2123            SubscribeToUpdates ( d ) ; 
2224        } 
2325
26+         // Update the actual color to ensure contrast 
2427        ApplyContrastCheck ( d ) ; 
2528    } 
2629
2730    private  static   void  OnMinRatioChanged ( DependencyObject  d ,  DependencyPropertyChangedEventArgs  e ) 
2831    { 
32+         // No opponent has been set, nothing to do 
33+         if  ( GetCallback ( d )  is  0 ) 
34+             return ; 
35+         
36+         // Update the actual color to ensure contrast 
2937        ApplyContrastCheck ( d ) ; 
3038    } 
3139
@@ -57,49 +65,97 @@ private static void ApplyContrastCheck(DependencyObject d)
5765
5866    private  static   void  SubscribeToUpdates ( DependencyObject  d ) 
5967    { 
60-         var  brush  =  FindBrush ( d ,  out  var  dp ) ; 
61-         if  ( brush  is  null ) 
62-             return ; 
68+         // Get the original color from the brush and the property to monitor. 
69+         // Use Transparent as a sentinel value if the brush is not a SolidColorBrush 
70+         var  solidColorBrush  =  FindBrush ( d ,  out  var  dp ) ; 
71+         var  color  =  solidColorBrush ? . Color  ??  Colors . Transparent ; 
72+ 
73+         // Record the original color 
74+         SetOriginal ( d ,  color ) ; 
6375
64-         // Apply initial update 
65-         SetOriginal ( d ,  brush . Color ) ; 
76+         // Rhetortical Question: Why don't we return if the solidColorBrush is null? 
77+         // Just because the brush is not a SolidColorBrush doesn't mean we can't monitor the 
78+         // Foreground property. We just can't monitor the brush's Color property 
6679
80+         // If the original is not a SolidColorBrush, we need to monitor the Foreground property 
6781        if  ( d  is  not SolidColorBrush ) 
6882        { 
69-             // Subscribe to updates from the source Foreground and Brush  
70-             d . RegisterPropertyChangedCallback ( dp ,  ( sender ,  prop )  => 
83+             // Subscribe to updates from the source Foreground 
84+             _   =   d . RegisterPropertyChangedCallback ( dp ,  ( sender ,  prop )  => 
7185            { 
7286                OnOriginalChangedFromSource ( d ,  sender ,  prop ) ; 
7387            } ) ; 
7488        } 
7589
90+         // Subscribe to updates from the source SolidColorBrush 
91+         // If solidColorBrush is null, this is a no-op 
92+         SubscribeToBrushUpdates ( d ,  solidColorBrush ) ; 
93+     } 
94+ 
95+     private  static   void  SubscribeToBrushUpdates ( DependencyObject  d ,  SolidColorBrush ?  brush ) 
96+     { 
97+         // No brush, nothing to do 
98+         if  ( brush  is  null ) 
99+             return ; 
100+ 
101+         // Unsubscribe from previous brush if any 
102+         var  oldBrush  =  GetCallbackObject ( d ) ; 
103+         var  oldCallback  =  GetCallback ( d ) ; 
104+         oldBrush ? . UnregisterPropertyChangedCallback ( SolidColorBrush . ColorProperty ,  oldCallback ) ; 
105+ 
106+         // Subscribe to updates from the source SolidColorBrush 
76107        var  callback  =  brush . RegisterPropertyChangedCallback ( SolidColorBrush . ColorProperty ,  ( sender ,  prop )  => 
77108        { 
78109            OnOriginalChangedFromSource ( d ,  sender ,  prop ) ; 
79110        } ) ; 
80111
112+         // Track the callback so we don't double subscribe and can unsubscribe if needed 
113+         SetCallbackObject ( d ,  brush ) ; 
81114        SetCallback ( d ,  callback ) ; 
82115    } 
83116
84117    private  static   void  OnOriginalChangedFromSource ( DependencyObject  obj ,  DependencyObject  sender ,  DependencyProperty  prop ) 
85118    { 
86-         // The contrast helper is updating the color.  
119+         // The contrast helper is updating the color 
87120        // Ignore the assignment. 
88121        if  ( _selfUpdate ) 
89122            return ; 
90123
91-         // Get brush 
124+         // Get the original color from the brush. 
125+         // We use the sender, not the obj, because the sender is the object that changed. 
126+         // Use Transparent as a sentinel value if the brush is not a SolidColorBrush 
92127        var  brush  =  FindBrush ( sender ,  out  _ ) ; 
93-         if  ( brush  is  null ) 
94-             return ; 
128+         var  color  =  brush ? . Color  ??  Colors . Transparent ; 
95129
96130        // Update original color 
97-         SetOriginal ( obj ,  brush . Color ) ; 
131+         SetOriginal ( obj ,  color ) ; 
132+ 
133+         // The sender is the Foreground property, not the brush itself. 
134+         // This means the brush changed and our callback on the brush is dead. 
135+         // We need to subscribe to the new brush if it's a SolidColorBrush. 
136+         if  ( sender  is  not SolidColorBrush ) 
137+         { 
138+             // Subscribe to the new brush 
139+             // Notice we're finding the brush on the object, not the sender this time. 
140+             // We may not find a SolidColorBrush, and that's ok. 
141+             var  solidColorBrush  =  FindBrush ( obj ,  out  _ ) ; 
142+             SubscribeToBrushUpdates ( obj ,  solidColorBrush ) ; 
143+         } 
98144
99145        // Apply contrast correction 
100146        ApplyContrastCheck ( obj ) ; 
101147    } 
102148
149+     /// <summary> 
150+     /// Finds the <see cref="SolidColorBrush"/> and its associated <see cref="DependencyProperty"/> 
151+     /// from <paramref name="d"/>.. 
152+     /// </summary> 
153+     /// <param name="d">The attached <see cref="DependencyObject"/>.</param> 
154+     /// <param name="dp"> 
155+     /// The <see cref="DependencyProperty"/> associated with the <see cref="SolidColorBrush"/> 
156+     /// belonging to <paramref name="d"/>. 
157+     /// </param> 
158+     /// <returns>The <see cref="SolidColorBrush"/> for <paramref name="d"/>.</returns> 
103159    private  static   SolidColorBrush ?  FindBrush ( DependencyObject  d ,  out  DependencyProperty ?  dp ) 
104160    { 
105161        ( SolidColorBrush ?  brush ,  dp )  =  d  switch 
@@ -131,6 +187,7 @@ private static void AssignColor(DependencyObject d, Color color)
131187                break ; 
132188        } 
133189
190+         // Unlock the original color updates 
134191        _selfUpdate  =  false ; 
135192    } 
136193
@@ -151,8 +208,24 @@ private static double CalculateWCAGContrastRatio(Color color1, Color color2)
151208        return  ( lighter  +  0.05f )  /  ( darker  +  0.05f ) ; 
152209    } 
153210
154-     private  static   double  CalculatePerceivedLuminance ( Color  color )  => 
155-         // Using the formula for perceived luminance 
156-         // Source WCAG guidelines: https://www.w3.org/TR/AERT/#color-contrast 
157-         ( 0.299f  *  color . R  +  0.587f  *  color . G  +  0.114f  *  color . B )  /  255f ; 
211+     private  static   double  CalculatePerceivedLuminance ( Color  color ) 
212+     { 
213+         // Color theory is a massive iceberg. Here's a peek at the tippy top: 
214+ 
215+         // There's two (main) standards for calculating luminance from RGB values. 
216+         // ITU Rec. 709: Y = 0.2126 R + 0.7152 G + 0.0722 B 
217+         // ITU Rec. 601: Y = 0.299 R + 0.587 G + 0.114 B 
218+ 
219+         // They're based on the standard ability of the human eye to perceive brightness, 
220+         // from different colors, as well as the average monitor's ability to produce them. 
221+         // Both standards produce similar results, but Rec. 709 is more accurate for modern displays. 
222+ 
223+         // NOTE: If we for whatrever reason we ever need to optimize this code, 
224+         // we can make approximations using integer math instead of floating point math. 
225+         // The precise values are not critical, as long as the relative luminance is accurate. 
226+         // Like so: return (2 * color.R + 7 * color.G + color.B); 
227+ 
228+         // TLDR: We use ITU Rec. 709 standard formula for perceived luminance. 
229+         return  ( 0.2126f  *  color . R  +  0.7152f  *  color . G  +  0.0722  *  color . B )  /  255 ; 
230+     } 
158231} 
0 commit comments