@@ -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