TextBoxCaretColor.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Runtime.CompilerServices; 6 using System.Runtime.InteropServices; 7 using CommunityToolkit.WinUI; 8 using Microsoft.UI; 9 using Microsoft.UI.Xaml; 10 using Microsoft.UI.Xaml.Controls; 11 using Microsoft.UI.Xaml.Media; 12 using Rectangle = Microsoft.UI.Xaml.Shapes.Rectangle; 13 14 namespace Microsoft.CmdPal.UI.Helpers; 15 16 /// <summary> 17 /// Attached property to color internal caret/overlay rectangles inside a TextBox 18 /// so they follow the TextBox's actual Foreground brush. 19 /// </summary> 20 public static class TextBoxCaretColor 21 { 22 public static readonly DependencyProperty SyncWithForegroundProperty = 23 DependencyProperty.RegisterAttached("SyncWithForeground", typeof(bool), typeof(TextBoxCaretColor), new PropertyMetadata(false, OnSyncCaretRectanglesChanged))!; 24 25 private static readonly ConditionalWeakTable<TextBox, State> States = []; 26 27 public static void SetSyncWithForeground(DependencyObject obj, bool value) 28 { 29 obj.SetValue(SyncWithForegroundProperty, value); 30 } 31 32 public static bool GetSyncWithForeground(DependencyObject obj) 33 { 34 return (bool)obj.GetValue(SyncWithForegroundProperty); 35 } 36 37 private static void OnSyncCaretRectanglesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 38 { 39 if (d is not TextBox tb) 40 { 41 return; 42 } 43 44 if ((bool)e.NewValue) 45 { 46 Attach(tb); 47 } 48 else 49 { 50 Detach(tb); 51 } 52 } 53 54 private static void Attach(TextBox tb) 55 { 56 if (States.TryGetValue(tb, out var st) && st.IsHooked) 57 { 58 return; 59 } 60 61 st ??= new State(); 62 st.IsHooked = true; 63 States.Remove(tb); 64 States.Add(tb, st); 65 66 tb.Loaded += TbOnLoaded; 67 tb.Unloaded += TbOnUnloaded; 68 tb.GotFocus += TbOnGotFocus; 69 70 st.ForegroundToken = tb.RegisterPropertyChangedCallback(Control.ForegroundProperty!, (_, _) => Apply(tb)); 71 72 if (tb.IsLoaded) 73 { 74 Apply(tb); 75 } 76 } 77 78 private static void Detach(TextBox tb) 79 { 80 if (!States.TryGetValue(tb, out var st)) 81 { 82 return; 83 } 84 85 tb.Loaded -= TbOnLoaded; 86 tb.Unloaded -= TbOnUnloaded; 87 tb.GotFocus -= TbOnGotFocus; 88 89 if (st.ForegroundToken != 0) 90 { 91 tb.UnregisterPropertyChangedCallback(Control.ForegroundProperty!, st.ForegroundToken); 92 st.ForegroundToken = 0; 93 } 94 95 st.IsHooked = false; 96 } 97 98 private static void TbOnLoaded(object sender, RoutedEventArgs e) 99 { 100 if (sender is TextBox tb) 101 { 102 Apply(tb); 103 } 104 } 105 106 private static void TbOnUnloaded(object sender, RoutedEventArgs e) 107 { 108 if (sender is TextBox tb) 109 { 110 Detach(tb); 111 } 112 } 113 114 private static void TbOnGotFocus(object sender, RoutedEventArgs e) 115 { 116 if (sender is TextBox tb) 117 { 118 Apply(tb); 119 } 120 } 121 122 private static void Apply(TextBox tb) 123 { 124 try 125 { 126 ApplyCore(tb); 127 } 128 catch (COMException) 129 { 130 // ignore 131 } 132 } 133 134 private static void ApplyCore(TextBox tb) 135 { 136 // Ensure template is realized 137 tb.ApplyTemplate(); 138 139 // Find the internal ScrollContentPresenter within the TextBox template 140 var scp = tb.FindDescendant<ScrollContentPresenter>(s => s.Name == "ScrollContentPresenter"); 141 if (scp is null) 142 { 143 return; 144 } 145 146 var brush = tb.Foreground; // use the actual current foreground brush 147 if (brush == null) 148 { 149 brush = new SolidColorBrush(Colors.Black); 150 } 151 152 foreach (var rect in scp.FindDescendants().OfType<Rectangle>()) 153 { 154 try 155 { 156 rect.Fill = brush; 157 rect.CompositeMode = ElementCompositeMode.SourceOver; 158 rect.Opacity = 0.9; 159 } 160 catch 161 { 162 // best-effort; some rectangles might be template-owned 163 } 164 } 165 } 166 167 private sealed class State 168 { 169 public long ForegroundToken { get; set; } 170 171 public bool IsHooked { get; set; } 172 } 173 }