Add extensible icon protocol processing

This commit is contained in:
Jiří Polášek
2026-08-12 06:16:20 +02:00
parent e6737dc6ea
commit 0917d43c15
14 changed files with 493 additions and 81 deletions

View File

@@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
using Windows.Storage.Streams;
@@ -56,10 +57,13 @@ internal sealed class CachedIconSourceProvider : IIconSourceProvider
IconDataViewModel icon,
double scale,
IconRequestMeasurement diagnostics = default,
IIconRequestDemand? demand = null)
IIconRequestDemand? demand = null,
ElementTheme theme = ElementTheme.Default)
{
var key = new IconCacheKey(icon, scale);
var partition = ClassifyCachePartition(icon.Icon);
var protocolProcessor = IconProtocolRegistry.Find(icon.Icon);
var cacheTheme = protocolProcessor?.GetCacheTheme(icon.Icon!, theme) ?? ElementTheme.Default;
var key = new IconCacheKey(icon, scale, cacheTheme);
var partition = ClassifyCachePartition(icon.Icon, protocolProcessor);
var cache = GetCache(partition);
var cacheSize = GetCacheSize(partition);
@@ -71,13 +75,14 @@ internal sealed class CachedIconSourceProvider : IIconSourceProvider
}
IconLoadDiagnostics.RecordCacheLookup(_iconSize, partition, cacheSize, hit: false);
return GetOrCreateSlowPath(key, icon, scale, partition, diagnostics, demand);
return GetOrCreateSlowPath(key, icon, scale, theme, partition, diagnostics, demand);
}
private Task<IconSource?> GetOrCreateSlowPath(
IconCacheKey key,
IconDataViewModel icon,
double scale,
ElementTheme theme,
IconCachePartition partition,
IconRequestMeasurement diagnostics,
IIconRequestDemand? demand)
@@ -150,6 +155,7 @@ internal sealed class CachedIconSourceProvider : IIconSourceProvider
streamReference,
_iconSize,
scale,
theme,
tcs,
IconLoadPriority.Low,
loadDiagnostics,
@@ -167,8 +173,15 @@ internal sealed class CachedIconSourceProvider : IIconSourceProvider
return task;
}
private static IconCachePartition ClassifyCachePartition(string? iconString)
private static IconCachePartition ClassifyCachePartition(
string? iconString,
IIconProtocolProcessor? protocolProcessor)
{
if (protocolProcessor is not null)
{
return protocolProcessor.CachePartition;
}
try
{
return FontIconGlyphClassifier.IsGlyphCandidate(iconString)
@@ -241,8 +254,9 @@ internal sealed class CachedIconSourceProvider : IIconSourceProvider
private readonly string? _fontFamily;
private readonly StreamIdentity? _streamIdentity;
private readonly int _scale;
private readonly ElementTheme _theme;
public IconCacheKey(IconDataViewModel icon, double scale)
public IconCacheKey(IconDataViewModel icon, double scale, ElementTheme cacheTheme)
{
_icon = icon.Icon;
_fontFamily = icon.FontFamily;
@@ -250,17 +264,19 @@ internal sealed class CachedIconSourceProvider : IIconSourceProvider
? StreamIdentities.GetValue(stream, static _ => new StreamIdentity())
: null;
_scale = (int)(100 * Math.Round(scale, 2));
_theme = cacheTheme;
}
public bool Equals(IconCacheKey other) =>
_icon == other._icon &&
_fontFamily == other._fontFamily &&
ReferenceEquals(_streamIdentity, other._streamIdentity) &&
_scale == other._scale;
_scale == other._scale &&
_theme == other._theme;
public override bool Equals(object? obj) => obj is IconCacheKey other && Equals(other);
public override int GetHashCode() => HashCode.Combine(_icon, _fontFamily, _streamIdentity, _scale);
public override int GetHashCode() => HashCode.Combine(_icon, _fontFamily, _streamIdentity, _scale, _theme);
}
// A RuntimeHelpers.GetHashCode value is not unique. Keep a weak mapping from each

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
using Windows.Storage.Streams;
@@ -24,6 +25,7 @@ internal interface IIconLoaderService : IAsyncDisposable
IRandomAccessStreamReference? streamRef,
Size iconSize,
double scale,
ElementTheme theme,
TaskCompletionSource<IconSource?> tcs,
IconLoadPriority priority,
IconLoadMeasurement? diagnostics = null,

View File

@@ -0,0 +1,30 @@
// 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.CodeAnalysis;
using Microsoft.UI.Xaml;
namespace Microsoft.CmdPal.UI.Helpers;
internal interface IIconProtocolProcessor
{
IconCachePartition CachePartition { get; }
ReadOnlySpan<string> ProtocolPrefixes { get; }
ElementTheme GetCacheTheme(string value, ElementTheme theme);
IconLoadInputKind ClassifyInput(string value);
bool TryPrepareSynchronously(
string value,
int targetSize,
ElementTheme theme,
[MaybeNullWhen(false)] out IconPathConverter.PreparedIcon preparedIcon);
ValueTask<IconProtocolProcessingResult> PrepareAsync(
string value,
int targetSize,
ElementTheme theme);
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI.Helpers;
@@ -13,5 +14,6 @@ internal interface IIconSourceProvider
IconDataViewModel icon,
double scale,
IconRequestMeasurement diagnostics = default,
IIconRequestDemand? demand = null);
IIconRequestDemand? demand = null,
ElementTheme theme = ElementTheme.Default);
}

