mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
CmdPal: Icon cache (#44538)
## Summary of the Pull Request This PR implements actual cache in IconCacheService and adds some fixes on top for free. The good - `IconCacheService` now caches decoded icons - Ensures that UI thread is not starved by loading icons by limiting number of threads that can load icons at any given time - `IconCacheService` decodes bitmaps directly to the required size to reduce memory usage - `IconBox` now reacts to theme, DPI scale, and size changes immediately - Introduced `AdaptiveCache` with time-based decay to improve icon reuse - Updated `IconCacheProvider` and `IconCacheService` to handle multiple icon sizes and scale-aware caching - Added priority-based decoding in `IconCacheService` for more responsive loading - Extended `IconPathConverter` to support target icon sizes - Switched hero images in `ShellPage` to use the jumbo icon cache - Made `MainWindow` title bar logic resilient to a null `XamlRoot` - Fixed Tag icon positioning - Removes custom `TypedEventHandlerExtensions` in favor of `CommunityToolkit.WinUI.Deferred`. The bad - Since IconData lacks a unique identity, when it includes a stream, it relies on simple reference equality, acknowledging that it might not be stable. We might cache some obsolete garbage because of this, but it is fast and better than nothing at all. Yet another task for the future me. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: - [ ] Closes: #38284 - [ ] Related to: #44407 - [ ] Related to: https://github.com/zadjii-msft/PowerToys/issues/333 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -899,6 +899,7 @@ LEFTTEXT
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
LFU
|
||||
lhwnd
|
||||
LIBFUZZER
|
||||
LIBID
|
||||
|
||||
@@ -32,6 +32,7 @@ using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
@@ -72,6 +73,8 @@ public partial class App : Application, IDisposable
|
||||
|
||||
Services = ConfigureServices();
|
||||
|
||||
IconCacheProvider.Initialize(Services);
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
// Ensure types used in XAML are preserved for AOT compilation
|
||||
@@ -113,12 +116,13 @@ public partial class App : Application, IDisposable
|
||||
|
||||
// Root services
|
||||
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
AddBuiltInCommands(services);
|
||||
|
||||
AddCoreServices(services);
|
||||
|
||||
AddUIServices(services);
|
||||
AddUIServices(services, dispatcherQueue);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
@@ -169,7 +173,7 @@ public partial class App : Application, IDisposable
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
}
|
||||
|
||||
private static void AddUIServices(ServiceCollection services)
|
||||
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
// Models
|
||||
var sm = SettingsModel.LoadSettings();
|
||||
@@ -188,6 +192,8 @@ public partial class App : Application, IDisposable
|
||||
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
services.AddSingleton<ResourceSwapper>();
|
||||
|
||||
services.AddIconServices(dispatcherQueue);
|
||||
}
|
||||
|
||||
private static void AddCoreServices(ServiceCollection services)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
Margin="4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
@@ -83,7 +83,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
Height="16"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SourceKey="{x:Bind Icon}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
// 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.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Deferred;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
@@ -20,7 +16,11 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
/// </summary>
|
||||
public partial class IconBox : ContentControl
|
||||
{
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
private double _lastScale;
|
||||
private ElementTheme _lastTheme;
|
||||
private double _lastFontSize;
|
||||
|
||||
private const double DefaultIconFontSize = 16.0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IconSource"/> to display within the <see cref="IconBox"/>. Overwritten, if <see cref="SourceKey"/> is used instead.
|
||||
@@ -48,10 +48,23 @@ public partial class IconBox : ContentControl
|
||||
public static readonly DependencyProperty SourceKeyProperty =
|
||||
DependencyProperty.Register(nameof(SourceKey), typeof(object), typeof(IconBox), new PropertyMetadata(null, OnSourceKeyPropertyChanged));
|
||||
|
||||
private TypedEventHandler<IconBox, SourceRequestedEventArgs>? _sourceRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="SourceRequested"/> event handler to provide the value of the <see cref="IconSource"/> for the <see cref="Source"/> property from the provided <see cref="SourceKey"/>.
|
||||
/// </summary>
|
||||
public event TypedEventHandler<IconBox, SourceRequestedEventArgs>? SourceRequested;
|
||||
public event TypedEventHandler<IconBox, SourceRequestedEventArgs>? SourceRequested
|
||||
{
|
||||
add
|
||||
{
|
||||
_sourceRequested += value;
|
||||
if (_sourceRequested?.GetInvocationList().Length == 1)
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
remove => _sourceRequested -= value;
|
||||
}
|
||||
|
||||
public IconBox()
|
||||
{
|
||||
@@ -59,119 +72,208 @@ public partial class IconBox : ContentControl
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (SourceKey is not null)
|
||||
{
|
||||
UpdateSourceKey(this, SourceKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is IconBox @this)
|
||||
if (d is not IconBox self)
|
||||
{
|
||||
switch (e.NewValue)
|
||||
{
|
||||
case null:
|
||||
@this.Content = null;
|
||||
break;
|
||||
case FontIconSource fontIco:
|
||||
fontIco.FontSize = double.IsNaN(@this.Width) ? @this.Height : @this.Width;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.NewValue)
|
||||
{
|
||||
case null:
|
||||
self.Content = null;
|
||||
self.Padding = default;
|
||||
break;
|
||||
case FontIconSource fontIcon:
|
||||
if (self.Content is IconSourceElement iconSourceElement)
|
||||
{
|
||||
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()
|
||||
{
|
||||
IconSource = fontIco,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
IconSource = fontIcon,
|
||||
};
|
||||
@this.Content = elem;
|
||||
break;
|
||||
case IconSource source:
|
||||
@this.Content = source.CreateIconElement();
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource.");
|
||||
}
|
||||
self.Content = elem;
|
||||
}
|
||||
|
||||
self.Padding = new Thickness(Math.Round(self._lastFontSize * -0.2));
|
||||
|
||||
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 IconBox @this)
|
||||
if (d is not IconBox self)
|
||||
{
|
||||
if (e.NewValue is null)
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateSourceKey(self, e.NewValue);
|
||||
}
|
||||
|
||||
private static void UpdateSourceKey(IconBox iconBox, object? sourceKey)
|
||||
{
|
||||
if (sourceKey is null)
|
||||
{
|
||||
iconBox.Source = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Callback(iconBox, sourceKey);
|
||||
}
|
||||
|
||||
private static async void Callback(IconBox iconBox, object? sourceKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var iconBoxSourceRequestedHandler = iconBox._sourceRequested;
|
||||
|
||||
if (iconBoxSourceRequestedHandler is null)
|
||||
{
|
||||
@this.Source = null;
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
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 (eventArgs.Key != sourceKey)
|
||||
{
|
||||
// TODO GH #239 switch back when using the new MD text block
|
||||
// Switching back to EnqueueAsync has broken icons in tags (they don't show)
|
||||
// _ = @this._queue.EnqueueAsync(() =>
|
||||
@this._queue.TryEnqueue(async void () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (@this.SourceRequested is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestedTheme = @this.ActualTheme;
|
||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||
|
||||
await @this.SourceRequested.InvokeAsync(@this, 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 (eventArgs.Key != @this.SourceKey)
|
||||
{
|
||||
// If the requested icon has changed, then just bail
|
||||
return;
|
||||
}
|
||||
|
||||
@this.Source = eventArgs.Value;
|
||||
|
||||
// Here's a little lesson in trickery:
|
||||
// Emoji are rendered just a bit bigger than Segoe Icons.
|
||||
// Just enough bigger that they get clipped if you put
|
||||
// them in a box at the same size.
|
||||
//
|
||||
// So, if the icon we get back was a font icon,
|
||||
// and the glyph for that icon is NOT in the range of
|
||||
// Segoe icons, then let's give the icon some extra space
|
||||
var iconData = eventArgs.Key switch
|
||||
{
|
||||
IconDataViewModel key => key,
|
||||
IconInfoViewModel info => requestedTheme == ElementTheme.Light ? info.Light : info.Dark,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (iconData?.Icon is not null && @this.Source is FontIconSource)
|
||||
{
|
||||
var iconSize =
|
||||
!double.IsNaN(@this.Width) ? @this.Width :
|
||||
!double.IsNaN(@this.Height) ? @this.Height :
|
||||
@this.ActualWidth > 0 ? @this.ActualWidth :
|
||||
@this.ActualHeight;
|
||||
|
||||
@this.Padding = new Thickness(Math.Round(iconSize * -0.2));
|
||||
}
|
||||
else
|
||||
{
|
||||
@this.Padding = default;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Exception from TryEnqueue bypasses the global error handler,
|
||||
// and crashes the app.
|
||||
Logger.LogError("Failed to set icon", ex);
|
||||
}
|
||||
});
|
||||
// If the requested icon has changed, then just bail
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventArgs.Value == iconBox.Source)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
/// <summary>
|
||||
/// See <see cref="IconBox.SourceRequested"/> event.
|
||||
/// </summary>
|
||||
public class SourceRequestedEventArgs(object? key, ElementTheme requestedTheme) : DeferredEventArgs
|
||||
public class SourceRequestedEventArgs(object? key, ElementTheme requestedTheme, double scale = 1.0) : DeferredEventArgs
|
||||
{
|
||||
public object? Key { get; private set; } = key;
|
||||
|
||||
public IconSource? Value { get; set; }
|
||||
|
||||
public ElementTheme Theme => requestedTheme;
|
||||
|
||||
public double Scale => scale;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
<local:IconBox
|
||||
x:Name="PART_Icon"
|
||||
Grid.Column="0"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="{Binding Text, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IconMarginConverter}}"
|
||||
SourceKey="{TemplateBinding Icon}" />
|
||||
|
||||
@@ -72,7 +72,7 @@ public partial class Tag : Control
|
||||
|
||||
if (GetTemplateChild(TagIconBox) is IconBox iconBox)
|
||||
{
|
||||
iconBox.SourceRequested += IconCacheProvider.SourceRequested;
|
||||
iconBox.SourceRequested += IconCacheProvider.SourceRequested20;
|
||||
iconBox.Visibility = HasIcon ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
|
||||
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
||||
|
||||
<x:Double x:Key="SmallGridSize">32</x:Double>
|
||||
<x:Double x:Key="MediumGridSize">48</x:Double>
|
||||
<x:Double x:Key="MediumGridContainerSize">100</x:Double>
|
||||
<x:Double x:Key="GalleryGridSize">160</x:Double>
|
||||
|
||||
<x:Double x:Key="ListViewItemMinHeight">40</x:Double>
|
||||
<x:Double x:Key="ListViewSectionMinHeight">0</x:Double>
|
||||
<x:Double x:Key="ListViewSeparatorMinHeight">0</x:Double>
|
||||
@@ -288,7 +293,7 @@
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
@@ -377,21 +382,21 @@
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="GridIconBorder"
|
||||
Width="28"
|
||||
Height="28"
|
||||
Width="{StaticResource SmallGridSize}"
|
||||
Height="{StaticResource SmallGridSize}"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested32}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
Width="100"
|
||||
Height="100"
|
||||
Width="{StaticResource MediumGridContainerSize}"
|
||||
Height="{StaticResource MediumGridContainerSize}"
|
||||
Padding="8"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
|
||||
@@ -403,15 +408,15 @@
|
||||
<cpcontrols:IconBox
|
||||
x:Name="GridIconBorder"
|
||||
Grid.Row="0"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Width="{StaticResource MediumGridSize}"
|
||||
Height="{StaticResource MediumGridSize}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="12"
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Row="1"
|
||||
@@ -430,7 +435,7 @@
|
||||
|
||||
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<StackPanel
|
||||
Width="160"
|
||||
Width="{StaticResource GalleryGridSize}"
|
||||
Margin="4"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -442,8 +447,8 @@
|
||||
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
|
||||
|
||||
<Grid
|
||||
Width="160"
|
||||
Height="160"
|
||||
Width="{StaticResource GalleryGridSize}"
|
||||
Height="{StaticResource GalleryGridSize}"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
@@ -456,7 +461,7 @@
|
||||
CornerRadius="4"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested256}" />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
|
||||
@@ -582,7 +587,7 @@
|
||||
Margin="8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
// 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 System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance, near-lock-free adaptive cache optimized for UI Icons.
|
||||
/// Eviction merely drops references to allow the GC to manage UI-bound lifetimes.
|
||||
/// </summary>
|
||||
internal sealed class AdaptiveCache<TKey, TValue>
|
||||
where TKey : IEquatable<TKey>
|
||||
{
|
||||
private readonly int _capacity;
|
||||
private readonly double _decayFactor;
|
||||
private readonly TimeSpan _decayInterval;
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, CacheEntry> _map;
|
||||
private readonly ConcurrentStack<CacheEntry> _pool = [];
|
||||
private readonly WaitCallback _maintenanceCallback;
|
||||
|
||||
private long _currentTick;
|
||||
private long _lastDecayTicks = DateTime.UtcNow.Ticks;
|
||||
private InterlockedBoolean _maintenanceSwitch = new(false);
|
||||
|
||||
public AdaptiveCache(int capacity = 384, TimeSpan? decayInterval = null, double decayFactor = 0.5)
|
||||
{
|
||||
_capacity = capacity;
|
||||
_decayInterval = decayInterval ?? TimeSpan.FromMinutes(5);
|
||||
_decayFactor = decayFactor;
|
||||
_map = new ConcurrentDictionary<TKey, CacheEntry>(Environment.ProcessorCount, capacity);
|
||||
|
||||
_maintenanceCallback = static state =>
|
||||
{
|
||||
var cache = (AdaptiveCache<TKey, TValue>)state!;
|
||||
try
|
||||
{
|
||||
cache.PerformCleanup();
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache._maintenanceSwitch.Clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public TValue GetOrAdd<TArg>(TKey key, Func<TKey, TArg, TValue> factory, TArg arg)
|
||||
{
|
||||
if (_map.TryGetValue(key, out var entry))
|
||||
{
|
||||
entry.Update(Interlocked.Increment(ref _currentTick));
|
||||
return entry.Value!;
|
||||
}
|
||||
|
||||
if (!_pool.TryPop(out var newEntry))
|
||||
{
|
||||
newEntry = new CacheEntry();
|
||||
}
|
||||
|
||||
var value = factory(key, arg);
|
||||
var tick = Interlocked.Increment(ref _currentTick);
|
||||
newEntry.Initialize(key, value, 1.0, tick);
|
||||
|
||||
if (!_map.TryAdd(key, newEntry))
|
||||
{
|
||||
newEntry.Clear();
|
||||
_pool.Push(newEntry);
|
||||
|
||||
if (_map.TryGetValue(key, out var existing))
|
||||
{
|
||||
existing.Update(tick);
|
||||
return existing.Value!;
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldMaintenanceRun())
|
||||
{
|
||||
TryRunMaintenance();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public bool TryGet(TKey key, [MaybeNullWhen(false)] out TValue value)
|
||||
{
|
||||
if (_map.TryGetValue(key, out var entry))
|
||||
{
|
||||
entry.Update(Interlocked.Increment(ref _currentTick));
|
||||
value = entry.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Add(TKey key, TValue value)
|
||||
{
|
||||
var tick = Interlocked.Increment(ref _currentTick);
|
||||
|
||||
if (_map.TryGetValue(key, out var existing))
|
||||
{
|
||||
existing.Update(tick);
|
||||
existing.SetValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_pool.TryPop(out var newEntry))
|
||||
{
|
||||
newEntry = new CacheEntry();
|
||||
}
|
||||
|
||||
newEntry.Initialize(key, value, 1.0, tick);
|
||||
|
||||
if (!_map.TryAdd(key, newEntry))
|
||||
{
|
||||
newEntry.Clear();
|
||||
_pool.Push(newEntry);
|
||||
}
|
||||
|
||||
if (ShouldMaintenanceRun())
|
||||
{
|
||||
TryRunMaintenance();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRemove(TKey key)
|
||||
{
|
||||
if (_map.TryRemove(key, out var evicted))
|
||||
{
|
||||
evicted.Clear();
|
||||
_pool.Push(evicted);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var key in _map.Keys)
|
||||
{
|
||||
TryRemove(key);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _currentTick, 0);
|
||||
}
|
||||
|
||||
private bool ShouldMaintenanceRun()
|
||||
{
|
||||
return _map.Count > _capacity || (DateTime.UtcNow.Ticks - Interlocked.Read(ref _lastDecayTicks)) > _decayInterval.Ticks;
|
||||
}
|
||||
|
||||
private void TryRunMaintenance()
|
||||
{
|
||||
if (_maintenanceSwitch.Set())
|
||||
{
|
||||
ThreadPool.UnsafeQueueUserWorkItem(_maintenanceCallback, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformCleanup()
|
||||
{
|
||||
var nowTicks = DateTime.UtcNow.Ticks;
|
||||
var isDecay = (nowTicks - Interlocked.Read(ref _lastDecayTicks)) > _decayInterval.Ticks;
|
||||
if (isDecay)
|
||||
{
|
||||
Interlocked.Exchange(ref _lastDecayTicks, nowTicks);
|
||||
}
|
||||
|
||||
var currentTick = Interlocked.Read(ref _currentTick);
|
||||
|
||||
foreach (var (key, entry) in _map)
|
||||
{
|
||||
if (isDecay)
|
||||
{
|
||||
entry.Decay(_decayFactor);
|
||||
}
|
||||
|
||||
var score = CalculateScore(entry, currentTick);
|
||||
|
||||
if (score < 0.1 || _map.Count > _capacity)
|
||||
{
|
||||
if (_map.TryRemove(key, out var evicted))
|
||||
{
|
||||
evicted.Clear();
|
||||
_pool.Push(evicted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the survival score of an entry.
|
||||
/// Higher score = stay in cache; Lower score = priority for eviction.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double CalculateScore(CacheEntry entry, long currentTick)
|
||||
{
|
||||
// Tuning parameter: How much weight to give recency vs frequency.
|
||||
// - a larger ageWeight makes the cache behave more like LRU (Least Recently Used).
|
||||
// - a smaller ageWeight makes it behave more like LFU (Least Frequently Used).
|
||||
const double ageWeight = 0.001;
|
||||
|
||||
var frequency = entry.GetFrequency();
|
||||
var age = currentTick - entry.GetLastAccess();
|
||||
|
||||
return frequency - (age * ageWeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single pooled entry in the cache, containing the value and
|
||||
/// atomic metadata for adaptive eviction logic.
|
||||
/// </summary>
|
||||
private sealed class CacheEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the key associated with this entry. Used primarily for identification during cleanup.
|
||||
/// </summary>
|
||||
public TKey Key { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cached value. This reference is cleared on eviction to allow GC collection.
|
||||
/// </summary>
|
||||
public TValue Value { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the frequency count as double bits to allow for Interlocked atomic math.
|
||||
/// Frequencies are decayed over time to ensure the cache adapts to new usage patterns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This allows the use of Interlocked.CompareExchange to perform thread-safe floating point
|
||||
/// arithmetic without a global lock.
|
||||
/// </remarks>
|
||||
private long _frequencyBits;
|
||||
|
||||
/// <summary>
|
||||
/// The tick (monotonically increasing counter) of the last time this entry was accessed.
|
||||
/// </summary>
|
||||
private long _lastAccessTick;
|
||||
|
||||
public void Initialize(TKey key, TValue value, double frequency, long lastAccessTick)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
_frequencyBits = BitConverter.DoubleToInt64Bits(frequency);
|
||||
_lastAccessTick = lastAccessTick;
|
||||
}
|
||||
|
||||
public void SetValue(TValue value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Key = default!;
|
||||
Value = default!;
|
||||
}
|
||||
|
||||
public void Update(long tick)
|
||||
{
|
||||
Interlocked.Exchange(ref _lastAccessTick, tick);
|
||||
long initial, updated;
|
||||
do
|
||||
{
|
||||
initial = Interlocked.Read(ref _frequencyBits);
|
||||
updated = BitConverter.DoubleToInt64Bits(BitConverter.Int64BitsToDouble(initial) + 1.0);
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref _frequencyBits, updated, initial) != initial);
|
||||
}
|
||||
|
||||
public void Decay(double factor)
|
||||
{
|
||||
long initial, updated;
|
||||
do
|
||||
{
|
||||
initial = Interlocked.Read(ref _frequencyBits);
|
||||
updated = BitConverter.DoubleToInt64Bits(BitConverter.Int64BitsToDouble(initial) * factor);
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref _frequencyBits, updated, initial) != initial);
|
||||
}
|
||||
|
||||
public double GetFrequency()
|
||||
{
|
||||
return BitConverter.Int64BitsToDouble(Interlocked.Read(ref _frequencyBits));
|
||||
}
|
||||
|
||||
public long GetLastAccess()
|
||||
{
|
||||
return Interlocked.Read(ref _lastAccessTick);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
|
||||
/// </summary>
|
||||
public static partial class IconCacheProvider
|
||||
{
|
||||
private static readonly IconCacheService IconService = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
|
||||
|
||||
#pragma warning disable IDE0060 // Remove unused parameter
|
||||
public static async void SourceRequested(IconBox sender, SourceRequestedEventArgs args)
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
{
|
||||
if (args.Key is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Key is IconDataViewModel iconData)
|
||||
{
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
args.Value = await IconService.GetIconSource(iconData);
|
||||
|
||||
deferral.Complete();
|
||||
}
|
||||
else if (args.Key is IconInfoViewModel iconInfo)
|
||||
{
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
var data = args.Theme == Microsoft.UI.Xaml.ElementTheme.Dark ? iconInfo.Dark : iconInfo.Light;
|
||||
args.Value = await IconService.GetIconSource(data);
|
||||
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// 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 System.Diagnostics;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
public Task<IconSource?> GetIconSource(IconDataViewModel icon) =>
|
||||
|
||||
// todo: actually implement a cache of some sort
|
||||
IconToSource(icon);
|
||||
|
||||
private async Task<IconSource?> IconToSource(IconDataViewModel icon)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(icon.Icon))
|
||||
{
|
||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false, icon.FontFamily);
|
||||
return source;
|
||||
}
|
||||
else if (icon.Data is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await StreamToIconSource(icon.Data.Unsafe!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine("Failed to load icon from stream: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<IconSource?> StreamToIconSource(IRandomAccessStreamReference iconStreamRef)
|
||||
{
|
||||
if (iconStreamRef is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bitmap = await IconStreamToBitmapImageAsync(iconStreamRef);
|
||||
var icon = new ImageIconSource() { ImageSource = bitmap };
|
||||
return icon;
|
||||
}
|
||||
|
||||
private async Task<BitmapImage> IconStreamToBitmapImageAsync(IRandomAccessStreamReference iconStreamRef)
|
||||
{
|
||||
// Return the bitmap image via TaskCompletionSource. Using WCT's EnqueueAsync does not suffice here, since if
|
||||
// we're already on the thread of the DispatcherQueue then it just directly calls the function, with no async involved.
|
||||
return await TryEnqueueAsync(dispatcherQueue, async () =>
|
||||
{
|
||||
using var bitmapStream = await iconStreamRef.OpenReadAsync();
|
||||
var itemImage = new BitmapImage();
|
||||
await itemImage.SetSourceAsync(bitmapStream);
|
||||
return itemImage;
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<T> TryEnqueueAsync<T>(DispatcherQueue dispatcher, Func<Task<T>> function)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource<T>();
|
||||
|
||||
var enqueued = dispatcher.TryEnqueue(DispatcherQueuePriority.Normal, async void () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await function();
|
||||
completionSource.SetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completionSource.SetException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
if (!enqueued)
|
||||
{
|
||||
completionSource.SetException(new InvalidOperationException("Failed to enqueue the operation on the UI dispatcher"));
|
||||
}
|
||||
|
||||
return completionSource.Task;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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 System.Runtime.CompilerServices;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed class CachedIconSourceProvider : IIconSourceProvider
|
||||
{
|
||||
private readonly AdaptiveCache<IconCacheKey, Task<IconSource?>> _cache;
|
||||
private readonly Size _iconSize;
|
||||
private readonly IconLoaderService _loader;
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
public CachedIconSourceProvider(IconLoaderService loader, Size iconSize, int cacheSize)
|
||||
{
|
||||
_loader = loader;
|
||||
_iconSize = iconSize;
|
||||
_cache = new AdaptiveCache<IconCacheKey, Task<IconSource?>>(cacheSize, TimeSpan.FromMinutes(60));
|
||||
}
|
||||
|
||||
public CachedIconSourceProvider(IconLoaderService loader, int iconSize, int cacheSize)
|
||||
: this(loader, new Size(iconSize, iconSize), cacheSize)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<IconSource?> GetIconSource(IconDataViewModel icon, double scale)
|
||||
{
|
||||
var key = new IconCacheKey(icon, scale);
|
||||
|
||||
return _cache.TryGet(key, out var existingTask)
|
||||
? existingTask
|
||||
: GetOrCreateSlowPath(key, icon, scale);
|
||||
}
|
||||
|
||||
private Task<IconSource?> GetOrCreateSlowPath(IconCacheKey key, IconDataViewModel icon, double scale)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_cache.TryGet(key, out var existingTask))
|
||||
{
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<IconSource?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
_loader.EnqueueLoad(
|
||||
icon.Icon,
|
||||
icon.FontFamily,
|
||||
icon.Data?.Unsafe,
|
||||
_iconSize,
|
||||
scale,
|
||||
tcs);
|
||||
|
||||
var task = tcs.Task;
|
||||
|
||||
_ = task.ContinueWith(
|
||||
_ =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_cache.TryRemove(key);
|
||||
}
|
||||
},
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
_cache.Add(key, task);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct IconCacheKey : IEquatable<IconCacheKey>
|
||||
{
|
||||
private readonly string? _icon;
|
||||
private readonly string? _fontFamily;
|
||||
private readonly int _streamRefHashCode;
|
||||
private readonly int _scale;
|
||||
|
||||
public IconCacheKey(IconDataViewModel icon, double scale)
|
||||
{
|
||||
_icon = icon.Icon;
|
||||
_fontFamily = icon.FontFamily;
|
||||
_streamRefHashCode = icon.Data?.Unsafe is { } stream
|
||||
? RuntimeHelpers.GetHashCode(stream)
|
||||
: 0;
|
||||
_scale = (int)(100 * Math.Round(scale, 2));
|
||||
}
|
||||
|
||||
public bool Equals(IconCacheKey other) =>
|
||||
_icon == other._icon &&
|
||||
_fontFamily == other._fontFamily &&
|
||||
_streamRefHashCode == other._streamRefHashCode &&
|
||||
_scale == other._scale;
|
||||
|
||||
public override bool Equals(object? obj) => obj is IconCacheKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(_icon, _fontFamily, _streamRefHashCode, _scale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal interface IIconLoaderService : IAsyncDisposable
|
||||
{
|
||||
void EnqueueLoad(
|
||||
string? iconString,
|
||||
string? fontFamily,
|
||||
IRandomAccessStreamReference? streamRef,
|
||||
Size iconSize,
|
||||
double scale,
|
||||
TaskCompletionSource<IconSource?> tcs,
|
||||
IconLoadPriority priority);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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 Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal interface IIconSourceProvider
|
||||
{
|
||||
Task<IconSource?> GetIconSource(IconDataViewModel icon, double scale);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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 Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
|
||||
/// </summary>
|
||||
public static partial class IconCacheProvider
|
||||
{
|
||||
/*
|
||||
Memory Usage Considerations (raw estimates):
|
||||
| Icon Size | Per Icon | Count | Total | Per Icon @ 200% | Total @ 200% | Per Icon @ 300% | Total @ 300% |
|
||||
| --------- | -------: | ----: | -------: | --------------: | -----------: | --------------: | -----------: |
|
||||
| 20×20 | 1.6 KB | 1024 | 1.6 MB | 6.4 KB | 6.4 MB | 14.4 KB | 14.4 MB |
|
||||
| 32×32 | 4.0 KB | 512 | 2.0 MB | 16 KB | 8.0 MB | 36.0 KB | 18.0 MB |
|
||||
| 48×48 | 9.0 KB | 256 | 2.3 MB | 36 KB | 9.0 MB | 81.0 KB | 20.3 MB |
|
||||
| 64×64 | 16.0 KB | 64 | 1.0 MB | 64 KB | 4.0 MB | 144.0 KB | 9.0 MB |
|
||||
| 256×256 | 256.0 KB | 64 | 16.0 MB | 1 MB | 64.0 MB | 2.3 MB | 144 MB |
|
||||
*/
|
||||
|
||||
private static IIconSourceProvider _provider20 = null!;
|
||||
private static IIconSourceProvider _provider32 = null!;
|
||||
private static IIconSourceProvider _provider64 = null!;
|
||||
private static IIconSourceProvider _provider256 = null!;
|
||||
|
||||
public static void Initialize(IServiceProvider serviceProvider)
|
||||
{
|
||||
_provider20 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size20);
|
||||
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
|
||||
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
|
||||
_provider256 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size256);
|
||||
}
|
||||
|
||||
private static async void SourceRequestedCore(IIconSourceProvider service, SourceRequestedEventArgs args)
|
||||
{
|
||||
if (args.Key is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
try
|
||||
{
|
||||
args.Value = args.Key switch
|
||||
{
|
||||
IconDataViewModel iconData => await service.GetIconSource(iconData, args.Scale),
|
||||
IconInfoViewModel iconInfo => await service.GetIconSource(
|
||||
args.Theme == Microsoft.UI.Xaml.ElementTheme.Light ? iconInfo.Light : iconInfo.Dark,
|
||||
args.Scale),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0060 // Remove unused parameter
|
||||
public static void SourceRequested20(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider20, args);
|
||||
|
||||
public static void SourceRequested32(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider32, args);
|
||||
|
||||
public static void SourceRequested64(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider64, args);
|
||||
|
||||
public static void SourceRequested256(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider256, args);
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal enum IconLoadPriority
|
||||
{
|
||||
Low,
|
||||
High,
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// 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 System.Threading.Channels;
|
||||
using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.Foundation;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
{
|
||||
private const DispatcherQueuePriority LoadingPriorityOnDispatcher = DispatcherQueuePriority.Low;
|
||||
private const int DefaultIconSize = 256;
|
||||
private const int MaxWorkerCount = 4;
|
||||
|
||||
private static readonly int WorkerCount = Math.Clamp(Environment.ProcessorCount / 2, 1, MaxWorkerCount);
|
||||
|
||||
private readonly Channel<Func<Task>> _highPriorityQueue = Channel.CreateBounded<Func<Task>>(32);
|
||||
private readonly Channel<Func<Task>> _lowPriorityQueue = Channel.CreateUnbounded<Func<Task>>();
|
||||
private readonly Task[] _workers;
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
public IconLoaderService(DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
_dispatcherQueue = dispatcherQueue;
|
||||
_workers = new Task[WorkerCount];
|
||||
|
||||
for (var i = 0; i < WorkerCount; i++)
|
||||
{
|
||||
_workers[i] = Task.Run(ProcessQueueAsync);
|
||||
}
|
||||
}
|
||||
|
||||
public void EnqueueLoad(
|
||||
string? iconString,
|
||||
string? fontFamily,
|
||||
IRandomAccessStreamReference? streamRef,
|
||||
Size iconSize,
|
||||
double scale,
|
||||
TaskCompletionSource<IconSource?> tcs,
|
||||
IconLoadPriority priority = IconLoadPriority.Low)
|
||||
{
|
||||
var workItem = () => LoadAndCompleteAsync(iconString, fontFamily, streamRef, iconSize, scale, tcs);
|
||||
|
||||
if (priority == IconLoadPriority.High)
|
||||
{
|
||||
if (_highPriorityQueue.Writer.TryWrite(workItem))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Logger.LogDebug("High priority icon queue full, falling back to low priority");
|
||||
#endif
|
||||
}
|
||||
|
||||
_lowPriorityQueue.Writer.TryWrite(workItem);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_highPriorityQueue.Writer.Complete();
|
||||
_lowPriorityQueue.Writer.Complete();
|
||||
|
||||
await Task.WhenAll(_workers).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ProcessQueueAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Func<Task>? workItem;
|
||||
|
||||
if (_highPriorityQueue.Reader.TryRead(out workItem))
|
||||
{
|
||||
await ExecuteWork(workItem).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var highWait = _highPriorityQueue.Reader.WaitToReadAsync().AsTask();
|
||||
var lowWait = _lowPriorityQueue.Reader.WaitToReadAsync().AsTask();
|
||||
|
||||
await Task.WhenAny(highWait, lowWait).ConfigureAwait(false);
|
||||
|
||||
// Check if both channels are completed (disposal)
|
||||
if (_highPriorityQueue.Reader.Completion.IsCompleted &&
|
||||
_lowPriorityQueue.Reader.Completion.IsCompleted)
|
||||
{
|
||||
// Drain any remaining items
|
||||
while (_highPriorityQueue.Reader.TryRead(out workItem))
|
||||
{
|
||||
await ExecuteWork(workItem).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
while (_lowPriorityQueue.Reader.TryRead(out workItem))
|
||||
{
|
||||
await ExecuteWork(workItem).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (_highPriorityQueue.Reader.TryRead(out workItem) ||
|
||||
_lowPriorityQueue.Reader.TryRead(out workItem))
|
||||
{
|
||||
await ExecuteWork(workItem).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task ExecuteWork(Func<Task> workItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
await workItem().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load icon", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAndCompleteAsync(
|
||||
string? iconString,
|
||||
string? fontFamily,
|
||||
IRandomAccessStreamReference? streamRef,
|
||||
Size iconSize,
|
||||
double scale,
|
||||
TaskCompletionSource<IconSource?> tcs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await LoadIconCoreAsync(iconString, fontFamily, streamRef, iconSize, scale).ConfigureAwait(false);
|
||||
tcs.TrySetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IconSource?> LoadIconCoreAsync(
|
||||
string? iconString,
|
||||
string? fontFamily,
|
||||
IRandomAccessStreamReference? streamRef,
|
||||
Size iconSize,
|
||||
double scale)
|
||||
{
|
||||
var scaledSize = new Size(iconSize.Width * scale, iconSize.Height * scale);
|
||||
|
||||
if (!string.IsNullOrEmpty(iconString))
|
||||
{
|
||||
return await _dispatcherQueue
|
||||
.EnqueueAsync(() => GetStringIconSource(iconString, fontFamily, scaledSize), LoadingPriorityOnDispatcher)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (streamRef != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var bitmapStream = await streamRef.OpenReadAsync().AsTask().ConfigureAwait(false);
|
||||
|
||||
return await _dispatcherQueue
|
||||
.EnqueueAsync(BuildImageSource, LoadingPriorityOnDispatcher)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
async Task<IconSource?> BuildImageSource()
|
||||
{
|
||||
var bitmap = new BitmapImage();
|
||||
ApplyDecodeSize(bitmap, scaledSize);
|
||||
await bitmap.SetSourceAsync(bitmapStream);
|
||||
return new ImageIconSource { ImageSource = bitmap };
|
||||
}
|
||||
}
|
||||
#pragma warning disable CS0168 // Variable is declared but never used
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CS0168 // Variable is declared but never used
|
||||
{
|
||||
#if DEBUG
|
||||
Logger.LogDebug($"Failed to open icon stream: {ex}");
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ApplyDecodeSize(BitmapImage bitmap, Size size)
|
||||
{
|
||||
if (size.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (size.Width >= size.Height)
|
||||
{
|
||||
bitmap.DecodePixelWidth = (int)size.Width;
|
||||
}
|
||||
else
|
||||
{
|
||||
bitmap.DecodePixelHeight = (int)size.Height;
|
||||
}
|
||||
}
|
||||
|
||||
private static IconSource? GetStringIconSource(string iconString, string? fontFamily, Size size)
|
||||
{
|
||||
var iconSize = (int)Math.Max(size.Width, size.Height);
|
||||
if (iconSize == 0)
|
||||
{
|
||||
iconSize = DefaultIconSize;
|
||||
}
|
||||
|
||||
return IconPathConverter.IconSourceMUX(iconString, false, fontFamily, iconSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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 Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal static class IconServiceRegistration
|
||||
{
|
||||
public static IServiceCollection AddIconServices(this IServiceCollection services, DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
// Single shared loader
|
||||
var loader = new IconLoaderService(dispatcherQueue);
|
||||
services.AddSingleton<IIconLoaderService>(loader);
|
||||
|
||||
// Keyed providers by size
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size20,
|
||||
(_, _) => new CachedIconSourceProvider(loader, 20, 1024));
|
||||
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size32,
|
||||
(_, _) => new IconSourceProvider(loader, 32));
|
||||
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size64,
|
||||
(_, _) => new CachedIconSourceProvider(loader, 64, 256));
|
||||
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size256,
|
||||
(_, _) => new CachedIconSourceProvider(loader, 256, 64));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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 Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed class IconSourceProvider : IIconSourceProvider
|
||||
{
|
||||
private readonly IconLoaderService _loader;
|
||||
private readonly Size _iconSize;
|
||||
|
||||
public IconSourceProvider(IconLoaderService loader, Size iconSize)
|
||||
{
|
||||
_loader = loader;
|
||||
_iconSize = iconSize;
|
||||
}
|
||||
|
||||
public IconSourceProvider(IconLoaderService loader, int iconSize)
|
||||
: this(loader, new Size(iconSize, iconSize))
|
||||
{
|
||||
}
|
||||
|
||||
public Task<IconSource?> GetIconSource(IconDataViewModel icon, double scale)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<IconSource?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
_loader.EnqueueLoad(
|
||||
icon.Icon,
|
||||
icon.FontFamily,
|
||||
icon.Data?.Unsafe,
|
||||
_iconSize,
|
||||
scale,
|
||||
tcs);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal enum WellKnownIconSize
|
||||
{
|
||||
Size20 = 20,
|
||||
Size32 = 32,
|
||||
Size64 = 64,
|
||||
Size256 = 256,
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// 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.Common.Deferred;
|
||||
using Windows.Foundation;
|
||||
|
||||
// Pilfered from CommunityToolkit.WinUI.Deferred
|
||||
namespace Microsoft.CmdPal.UI.Deferred;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions to <see cref="TypedEventHandler{TSender, TResult}"/> for Deferred Events.
|
||||
/// </summary>
|
||||
public static class TypedEventHandlerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Use to invoke an async <see cref="TypedEventHandler{TSender, TResult}"/> using <see cref="DeferredEventArgs"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="S">Type of sender.</typeparam>
|
||||
/// <typeparam name="R"><see cref="EventArgs"/> type.</typeparam>
|
||||
/// <param name="eventHandler"><see cref="TypedEventHandler{TSender, TResult}"/> to be invoked.</param>
|
||||
/// <param name="sender">Sender of the event.</param>
|
||||
/// <param name="eventArgs"><see cref="EventArgs"/> instance.</param>
|
||||
/// <returns><see cref="Task"/> to wait on deferred event handler.</returns>
|
||||
#pragma warning disable CA1715 // Identifiers should have correct prefix
|
||||
#pragma warning disable SA1314 // Type parameter names should begin with T
|
||||
public static Task InvokeAsync<S, R>(this TypedEventHandler<S, R> eventHandler, S sender, R eventArgs)
|
||||
#pragma warning restore SA1314 // Type parameter names should begin with T
|
||||
#pragma warning restore CA1715 // Identifiers should have correct prefix
|
||||
where R : DeferredEventArgs => InvokeAsync(eventHandler, sender, eventArgs, CancellationToken.None);
|
||||
|
||||
/// <summary>
|
||||
/// Use to invoke an async <see cref="TypedEventHandler{TSender, TResult}"/> using <see cref="DeferredEventArgs"/> with a <see cref="CancellationToken"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="S">Type of sender.</typeparam>
|
||||
/// <typeparam name="R"><see cref="EventArgs"/> type.</typeparam>
|
||||
/// <param name="eventHandler"><see cref="TypedEventHandler{TSender, TResult}"/> to be invoked.</param>
|
||||
/// <param name="sender">Sender of the event.</param>
|
||||
/// <param name="eventArgs"><see cref="EventArgs"/> instance.</param>
|
||||
/// <param name="cancellationToken"><see cref="CancellationToken"/> option.</param>
|
||||
/// <returns><see cref="Task"/> to wait on deferred event handler.</returns>
|
||||
#pragma warning disable CA1715 // Identifiers should have correct prefix
|
||||
#pragma warning disable SA1314 // Type parameter names should begin with T
|
||||
public static Task InvokeAsync<S, R>(this TypedEventHandler<S, R> eventHandler, S sender, R eventArgs, CancellationToken cancellationToken)
|
||||
#pragma warning restore SA1314 // Type parameter names should begin with T
|
||||
#pragma warning restore CA1715 // Identifiers should have correct prefix
|
||||
where R : DeferredEventArgs
|
||||
{
|
||||
if (eventHandler is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var tasks = eventHandler.GetInvocationList()
|
||||
.OfType<TypedEventHandler<S, R>>()
|
||||
.Select(invocationDelegate =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
invocationDelegate(sender, eventArgs);
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var deferral = eventArgs.GetCurrentDeferralAndReset();
|
||||
|
||||
return deferral?.WaitForCompletion(cancellationToken) ?? Task.CompletedTask;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
@@ -648,8 +648,14 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// Updates our window s.t. the top of the window is draggable.
|
||||
private void UpdateRegionsForCustomTitleBar()
|
||||
{
|
||||
var xamlRoot = RootElement.XamlRoot;
|
||||
if (xamlRoot is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Specify the interactive regions of the title bar.
|
||||
var scaleAdjustment = RootElement.XamlRoot.RasterizationScale;
|
||||
var scaleAdjustment = xamlRoot.RasterizationScale;
|
||||
|
||||
// Get the rectangle around our XAML content. We're going to mark this
|
||||
// rectangle as "Passthrough", so that the normal window operations
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
Height="16"
|
||||
Margin="0,3,8,0"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind Name}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -296,7 +296,7 @@
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
@@ -429,7 +429,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}"
|
||||
Visibility="{x:Bind HasHeroImage, Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind ViewModel.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
@@ -94,7 +94,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
@@ -165,7 +165,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
</controls:Case>
|
||||
<controls:Case Value="False">
|
||||
<Image
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Microsoft.Terminal.UI
|
||||
{
|
||||
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
|
||||
// static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale, String fontFamily);
|
||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale, String fontFamily, Int32 targetSize);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user