// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using CommunityToolkit.WinUI.Deferred; using ManagedCommon; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Windows.Foundation; namespace Microsoft.CmdPal.UI.Controls; /// /// A helper control which takes an and creates the corresponding . /// public partial class IconBox : ContentControl { private const double DefaultIconFontSize = 16.0; private double _lastScale; private ElementTheme _lastTheme; private double _lastFontSize; /// /// Gets or sets the to display within the . Overwritten, if is used instead. /// public IconSource? Source { get => (IconSource?)GetValue(SourceProperty); set => SetValue(SourceProperty, value); } // Using a DependencyProperty as the backing store for Source. This enables animation, styling, binding, etc... public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(IconSource), typeof(IconBox), new PropertyMetadata(null, OnSourcePropertyChanged)); /// /// Gets or sets a value to use as the to retrieve an to set as the . /// public object? SourceKey { get => (object?)GetValue(SourceKeyProperty); set => SetValue(SourceKeyProperty, value); } // Using a DependencyProperty as the backing store for SourceKey. This enables animation, styling, binding, etc... public static readonly DependencyProperty SourceKeyProperty = DependencyProperty.Register(nameof(SourceKey), typeof(object), typeof(IconBox), new PropertyMetadata(null, OnSourceKeyPropertyChanged)); private TypedEventHandler? _sourceRequested; /// /// Gets or sets the event handler to provide the value of the for the property from the provided . /// public event TypedEventHandler? SourceRequested { add { _sourceRequested += value; if (_sourceRequested?.GetInvocationList().Length == 1) { Refresh(); } #if DEBUG if (_sourceRequested?.GetInvocationList().Length > 1) { Logger.LogWarning("There shouldn't be more than one handler for IconBox.SourceRequested"); } #endif } remove => _sourceRequested -= value; } public IconBox() { TabFocusNavigation = KeyboardNavigationMode.Once; IsTabStop = false; HorizontalContentAlignment = HorizontalAlignment.Center; VerticalContentAlignment = VerticalAlignment.Center; Loaded += OnLoaded; Unloaded += OnUnloaded; ActualThemeChanged += OnActualThemeChanged; SizeChanged += OnSizeChanged; UpdateLastFontSize(); } private void UpdateLastFontSize() { _lastFontSize = Pick(Width) ?? Pick(Height) ?? Pick(ActualWidth) ?? Pick(ActualHeight) ?? DefaultIconFontSize; return; static double? Pick(double value) => double.IsFinite(value) && value > 0 ? value : null; } private void OnSizeChanged(object s, SizeChangedEventArgs e) { UpdateLastFontSize(); if (Source is FontIconSource fontIcon) { fontIcon.FontSize = _lastFontSize; UpdatePaddingForFontIcon(); } } private void UpdatePaddingForFontIcon() => Padding = new Thickness(Math.Round(_lastFontSize * -0.2)); private void OnActualThemeChanged(FrameworkElement sender, object args) { if (_lastTheme == ActualTheme) { return; } _lastTheme = ActualTheme; Refresh(); } private void OnLoaded(object sender, RoutedEventArgs e) { _lastTheme = ActualTheme; UpdateLastFontSize(); if (XamlRoot is not null) { _lastScale = XamlRoot.RasterizationScale; XamlRoot.Changed += OnXamlRootChanged; } } private void OnUnloaded(object sender, RoutedEventArgs e) { if (XamlRoot is not null) { XamlRoot.Changed -= OnXamlRootChanged; } } private void OnXamlRootChanged(XamlRoot sender, XamlRootChangedEventArgs args) { var newScale = sender.RasterizationScale; var changedLastTheme = _lastTheme != ActualTheme; _lastTheme = ActualTheme; if ((changedLastTheme || Math.Abs(newScale - _lastScale) > 0.01) && SourceKey is not null) { _lastScale = newScale; UpdateSourceKey(this, SourceKey); } } private void Refresh() { UpdateSourceKey(this, SourceKey); } private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not IconBox self) { return; } switch (e.NewValue) { case null: self.Content = null; self.Padding = default; break; case FontIconSource fontIcon: self.UpdateLastFontSize(); if (self.Content is IconSourceElement iconSourceElement) { fontIcon.FontSize = self._lastFontSize; iconSourceElement.IconSource = fontIcon; } else { fontIcon.FontSize = self._lastFontSize; // For inexplicable reasons, FontIconSource.CreateIconElement // doesn't work, so do it ourselves // TODO: File platform bug? IconSourceElement elem = new() { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, IconSource = fontIcon, }; self.Content = elem; } self.UpdatePaddingForFontIcon(); break; case BitmapIconSource bitmapIcon: if (self.Content is IconSourceElement iconSourceElement2) { iconSourceElement2.IconSource = bitmapIcon; } else { self.Content = bitmapIcon.CreateIconElement(); } self.Padding = default; break; case IconSource source: self.Content = source.CreateIconElement(); self.Padding = default; break; default: throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource."); } } private static void OnSourceKeyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not IconBox self) { return; } UpdateSourceKey(self, e.NewValue); } private static void UpdateSourceKey(IconBox iconBox, object? sourceKey) { if (sourceKey is null) { iconBox.Source = null; return; } RequestIconFromSource(iconBox, sourceKey); } private static async void RequestIconFromSource(IconBox iconBox, object? sourceKey) { try { var iconBoxSourceRequestedHandler = iconBox._sourceRequested; if (iconBoxSourceRequestedHandler is null) { return; } var eventArgs = new SourceRequestedEventArgs(sourceKey, iconBox._lastTheme, iconBox._lastScale); await iconBoxSourceRequestedHandler.InvokeAsync(iconBox, eventArgs); // After the await: // Is the icon we're looking up now, the one we still // want to find? Since this IconBox might be used in a // list virtualization situation, it's very possible we // may have already been set to a new icon before we // even got back from the await. if (!ReferenceEquals(sourceKey, iconBox.SourceKey)) { // If the requested icon has changed, then just bail return; } iconBox.Source = eventArgs.Value; } catch (Exception ex) { // Exception from TryEnqueue bypasses the global error handler, // and crashes the app. Logger.LogError("Failed to set icon", ex); } } }