View File

@@ -277,6 +277,11 @@ internal static class IconLoadDiagnostics
{
if (!string.IsNullOrEmpty(iconString))
{
if (IconProtocolRegistry.Find(iconString) is { } protocolProcessor)
{
return protocolProcessor.ClassifyInput(iconString);
}
var path = iconString.AsSpan();
var comma = path.IndexOf(',');
if (comma >= 0)

View File

@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.WinUI;
using ManagedCommon;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
@@ -121,6 +122,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
IRandomAccessStreamReference? streamRef,
Size iconSize,
double scale,
ElementTheme theme,
TaskCompletionSource<IconSource?> tcs,
IconLoadPriority priority = IconLoadPriority.Low,
IconLoadMeasurement? diagnostics = null,
@@ -134,6 +136,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
streamRef,
iconSize,
scale,
theme,
tcs,
diagnostics);
if (_queue.TryEnqueue(operation, priority, demand, out var actualPriority))
@@ -181,6 +184,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
IRandomAccessStreamReference? streamRef,
Size iconSize,
double scale,
ElementTheme theme,
TaskCompletionSource<IconSource?> tcs,
IconLoadMeasurement? diagnostics)
{
@@ -191,7 +195,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
diagnostics = null;
}
var result = await LoadIconCoreAsync(iconString, fontFamily, streamRef, iconSize, scale, diagnostics).ConfigureAwait(false);
var result = await LoadIconCoreAsync(iconString, fontFamily, streamRef, iconSize, scale, theme, diagnostics).ConfigureAwait(false);
diagnostics?.Complete();
tcs.TrySetResult(result);
}
@@ -208,6 +212,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
IRandomAccessStreamReference? streamRef,
Size iconSize,
double scale,
ElementTheme theme,
IconLoadMeasurement? diagnostics)
{
var scaledSize = iconSize.IsEmpty
@@ -220,11 +225,34 @@ internal sealed partial class IconLoaderService : IIconLoaderService
var targetSize = scaledSize.IsEmpty
? DefaultIconSize
: (int)Math.Max(scaledSize.Width, scaledSize.Height);
var preparedIcon = IconPathConverter.Prepare(iconString, fontFamily, targetSize);
diagnostics?.CompleteBackgroundPreparation(preparationStartedAt);
IconProtocolProcessingResult? protocolResult = null;
IconPathConverter.PreparedIcon? preparedIcon = null;
try
{
if (IconProtocolRegistry.Find(iconString) is not { } protocolProcessor)
{
preparedIcon = IconPathConverter.Prepare(iconString, fontFamily, targetSize, theme);
}
else if (!protocolProcessor.TryPrepareSynchronously(iconString, targetSize, theme, out preparedIcon))
{
protocolResult = await protocolProcessor.PrepareAsync(iconString, targetSize, theme).ConfigureAwait(false);
if (protocolResult.BitmapStream is { } bitmapStream)
{
diagnostics?.CompleteBackgroundPreparation(preparationStartedAt);
return await CreateImageIconSourceAsync(bitmapStream, scaledSize, diagnostics).ConfigureAwait(false);
}
preparedIcon = protocolResult.TakePreparedIcon();
if (preparedIcon is null && protocolResult.FallbackIconString is { } fallbackIconString)
{
preparedIcon = IconPathConverter.Prepare(fallbackIconString, fontFamily, targetSize, theme);
}
}
preparedIcon ??= IconPathConverter.PreparedIcon.Empty();
diagnostics?.CompleteBackgroundPreparation(preparationStartedAt);
var materializationKind = diagnostics is null
? IconDispatcherMaterializationKind.Unknown
: GetDispatcherMaterializationKind(preparedIcon);
@@ -331,7 +359,8 @@ internal sealed partial class IconLoaderService : IIconLoaderService
}
finally
{
preparedIcon.Dispose();
preparedIcon?.Dispose();
protocolResult?.Dispose();
}
}
@@ -342,70 +371,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
var preparationStartedAt = diagnostics?.BeginBackgroundPreparation() ?? 0;
using var bitmapStream = await streamRef.OpenReadAsync().AsTask().ConfigureAwait(false);
diagnostics?.CompleteBackgroundPreparation(preparationStartedAt);
var dispatcherEnqueuedAt = diagnostics?.BeginDispatcherWait(
IconDispatcherMaterializationKind.BitmapStream) ?? 0;
try
{
return await _dispatcherQueue
.EnqueueAsync(BuildImageSource, LoadingPriorityOnDispatcher)
.ConfigureAwait(false);
}
catch
{
// This is a no-op after the callback has started or completed.
diagnostics?.DispatcherWaitFailed(dispatcherEnqueuedAt);
throw;
}
async Task<IconSource?> BuildImageSource()
{
var dispatcherStartedAt = diagnostics?.DispatcherStarted(dispatcherEnqueuedAt) ?? 0;
var suspensionStartedAt = 0L;
var continuationStartedAt = 0L;
try
{
var bitmap = new BitmapImage();
ApplyDecodeSize(bitmap, scaledSize);
var operation = bitmap.SetSourceAsync(bitmapStream);
suspensionStartedAt = diagnostics?.DispatcherUiSliceCompleted(
dispatcherStartedAt,
IconDispatcherUiSliceKind.BeforeAsyncSuspension) ?? 0;
try
{
await operation;
}
finally
{
if (suspensionStartedAt != 0)
{
continuationStartedAt = diagnostics?.DispatcherAsyncSuspensionCompleted(
suspensionStartedAt) ?? 0;
}
}
var result = new ImageIconSource { ImageSource = bitmap };
diagnostics?.SetResult(result);
return result;
}
finally
{
if (suspensionStartedAt == 0)
{
diagnostics?.DispatcherUiSliceCompleted(
dispatcherStartedAt,
IconDispatcherUiSliceKind.SynchronousCallback);
}
else if (continuationStartedAt != 0)
{
diagnostics?.DispatcherUiSliceCompleted(
continuationStartedAt,
IconDispatcherUiSliceKind.AsyncContinuation);
}
diagnostics?.DispatcherCompleted(dispatcherStartedAt);
}
}
return await CreateImageIconSourceAsync(bitmapStream, scaledSize, diagnostics).ConfigureAwait(false);
}
#pragma warning disable CS0168 // Variable is declared but never used
catch (Exception ex)
@@ -433,6 +399,76 @@ internal sealed partial class IconLoaderService : IIconLoaderService
_ => IconDispatcherMaterializationKind.Unknown,
};
private async Task<IconSource?> CreateImageIconSourceAsync(
IRandomAccessStream bitmapStream,
Size scaledSize,
IconLoadMeasurement? diagnostics)
{
var dispatcherEnqueuedAt = diagnostics?.BeginDispatcherWait(
IconDispatcherMaterializationKind.BitmapStream) ?? 0;
try
{
return await _dispatcherQueue
.EnqueueAsync(BuildImageSource, LoadingPriorityOnDispatcher)
.ConfigureAwait(false);
}
catch
{
// This is a no-op after the callback has started or completed.
diagnostics?.DispatcherWaitFailed(dispatcherEnqueuedAt);
throw;
}
async Task<IconSource?> BuildImageSource()
{
var dispatcherStartedAt = diagnostics?.DispatcherStarted(dispatcherEnqueuedAt) ?? 0;
var suspensionStartedAt = 0L;
var continuationStartedAt = 0L;
try
{
var bitmap = new BitmapImage();
ApplyDecodeSize(bitmap, scaledSize);
var operation = bitmap.SetSourceAsync(bitmapStream);
suspensionStartedAt = diagnostics?.DispatcherUiSliceCompleted(
dispatcherStartedAt,
IconDispatcherUiSliceKind.BeforeAsyncSuspension) ?? 0;
try
{
await operation;
}
finally
{
if (suspensionStartedAt != 0)
{
continuationStartedAt = diagnostics?.DispatcherAsyncSuspensionCompleted(
suspensionStartedAt) ?? 0;
}
}
var result = new ImageIconSource { ImageSource = bitmap };
diagnostics?.SetResult(result);
return result;
}
finally
{
if (suspensionStartedAt == 0)
{
diagnostics?.DispatcherUiSliceCompleted(
dispatcherStartedAt,
IconDispatcherUiSliceKind.SynchronousCallback);
}
else if (continuationStartedAt != 0)
{
diagnostics?.DispatcherUiSliceCompleted(
continuationStartedAt,
IconDispatcherUiSliceKind.AsyncContinuation);
}
diagnostics?.DispatcherCompleted(dispatcherStartedAt);
}
}
}
private static void ApplyDecodeSize(BitmapImage bitmap, Size size)
{
if (size.IsEmpty)
@@ -458,6 +494,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
private readonly IRandomAccessStreamReference? _streamRef;
private readonly Size _iconSize;
private readonly double _scale;
private readonly ElementTheme _theme;
private readonly TaskCompletionSource<IconSource?> _completion;
private readonly IconLoadMeasurement? _diagnostics;
@@ -468,6 +505,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
IRandomAccessStreamReference? streamRef,
Size iconSize,
double scale,
ElementTheme theme,
TaskCompletionSource<IconSource?> completion,
IconLoadMeasurement? diagnostics)
{
@@ -477,6 +515,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
_streamRef = streamRef;
_iconSize = iconSize;
_scale = scale;
_theme = theme;
_completion = completion;
_diagnostics = diagnostics;
}
@@ -501,6 +540,7 @@ internal sealed partial class IconLoaderService : IIconLoaderService
_streamRef,
_iconSize,
_scale,
_theme,
_completion,
_diagnostics);

