/ src / modules / cmdpal / Microsoft.CmdPal.UI / Helpers / TextBoxCaretColor.cs
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  }