View File

@@ -6,6 +6,7 @@ using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
@@ -22,13 +23,32 @@ internal static partial class IconPathConverter
private const string InvalidGlyph = "\u25CC";
private const int DefaultBinaryIconSize = 256;
public static PreparedIcon Prepare(string iconPath, string? fontFamily, int targetSize)
public static PreparedIcon Prepare(
string iconPath,
string? fontFamily,
int targetSize,
ElementTheme theme = ElementTheme.Default)
{
if (string.IsNullOrEmpty(iconPath))
{
return PreparedIcon.Empty();
}
if (IconProtocolRegistry.Find(iconPath) is { } protocolProcessor)
{
try
{
return protocolProcessor.TryPrepareSynchronously(iconPath, targetSize, theme, out var protocolIcon)
? protocolIcon
: PreparedIcon.Empty();
}
catch
{
// A claimed protocol must not fall through and become a glyph or URI.
return PreparedIcon.Empty();
}
}
if (IconPathParser.TryParseBinaryIconReference(iconPath, out var binaryIcon))
{
var bitmap = ExtractBinaryIcon(binaryIcon, targetSize >= 0 ? targetSize : DefaultBinaryIconSize);

View File

@@ -0,0 +1,59 @@
// 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 Windows.Storage.Streams;
namespace Microsoft.CmdPal.UI.Helpers;
internal sealed partial class IconProtocolProcessingResult : IDisposable
{
private IconPathConverter.PreparedIcon? _preparedIcon;
private IRandomAccessStream? _bitmapStream;
private IconProtocolProcessingResult(
ResultKind kind,
IconPathConverter.PreparedIcon? preparedIcon = null,
IRandomAccessStream? bitmapStream = null,
string? fallbackIconString = null)
{
Kind = kind;
_preparedIcon = preparedIcon;
_bitmapStream = bitmapStream;
FallbackIconString = fallbackIconString;
}
public ResultKind Kind { get; }
public IRandomAccessStream? BitmapStream => _bitmapStream;
public string? FallbackIconString { get; }
public static IconProtocolProcessingResult Empty() => new(ResultKind.Empty);
public static IconProtocolProcessingResult FromPreparedIcon(IconPathConverter.PreparedIcon preparedIcon) =>
new(ResultKind.PreparedIcon, preparedIcon: preparedIcon);
public static IconProtocolProcessingResult FromBitmapStream(IRandomAccessStream bitmapStream) =>
new(ResultKind.BitmapStream, bitmapStream: bitmapStream);
public static IconProtocolProcessingResult FromFallbackIconString(string fallbackIconString) =>
new(ResultKind.FallbackIconString, fallbackIconString: fallbackIconString);
public IconPathConverter.PreparedIcon? TakePreparedIcon() =>
Interlocked.Exchange(ref _preparedIcon, null);
public void Dispose()
{
Interlocked.Exchange(ref _preparedIcon, null)?.Dispose();
Interlocked.Exchange(ref _bitmapStream, null)?.Dispose();
}
internal enum ResultKind
{
Empty,
PreparedIcon,
BitmapStream,
FallbackIconString,
}
}

View File

@@ -0,0 +1,82 @@
// 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 static class IconProtocolRegistry
{
// This is deliberately immutable after type initialization. Protocol lookup is
// used from the WinUI STA and loader workers, so it must not acquire a registry lock.
// Explicit construction also keeps the registry visible to Native AOT without reflection.
private static readonly IIconProtocolProcessor[] Processors = [];
static IconProtocolRegistry()
{
ValidateProcessors(Processors);
}
public static IIconProtocolProcessor? Find(string? value) => Find(value, Processors);
internal static IIconProtocolProcessor? Find(
string? value,
ReadOnlySpan<IIconProtocolProcessor> processors)
{
// Every registered protocol starts with '|'. This leaves ordinary glyphs and
// paths—the overwhelmingly common inputs—at one predictable character check.
if (string.IsNullOrEmpty(value) || value[0] != '|')
{
return null;
}
foreach (var processor in processors)
{
foreach (var prefix in processor.ProtocolPrefixes)
{
if (value.StartsWith(prefix, StringComparison.Ordinal))
{
return processor;
}
}
}
return null;
}
internal static void ValidateProcessors(ReadOnlySpan<IIconProtocolProcessor> processors)
{
List<string> declaredPrefixes = [];
for (var processorIndex = 0; processorIndex < processors.Length; processorIndex++)
{
var prefixes = processors[processorIndex].ProtocolPrefixes;
if (prefixes.IsEmpty)
{
throw new InvalidOperationException(
$"Icon protocol processor {processorIndex} declares no prefixes.");
}
for (var prefixIndex = 0; prefixIndex < prefixes.Length; prefixIndex++)
{
var prefix = prefixes[prefixIndex];
if (string.IsNullOrEmpty(prefix) || prefix[0] != '|')
{
throw new InvalidOperationException(
$"Icon protocol prefix {prefixIndex} on processor {processorIndex} must be non-empty and start with '|'.");
}
foreach (var declaredPrefix in declaredPrefixes)
{
if (prefix.StartsWith(declaredPrefix, StringComparison.Ordinal) ||
declaredPrefix.StartsWith(prefix, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Icon protocol prefixes '{declaredPrefix}' and '{prefix}' overlap; routing would depend on declaration order.");
}
}
declaredPrefixes.Add(prefix);
}
}
}
}

View File

@@ -48,12 +48,14 @@ public static partial class IconProvider
iconData,
args.Scale,
args.Diagnostics,
args),
args,
args.Theme),
IconInfoViewModel iconInfo => await service.GetIconSource(
args.Theme == Microsoft.UI.Xaml.ElementTheme.Light ? iconInfo.Light : iconInfo.Dark,
args.Scale,
args.Diagnostics,
args),
args,
args.Theme),
_ => null,
};
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
@@ -30,7 +31,8 @@ internal sealed class IconSourceProvider : IIconSourceProvider
IconDataViewModel icon,
double scale,
IconRequestMeasurement diagnostics = default,
IIconRequestDemand? demand = null)
IIconRequestDemand? demand = null,
ElementTheme theme = ElementTheme.Default)
{
var tcs = new TaskCompletionSource<IconSource?>(TaskCreationOptions.RunContinuationsAsynchronously);
IconLoadMeasurement? loadDiagnostics = null;
@@ -63,6 +65,7 @@ internal sealed class IconSourceProvider : IIconSourceProvider
streamReference,
_iconSize,
scale,
theme,
tcs,
_isPriority ? IconLoadPriority.High : IconLoadPriority.Low,
loadDiagnostics,

View File

@@ -8,6 +8,7 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Foundation;
@@ -397,11 +398,13 @@ public partial class CachedIconSourceProviderTests
IRandomAccessStreamReference? streamRef,
Size iconSize,
double scale,
ElementTheme theme,
TaskCompletionSource<IconSource?> tcs,
IconLoadPriority priority,
IconLoadMeasurement? diagnostics = null,
IconLoadDemand? demand = null)
{
_ = theme;
Interlocked.Increment(ref _enqueueCount);
LastDemand = demand;
if (!AcceptLoads)

View File

@@ -0,0 +1,145 @@
// 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.UI.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.UI.UnitTests;
[TestClass]
public class IconProtocolRegistryTests
{
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow("\uE700")]
[DataRow("C:\\Icons\\sample.svg")]
[DataRow("|Unknown|value")]
public void UnknownInputsDoNotEnterTheBuiltInRegistry(string? value)
{
Assert.IsNull(IconProtocolRegistry.Find(value));
}
[TestMethod]
public void OrdinaryInputsSkipProcessorPrefixAccess()
{
var processor = new TestProcessor("|Test|");
var result = IconProtocolRegistry.Find("ordinary.png", [processor]);
Assert.IsNull(result);
Assert.AreEqual(0, processor.PrefixAccesses);
}
[TestMethod]
public void RegistryReturnsMatchingProcessorWithoutInspectingLaterProcessors()
{
var first = new TestProcessor("|Other|");
var matching = new TestProcessor("|Test|", "|Alternate|");
var later = new TestProcessor("|Later|");
var result = IconProtocolRegistry.Find("|Alternate|value", [first, matching, later]);
Assert.AreSame(matching, result);
Assert.AreEqual(1, first.PrefixAccesses);
Assert.AreEqual(1, matching.PrefixAccesses);
Assert.AreEqual(0, later.PrefixAccesses);
}
[TestMethod]
public void ValidationAcceptsDistinctProtocolPrefixes()
{
IconProtocolRegistry.ValidateProcessors(
[
new TestProcessor("|Svg|", "|ThemedSvg|"),
new TestProcessor("|AppIcon|", "|JumboAppIcon|"),
]);
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow("Test|")]
public void ValidationRejectsMalformedProtocolPrefixes(string? prefix)
{
Assert.ThrowsException<InvalidOperationException>(() =>
IconProtocolRegistry.ValidateProcessors([new TestProcessor([prefix!])]));
}
[DataTestMethod]
[DataRow("|Test|", "|Test|")]
[DataRow("|Icon", "|IconX|")]
[DataRow("|Icon|", "|Icon|Variant|")]
public void ValidationRejectsDuplicateOrOverlappingProtocolPrefixes(string first, string second)
{
Assert.ThrowsException<InvalidOperationException>(() =>
IconProtocolRegistry.ValidateProcessors(
[new TestProcessor(first), new TestProcessor(second)]));
}
[TestMethod]
public void ValidationRejectsProcessorWithoutProtocolPrefixes()
{
Assert.ThrowsException<InvalidOperationException>(() =>
IconProtocolRegistry.ValidateProcessors([new TestProcessor()]));
}
[TestMethod]
public void ProcessingResultCanTransferPreparedIconOwnershipOnce()
{
var prepared = IconPathConverter.PreparedIcon.FromGlyph("\uE700", "Segoe Fluent Icons", 20);
using var result = IconProtocolProcessingResult.FromPreparedIcon(prepared);
var transferred = result.TakePreparedIcon();
Assert.IsNotNull(transferred);
Assert.AreSame(prepared, transferred);
Assert.IsNull(result.TakePreparedIcon());
transferred.Dispose();
}
private sealed class TestProcessor : IIconProtocolProcessor
{
private readonly string[] _prefixes;
public TestProcessor(params string[] prefixes)
{
_prefixes = prefixes;
}
public int PrefixAccesses { get; private set; }
public IconCachePartition CachePartition => IconCachePartition.Other;
public ReadOnlySpan<string> ProtocolPrefixes
{
get
{
PrefixAccesses++;
return _prefixes;
}
}
public ElementTheme GetCacheTheme(string value, ElementTheme theme) => ElementTheme.Default;
public IconLoadInputKind ClassifyInput(string value) => IconLoadInputKind.String;
public bool TryPrepareSynchronously(
string value,
int targetSize,
ElementTheme theme,
out IconPathConverter.PreparedIcon preparedIcon)
{
preparedIcon = IconPathConverter.PreparedIcon.Empty();
return true;
}
public ValueTask<IconProtocolProcessingResult> PrepareAsync(
string value,
int targetSize,
ElementTheme theme) =>
ValueTask.FromResult(IconProtocolProcessingResult.Empty());
}
}

View File

@@ -56,11 +56,14 @@
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconRequestStatus.cs" Link="Helpers\Icons\IconRequestStatus.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconPathConverter.cs" Link="Helpers\Icons\IconPathConverter.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconPathParser.cs" Link="Helpers\Icons\IconPathParser.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconProtocolProcessingResult.cs" Link="Helpers\Icons\IconProtocolProcessingResult.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconProtocolRegistry.cs" Link="Helpers\Icons\IconProtocolRegistry.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconRequestDemand.cs" Link="Helpers\Icons\IconRequestDemand.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconRequestDemandState.cs" Link="Helpers\Icons\IconRequestDemandState.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconSourceProvider.cs" Link="Helpers\Icons\IconSourceProvider.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconUiResponsivenessProbe.cs" Link="Helpers\Icons\IconUiResponsivenessProbe.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IIconLoaderService.cs" Link="Helpers\Icons\IIconLoaderService.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IIconProtocolProcessor.cs" Link="Helpers\Icons\IIconProtocolProcessor.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IIconRequestDemand.cs" Link="Helpers\Icons\IIconRequestDemand.cs" />
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IIconSourceProvider.cs" Link="Helpers\Icons\IIconSourceProvider.cs" />
</ItemGroup>