mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
Convert C++ icon helpers to C#
Port glyph classification, icon path parsing, URI and glyph materialization, and shell binary extraction to managed code while preserving the native parsing quirks and fallback behavior. Cover the classifier, parser, and converter with focused tests. Keep ICU emoji-property calls on the normal GC-transition path because ICU initializes that data lazily, and preserve the rationale for returning a non-null empty BitmapIconSource in virtualized lists.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
// 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 readonly record struct BinaryIconReference(string Path, int Index);
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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.Buffers;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal static partial class FontIconGlyphClassifier
|
||||
{
|
||||
private const string FluentIconFontFamily = "Segoe Fluent Icons, Segoe MDL2 Assets";
|
||||
private const string EmojiFontFamily = "Segoe UI Emoji, Segoe UI";
|
||||
private const string GeneralFontFamily = "Segoe UI";
|
||||
private const int EmojiPresentationProperty = 58;
|
||||
private const int FluentIconsPrivateUseAreaStart = 0xE700;
|
||||
private const int FluentIconsPrivateUseAreaEnd = 0xF8FF;
|
||||
private const int TextVariationSelector = 0xFE0E;
|
||||
private const int EmojiVariationSelector = 0xFE0F;
|
||||
|
||||
public static FontIconGlyphKind Classify(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return FontIconGlyphKind.None;
|
||||
}
|
||||
|
||||
// Most CmdPal glyphs are one UTF-16 code unit in the Fluent icon PUA.
|
||||
if (text.Length == 1)
|
||||
{
|
||||
var character = text[0];
|
||||
if (char.IsHighSurrogate(character))
|
||||
{
|
||||
return FontIconGlyphKind.Invalid;
|
||||
}
|
||||
|
||||
if (IsFluentIconPua(character))
|
||||
{
|
||||
return FontIconGlyphKind.FluentSymbol;
|
||||
}
|
||||
|
||||
return IsEmoji(text) ? FontIconGlyphKind.Emoji : FontIconGlyphKind.Other;
|
||||
}
|
||||
|
||||
// Two adjacent ASCII characters cannot be the single glyph expected here. This
|
||||
// rejects common paths without paying for Unicode grapheme segmentation.
|
||||
if (text[0] <= 0x7F && text[1] <= 0x7F)
|
||||
{
|
||||
return FontIconGlyphKind.Invalid;
|
||||
}
|
||||
|
||||
var textElementLength = StringInfo.GetNextTextElementLength(text.AsSpan());
|
||||
if (textElementLength == 0)
|
||||
{
|
||||
return FontIconGlyphKind.None;
|
||||
}
|
||||
|
||||
if (textElementLength != text.Length)
|
||||
{
|
||||
return FontIconGlyphKind.Invalid;
|
||||
}
|
||||
|
||||
return IsEmoji(text) ? FontIconGlyphKind.Emoji : FontIconGlyphKind.Other;
|
||||
}
|
||||
|
||||
public static string GetFontFamily(FontIconGlyphKind glyphKind, string? requestedFontFamily)
|
||||
{
|
||||
if (glyphKind == FontIconGlyphKind.Invalid)
|
||||
{
|
||||
return GeneralFontFamily;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(requestedFontFamily))
|
||||
{
|
||||
return requestedFontFamily;
|
||||
}
|
||||
|
||||
return glyphKind switch
|
||||
{
|
||||
FontIconGlyphKind.FluentSymbol => FluentIconFontFamily,
|
||||
FontIconGlyphKind.Emoji => EmojiFontFamily,
|
||||
_ => GeneralFontFamily,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsFluentIconPua(int codePoint) =>
|
||||
codePoint is >= FluentIconsPrivateUseAreaStart and <= FluentIconsPrivateUseAreaEnd;
|
||||
|
||||
private static bool IsEmoji(string text)
|
||||
{
|
||||
foreach (var codePoint in text.EnumerateRunes())
|
||||
{
|
||||
if (codePoint.Value == EmojiVariationSelector)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (codePoint.Value == TextVariationSelector)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var status = Rune.DecodeFromUtf16(text.AsSpan(), out var first, out _);
|
||||
return status == OperationStatus.Done && NativeMethods.HasBinaryProperty(first.Value, EmojiPresentationProperty) != 0;
|
||||
}
|
||||
|
||||
private static partial class NativeMethods
|
||||
{
|
||||
// ICU lazily loads emoji property data behind a one-time lock, so this call
|
||||
// cannot safely suppress its GC transition.
|
||||
[LibraryImport("icu.dll", EntryPoint = "u_hasBinaryProperty")]
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
internal static partial byte HasBinaryProperty(int codePoint, int property);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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 FontIconGlyphKind
|
||||
{
|
||||
Invalid = -1,
|
||||
None = 0,
|
||||
Emoji = 1,
|
||||
FluentSymbol = 2,
|
||||
Other = 3,
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
@@ -98,9 +97,10 @@ internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
|
||||
private FontFamily GetOrCreateFontFamily(FontIconGlyphKind glyphKind, string? requestedFontFamily)
|
||||
{
|
||||
var familySource = FontIconGlyphClassifier.GetFontFamily(glyphKind, requestedFontFamily);
|
||||
if (!string.IsNullOrEmpty(requestedFontFamily))
|
||||
{
|
||||
return new FontFamily(requestedFontFamily);
|
||||
return new FontFamily(familySource);
|
||||
}
|
||||
|
||||
// TryLoadGlyph gates this method to the service's dispatcher thread, so these
|
||||
@@ -108,10 +108,10 @@ internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
return glyphKind switch
|
||||
{
|
||||
FontIconGlyphKind.FluentSymbol =>
|
||||
_fluentIconFontFamily ??= new FontFamily("Segoe Fluent Icons, Segoe MDL2 Assets"),
|
||||
_fluentIconFontFamily ??= new FontFamily(familySource),
|
||||
FontIconGlyphKind.Emoji =>
|
||||
_emojiFontFamily ??= new FontFamily("Segoe UI Emoji, Segoe UI"),
|
||||
_ => _generalFontFamily ??= new FontFamily("Segoe UI"),
|
||||
_emojiFontFamily ??= new FontFamily(familySource),
|
||||
_ => _generalFontFamily ??= new FontFamily(familySource),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -216,37 +216,89 @@ internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
|
||||
if (!string.IsNullOrEmpty(iconString))
|
||||
{
|
||||
var dispatcherEnqueuedAt = diagnostics?.BeginDispatcherWait(
|
||||
IconDispatcherMaterializationKind.Unknown) ?? 0;
|
||||
var preparationStartedAt = diagnostics?.BeginBackgroundPreparation() ?? 0;
|
||||
var targetSize = scaledSize.IsEmpty
|
||||
? DefaultIconSize
|
||||
: (int)Math.Max(scaledSize.Width, scaledSize.Height);
|
||||
var preparedIcon = IconPathConverter.Prepare(iconString, fontFamily, targetSize);
|
||||
diagnostics?.CompleteBackgroundPreparation(preparationStartedAt);
|
||||
|
||||
try
|
||||
{
|
||||
return await _dispatcherQueue
|
||||
.EnqueueAsync(
|
||||
() =>
|
||||
var materializationKind = diagnostics is null
|
||||
? IconDispatcherMaterializationKind.Unknown
|
||||
: GetDispatcherMaterializationKind(preparedIcon);
|
||||
var dispatcherEnqueuedAt = diagnostics?.BeginDispatcherWait(materializationKind) ?? 0;
|
||||
try
|
||||
{
|
||||
return await _dispatcherQueue
|
||||
.EnqueueAsync(CreateIconSourceOnDispatcher, LoadingPriorityOnDispatcher)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// This is a no-op after the callback has started or completed.
|
||||
diagnostics?.DispatcherWaitFailed(dispatcherEnqueuedAt);
|
||||
throw;
|
||||
}
|
||||
|
||||
async Task<IconSource?> CreateIconSourceOnDispatcher()
|
||||
{
|
||||
var dispatcherStartedAt = diagnostics?.DispatcherStarted(dispatcherEnqueuedAt) ?? 0;
|
||||
var suspensionStartedAt = 0L;
|
||||
var continuationStartedAt = 0L;
|
||||
try
|
||||
{
|
||||
var operation = IconPathConverter.CreateIconSourceAsync(preparedIcon);
|
||||
if (operation.IsCompleted)
|
||||
{
|
||||
var dispatcherStartedAt = diagnostics?.DispatcherStarted(dispatcherEnqueuedAt) ?? 0;
|
||||
try
|
||||
var synchronousResult = await operation;
|
||||
diagnostics?.SetResult(synchronousResult);
|
||||
return synchronousResult;
|
||||
}
|
||||
|
||||
suspensionStartedAt = diagnostics?.DispatcherUiSliceCompleted(
|
||||
dispatcherStartedAt,
|
||||
IconDispatcherUiSliceKind.BeforeAsyncSuspension) ?? 0;
|
||||
IconSource result;
|
||||
try
|
||||
{
|
||||
result = await operation;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (suspensionStartedAt != 0)
|
||||
{
|
||||
var result = GetStringIconSource(iconString, fontFamily, scaledSize);
|
||||
diagnostics?.SetResult(result);
|
||||
return result;
|
||||
continuationStartedAt = diagnostics?.DispatcherAsyncSuspensionCompleted(
|
||||
suspensionStartedAt) ?? 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
diagnostics?.DispatcherUiSliceCompleted(
|
||||
dispatcherStartedAt,
|
||||
IconDispatcherUiSliceKind.SynchronousCallback);
|
||||
diagnostics?.DispatcherCompleted(dispatcherStartedAt);
|
||||
}
|
||||
},
|
||||
LoadingPriorityOnDispatcher)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
finally
|
||||
{
|
||||
// This is a no-op after the callback has started or completed.
|
||||
diagnostics?.DispatcherWaitFailed(dispatcherEnqueuedAt);
|
||||
throw;
|
||||
preparedIcon.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +388,18 @@ internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IconDispatcherMaterializationKind GetDispatcherMaterializationKind(
|
||||
IconPathConverter.PreparedIcon preparedIcon) =>
|
||||
preparedIcon.Kind switch
|
||||
{
|
||||
IconPathConverter.PreparedIconKind.Empty => IconDispatcherMaterializationKind.Empty,
|
||||
IconPathConverter.PreparedIconKind.BitmapUri => IconDispatcherMaterializationKind.BitmapUri,
|
||||
IconPathConverter.PreparedIconKind.SvgUri => IconDispatcherMaterializationKind.SvgUri,
|
||||
IconPathConverter.PreparedIconKind.Glyph => IconDispatcherMaterializationKind.Glyph,
|
||||
IconPathConverter.PreparedIconKind.Binary => IconDispatcherMaterializationKind.Binary,
|
||||
_ => IconDispatcherMaterializationKind.Unknown,
|
||||
};
|
||||
|
||||
private static void ApplyDecodeSize(BitmapImage bitmap, Size size)
|
||||
{
|
||||
if (size.IsEmpty)
|
||||
@@ -353,14 +417,6 @@ internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
}
|
||||
}
|
||||
|
||||
private static IconSource? GetStringIconSource(string iconString, string? fontFamily, Size size)
|
||||
{
|
||||
var iconSize = size.IsEmpty
|
||||
? DefaultIconSize
|
||||
: (int)Math.Max(size.Width, size.Height);
|
||||
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
|
||||
}
|
||||
|
||||
private sealed class IconLoadOperation : IconLoadQueue.Operation
|
||||
{
|
||||
private readonly IconLoaderService _owner;
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// 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.InteropServices;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.Graphics.Imaging;
|
||||
using DrawingIcon = System.Drawing.Icon;
|
||||
using DrawingImageLockMode = System.Drawing.Imaging.ImageLockMode;
|
||||
using DrawingPixelFormat = System.Drawing.Imaging.PixelFormat;
|
||||
using DrawingRectangle = System.Drawing.Rectangle;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(iconPath))
|
||||
{
|
||||
return PreparedIcon.Empty();
|
||||
}
|
||||
|
||||
if (IconPathParser.TryParseBinaryIconReference(iconPath, out var binaryIcon))
|
||||
{
|
||||
var bitmap = ExtractBinaryIcon(binaryIcon, targetSize >= 0 ? targetSize : DefaultBinaryIconSize);
|
||||
return PreparedIcon.FromBinary(bitmap);
|
||||
}
|
||||
|
||||
// Font glyphs start outside ASCII, while every supported URI starts inside it.
|
||||
// Avoid using exception-based URI probing for the common Fluent glyph case.
|
||||
if (iconPath[0] < 128 && Uri.TryCreate(iconPath, UriKind.Absolute, out var uri))
|
||||
{
|
||||
var isSvg = Path.GetExtension(uri.AbsolutePath).Equals(".svg", StringComparison.OrdinalIgnoreCase);
|
||||
return PreparedIcon.FromUri(uri, isSvg, targetSize);
|
||||
}
|
||||
|
||||
var glyphKind = FontIconGlyphClassifier.Classify(iconPath);
|
||||
var glyph = glyphKind == FontIconGlyphKind.Invalid ? InvalidGlyph : iconPath;
|
||||
var family = FontIconGlyphClassifier.GetFontFamily(glyphKind, fontFamily);
|
||||
return PreparedIcon.FromGlyph(glyph, family, targetSize > 0 ? targetSize : 8);
|
||||
}
|
||||
|
||||
public static async Task<IconSource> CreateIconSourceAsync(PreparedIcon icon)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (icon.Kind)
|
||||
{
|
||||
case PreparedIconKind.BitmapUri:
|
||||
var bitmap = new BitmapImage
|
||||
{
|
||||
DecodePixelWidth = icon.TargetSize > 0 ? icon.TargetSize : 0,
|
||||
UriSource = icon.Uri!,
|
||||
};
|
||||
return new ImageIconSource { ImageSource = bitmap };
|
||||
|
||||
case PreparedIconKind.SvgUri:
|
||||
var svg = new SvgImageSource(icon.Uri!);
|
||||
if (icon.TargetSize > 0)
|
||||
{
|
||||
svg.RasterizePixelWidth = icon.TargetSize;
|
||||
}
|
||||
|
||||
return new ImageIconSource { ImageSource = svg };
|
||||
|
||||
case PreparedIconKind.Glyph:
|
||||
return new FontIconSource
|
||||
{
|
||||
FontFamily = new FontFamily(icon.FontFamily!),
|
||||
FontSize = icon.TargetSize,
|
||||
Glyph = icon.Glyph!,
|
||||
};
|
||||
|
||||
case PreparedIconKind.Binary:
|
||||
var softwareBitmap = icon.TakeSoftwareBitmap();
|
||||
if (softwareBitmap is null)
|
||||
{
|
||||
return new ImageIconSource();
|
||||
}
|
||||
|
||||
var ownershipTransferred = false;
|
||||
try
|
||||
{
|
||||
var bitmapSource = new SoftwareBitmapSource();
|
||||
try
|
||||
{
|
||||
await bitmapSource.SetBitmapAsync(softwareBitmap);
|
||||
|
||||
var iconSource = new ImageIconSource { ImageSource = bitmapSource };
|
||||
|
||||
// SetBitmapAsync can finish before WinUI's AsyncCopyToSurfaceTask.
|
||||
// Once XAML accepts the bitmap, explicitly closing either object can
|
||||
// fail-fast that later copy with RO_E_CLOSED. Release both through
|
||||
// their normal WinRT reference lifetimes instead.
|
||||
ownershipTransferred = true;
|
||||
return iconSource;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// The source has not escaped to a caller or visual tree.
|
||||
bitmapSource.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!ownershipTransferred)
|
||||
{
|
||||
softwareBitmap.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return CreateEmptyIconSource();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return icon.Kind == PreparedIconKind.Binary
|
||||
? new ImageIconSource()
|
||||
: CreateEmptyIconSource();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the empty value non-null. A virtualized ListView can crash when a
|
||||
// data-bound IconSourceElement alternates between null and non-null sources;
|
||||
// a BitmapIconSource with a null URI remains visually empty without crossing
|
||||
// that unstable boundary.
|
||||
private static BitmapIconSource CreateEmptyIconSource() => new() { UriSource = null };
|
||||
|
||||
private static SoftwareBitmap? ExtractBinaryIcon(BinaryIconReference iconReference, int targetSize)
|
||||
{
|
||||
nint iconHandle = 0;
|
||||
try
|
||||
{
|
||||
_ = NativeMethods.SHDefExtractIcon(
|
||||
iconReference.Path,
|
||||
iconReference.Index,
|
||||
0,
|
||||
out iconHandle,
|
||||
0,
|
||||
(uint)targetSize);
|
||||
if (iconHandle == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var icon = DrawingIcon.FromHandle(iconHandle);
|
||||
using var sourceBitmap = icon.ToBitmap();
|
||||
using var bitmap = sourceBitmap.Clone(
|
||||
new DrawingRectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height),
|
||||
DrawingPixelFormat.Format32bppPArgb);
|
||||
|
||||
var rectangle = new DrawingRectangle(0, 0, bitmap.Width, bitmap.Height);
|
||||
var bitmapData = bitmap.LockBits(rectangle, DrawingImageLockMode.ReadOnly, DrawingPixelFormat.Format32bppPArgb);
|
||||
try
|
||||
{
|
||||
var bytesPerRow = checked(bitmap.Width * 4);
|
||||
var pixels = GC.AllocateUninitializedArray<byte>(checked(bytesPerRow * bitmap.Height));
|
||||
for (var row = 0; row < bitmap.Height; row++)
|
||||
{
|
||||
var source = nint.Add(bitmapData.Scan0, row * bitmapData.Stride);
|
||||
Marshal.Copy(source, pixels, row * bytesPerRow, bytesPerRow);
|
||||
}
|
||||
|
||||
var softwareBitmap = new SoftwareBitmap(
|
||||
BitmapPixelFormat.Bgra8,
|
||||
bitmap.Width,
|
||||
bitmap.Height,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
softwareBitmap.CopyFromBuffer(pixels.AsBuffer());
|
||||
return softwareBitmap;
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmap.UnlockBits(bitmapData);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (iconHandle != 0)
|
||||
{
|
||||
_ = NativeMethods.DestroyIcon(iconHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class PreparedIcon : IDisposable
|
||||
{
|
||||
private SoftwareBitmap? _softwareBitmap;
|
||||
|
||||
private PreparedIcon(
|
||||
PreparedIconKind kind,
|
||||
Uri? uri = null,
|
||||
string? glyph = null,
|
||||
string? fontFamily = null,
|
||||
SoftwareBitmap? softwareBitmap = null,
|
||||
int targetSize = 0)
|
||||
{
|
||||
Kind = kind;
|
||||
Uri = uri;
|
||||
Glyph = glyph;
|
||||
FontFamily = fontFamily;
|
||||
_softwareBitmap = softwareBitmap;
|
||||
TargetSize = targetSize;
|
||||
}
|
||||
|
||||
public PreparedIconKind Kind { get; }
|
||||
|
||||
public Uri? Uri { get; }
|
||||
|
||||
public string? Glyph { get; }
|
||||
|
||||
public string? FontFamily { get; }
|
||||
|
||||
public SoftwareBitmap? SoftwareBitmap => _softwareBitmap;
|
||||
|
||||
public int TargetSize { get; }
|
||||
|
||||
public static PreparedIcon Empty() => new(PreparedIconKind.Empty);
|
||||
|
||||
public static PreparedIcon FromUri(Uri uri, bool isSvg, int targetSize) =>
|
||||
new(isSvg ? PreparedIconKind.SvgUri : PreparedIconKind.BitmapUri, uri: uri, targetSize: targetSize);
|
||||
|
||||
public static PreparedIcon FromGlyph(string glyph, string fontFamily, int targetSize) =>
|
||||
new(PreparedIconKind.Glyph, glyph: glyph, fontFamily: fontFamily, targetSize: targetSize);
|
||||
|
||||
public static PreparedIcon FromBinary(SoftwareBitmap? bitmap) =>
|
||||
new(PreparedIconKind.Binary, softwareBitmap: bitmap);
|
||||
|
||||
// Asynchronous materialization takes ownership before the PreparedIcon is
|
||||
// disposed. On success, XAML owns the bitmap's remaining lifetime.
|
||||
public SoftwareBitmap? TakeSoftwareBitmap() =>
|
||||
Interlocked.Exchange(ref _softwareBitmap, null);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Interlocked.Exchange(ref _softwareBitmap, null)?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal enum PreparedIconKind
|
||||
{
|
||||
Empty,
|
||||
BitmapUri,
|
||||
SvgUri,
|
||||
Glyph,
|
||||
Binary,
|
||||
}
|
||||
|
||||
private static partial class NativeMethods
|
||||
{
|
||||
[LibraryImport("shell32.dll", EntryPoint = "SHDefExtractIconW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
internal static partial int SHDefExtractIcon(
|
||||
string iconFile,
|
||||
int iconIndex,
|
||||
uint flags,
|
||||
out nint largeIcon,
|
||||
nint smallIcon,
|
||||
uint iconSize);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
internal static partial int DestroyIcon(nint icon);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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 IconPathParser
|
||||
{
|
||||
private const uint UnsignedConversionError = uint.MaxValue;
|
||||
private const int SignedConversionError = int.MaxValue;
|
||||
|
||||
public static bool TryParseBinaryIconReference(string iconPath, out BinaryIconReference iconReference)
|
||||
{
|
||||
var commaIndex = iconPath.IndexOf(',');
|
||||
var path = commaIndex < 0 ? iconPath : iconPath[..commaIndex];
|
||||
|
||||
if (!path.EndsWith(".exe", StringComparison.Ordinal)
|
||||
&& !path.EndsWith(".dll", StringComparison.Ordinal)
|
||||
&& !path.EndsWith(".lnk", StringComparison.Ordinal))
|
||||
{
|
||||
iconReference = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var index = 0;
|
||||
if (commaIndex >= 0)
|
||||
{
|
||||
index = ParseNativeIconIndex(iconPath.AsSpan(commaIndex + 1));
|
||||
if (index == SignedConversionError)
|
||||
{
|
||||
iconReference = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
iconReference = new(path, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Preserve til::to_int quirks from the native converter: '-' is recognized anywhere,
|
||||
// A-F digits are accepted even for decimal and octal input, and values at or above
|
||||
// uint.MaxValue / 16 are rejected.
|
||||
private static int ParseNativeIconIndex(ReadOnlySpan<char> text)
|
||||
{
|
||||
var signPosition = text.IndexOf('-');
|
||||
var hasSign = signPosition >= 0;
|
||||
var unsignedText = hasSign ? text[(signPosition + 1)..] : text;
|
||||
var result = ParseNativeUnsignedLong(unsignedText);
|
||||
if (result == UnsignedConversionError)
|
||||
{
|
||||
return SignedConversionError;
|
||||
}
|
||||
|
||||
return hasSign ? -(int)result : (int)result;
|
||||
}
|
||||
|
||||
private static uint ParseNativeUnsignedLong(ReadOnlySpan<char> text)
|
||||
{
|
||||
const uint maximumValue = uint.MaxValue / 16;
|
||||
|
||||
var numberBase = 10u;
|
||||
var position = 0;
|
||||
if (text.Length > 1 && text[0] == '0')
|
||||
{
|
||||
numberBase = 8;
|
||||
position++;
|
||||
if (text.Length > 2 && text[position] is 'x' or 'X')
|
||||
{
|
||||
numberBase = 16;
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
if (position == text.Length)
|
||||
{
|
||||
return UnsignedConversionError;
|
||||
}
|
||||
|
||||
var accumulator = 0u;
|
||||
while (true)
|
||||
{
|
||||
var character = text[position];
|
||||
uint value;
|
||||
if (character is >= '0' and <= '9')
|
||||
{
|
||||
value = (uint)(character - '0');
|
||||
}
|
||||
else if (character is >= 'A' and <= 'F')
|
||||
{
|
||||
value = (uint)(character - 'A') + 10u;
|
||||
}
|
||||
else if (character is >= 'a' and <= 'f')
|
||||
{
|
||||
value = (uint)(character - 'a') + 10u;
|
||||
}
|
||||
else
|
||||
{
|
||||
return UnsignedConversionError;
|
||||
}
|
||||
|
||||
accumulator = unchecked(accumulator + value);
|
||||
if (accumulator >= maximumValue)
|
||||
{
|
||||
return UnsignedConversionError;
|
||||
}
|
||||
|
||||
position++;
|
||||
if (position == text.Length)
|
||||
{
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
accumulator = unchecked(accumulator * numberBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
<PackageReference Include="WinUIEx" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "FontIconGlyphClassifier.h"
|
||||
#include "FontIconGlyphClassifier.g.cpp"
|
||||
|
||||
#include <icu.h>
|
||||
#include <utility>
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Check if the code point is in the Private Use Area range used by Fluent UI icons.
|
||||
[[nodiscard]] constexpr bool _isFluentIconPua(const UChar32 cp) noexcept
|
||||
{
|
||||
constexpr UChar32 fluentIconsPrivateUseAreaStart = 0xE700;
|
||||
constexpr UChar32 fluentIconsPrivateUseAreaEnd = 0xF8FF;
|
||||
return cp >= fluentIconsPrivateUseAreaStart && cp <= fluentIconsPrivateUseAreaEnd;
|
||||
}
|
||||
|
||||
// Determine if the given text (as a sequence of UChar code units) is emoji
|
||||
[[nodiscard]] bool _isEmoji(const UChar* p, const int32_t length) noexcept
|
||||
{
|
||||
if (!p || length < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://www.unicode.org/reports/tr51/#Emoji_Variation_Selector_Notes
|
||||
constexpr UChar32 vs15CodePoint = 0xFE0E; // Variation Selectors 15: text variation selector
|
||||
constexpr UChar32 vs16CodePoint = 0xFE0F; // Variation Selectors: 16 emoji variation selector
|
||||
|
||||
// Decode the first code point correctly (surrogate-safe)
|
||||
int32_t i0{ 0 };
|
||||
UChar32 first{ 0 };
|
||||
U16_NEXT(p, i0, length, first);
|
||||
|
||||
for (int32_t i = 0; i < length;)
|
||||
{
|
||||
UChar32 cp{ 0 };
|
||||
U16_NEXT(p, i, length, cp);
|
||||
|
||||
if (cp == vs16CodePoint) { return true; }
|
||||
if (cp == vs15CodePoint) { return false; }
|
||||
}
|
||||
|
||||
return !U_IS_SURROGATE(first) && u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION);
|
||||
}
|
||||
}
|
||||
|
||||
bool FontIconGlyphClassifier::IsLikelyToBeEmojiOrSymbolIcon(const hstring& text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.size() == 1 && !IS_HIGH_SURROGATE(text[0]))
|
||||
{
|
||||
// If it's a single code unit, it's definitely either zero or one grapheme clusters.
|
||||
// If it turns out to be illegal Unicode, we don't really care.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (text.size() >= 2 && text[0] <= 0x7F && text[1] <= 0x7F)
|
||||
{
|
||||
// Two adjacent ASCII characters (as seen in most file paths) aren't a single
|
||||
// grapheme cluster.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use ICU to determine whether text is composed of a single grapheme cluster.
|
||||
int32_t off{ 0 };
|
||||
UErrorCode status{ U_ZERO_ERROR };
|
||||
|
||||
UBreakIterator* const bi{ ubrk_open(UBRK_CHARACTER,
|
||||
nullptr,
|
||||
reinterpret_cast<const UChar*>(text.data()),
|
||||
static_cast<int>(text.size()),
|
||||
&status) };
|
||||
if (bi)
|
||||
{
|
||||
if (U_SUCCESS(status))
|
||||
{
|
||||
off = ubrk_next(bi);
|
||||
}
|
||||
ubrk_close(bi);
|
||||
}
|
||||
return std::cmp_equal(off, text.size());
|
||||
}
|
||||
|
||||
FontIconGlyphKind FontIconGlyphClassifier::Classify(hstring const& text) noexcept
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return FontIconGlyphKind::None;
|
||||
}
|
||||
|
||||
const size_t textSize{ text.size() };
|
||||
const auto* buffer{ reinterpret_cast<const UChar*>(text.c_str()) };
|
||||
|
||||
// Fast path 1: Single UTF-16 code unit (most common case)
|
||||
if (textSize == 1)
|
||||
{
|
||||
const UChar ch{ buffer[0] };
|
||||
|
||||
if (IS_HIGH_SURROGATE(ch))
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
if (_isFluentIconPua(ch))
|
||||
{
|
||||
return FontIconGlyphKind::FluentSymbol;
|
||||
}
|
||||
|
||||
if (_isEmoji(&ch, 1))
|
||||
{
|
||||
return FontIconGlyphKind::Emoji;
|
||||
}
|
||||
|
||||
return FontIconGlyphKind::Other;
|
||||
}
|
||||
|
||||
// Fast path 2: Common file path pattern - two ASCII printable characters
|
||||
if (textSize >= 2 && buffer[0] <= 0x7F && buffer[1] <= 0x7F)
|
||||
{
|
||||
// Definitely multiple graphemes
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
// Expensive path: Use ICU to determine grapheme boundaries
|
||||
UErrorCode status{ U_ZERO_ERROR };
|
||||
|
||||
UBreakIterator* bi{ ubrk_open(UBRK_CHARACTER,
|
||||
nullptr,
|
||||
buffer,
|
||||
static_cast<int32_t>(textSize),
|
||||
&status) };
|
||||
|
||||
if (U_FAILURE(status) || !bi)
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
const int32_t start{ ubrk_first(bi) };
|
||||
const int32_t end{ ubrk_next(bi) }; // end of first grapheme
|
||||
ubrk_close(bi);
|
||||
|
||||
// No graphemes found
|
||||
if (end == UBRK_DONE || end <= start)
|
||||
{
|
||||
return FontIconGlyphKind::None;
|
||||
}
|
||||
|
||||
// If there's more than one grapheme, it's not a valid icon glyph
|
||||
if (std::cmp_not_equal(end, textSize))
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
// Exactly one grapheme: classify
|
||||
const UChar* grapheme = buffer + start;
|
||||
const int32_t graphemeLength = end - start;
|
||||
|
||||
if (graphemeLength == 1 && _isFluentIconPua(grapheme[0]))
|
||||
{
|
||||
return FontIconGlyphKind::FluentSymbol;
|
||||
}
|
||||
|
||||
if (_isEmoji(grapheme, graphemeLength))
|
||||
{
|
||||
return FontIconGlyphKind::Emoji;
|
||||
}
|
||||
|
||||
return FontIconGlyphKind::Other;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "FontIconGlyphClassifier.g.h"
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
struct FontIconGlyphClassifier
|
||||
{
|
||||
[[nodiscard]] static bool IsLikelyToBeEmojiOrSymbolIcon(const winrt::hstring& text);
|
||||
|
||||
[[nodiscard]] static FontIconGlyphKind Classify(winrt::hstring const& text) noexcept;
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::factory_implementation
|
||||
{
|
||||
BASIC_FACTORY(FontIconGlyphClassifier);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft.Terminal.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Categorizes the type of a single grapheme cluster or input text.
|
||||
/// Used to determine how the input should be handled or rendered (for example,
|
||||
/// whether it should be treated as an emoji, an icon from a symbol font, plain text, etc.).
|
||||
/// </summary>
|
||||
enum FontIconGlyphKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Input is invalid or contains more than one grapheme cluster and therefore cannot be
|
||||
/// treated as a single symbol. Typical for multi-character text like file paths
|
||||
/// or composed strings that include separators.
|
||||
/// </summary>
|
||||
Invalid = -1,
|
||||
|
||||
/// <summary>
|
||||
/// No grapheme present (empty string). Indicates absence of a symbol.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A single emoji grapheme cluster. This may consist of multiple Unicode code
|
||||
/// points combined into one visible glyph (e.g., emoji with modifiers or ZWJ sequences).
|
||||
/// </summary>
|
||||
Emoji = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A single glyph from the Segoe Fluent Icons / MDL2 Assets Private Use Area (PUA),
|
||||
/// typically in the Unicode range U+E700–U+F8FF. These are font-based icons (Fluent/MDL2).
|
||||
/// </summary>
|
||||
FluentSymbol = 2,
|
||||
|
||||
/// <summary>
|
||||
/// A single non-emoji grapheme that is not a Fluent/MDL2 PUA symbol.
|
||||
/// Covers ordinary characters, letters, numbers, or other single glyph symbols.
|
||||
/// </summary>
|
||||
Other = 3,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Static utility class for text and icon analysis
|
||||
/// </summary>
|
||||
static runtimeclass FontIconGlyphClassifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if text represents a single grapheme cluster (emoji/symbol icon).
|
||||
/// Uses ICU for Unicode boundary detection to distinguish icons from file paths.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to analyze</param>
|
||||
/// <returns>True if single grapheme cluster, false for multi-character text or paths</returns>
|
||||
static Boolean IsLikelyToBeEmojiOrSymbolIcon(String text);
|
||||
|
||||
/// <summary>
|
||||
/// Classifies the input into a glyph kind suitable for icon or text rendering.
|
||||
/// </summary>
|
||||
static FontIconGlyphKind Classify(String text);
|
||||
};
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "IconPathConverter.h"
|
||||
#include "IconPathConverter.g.cpp"
|
||||
|
||||
#include "FontIconGlyphClassifier.h"
|
||||
|
||||
#include <Shlobj.h>
|
||||
#include <Shlobj_core.h>
|
||||
#include <wincodec.h>
|
||||
|
||||
namespace winrt
|
||||
{
|
||||
namespace MUX = Microsoft::UI::Xaml;
|
||||
}
|
||||
|
||||
using namespace winrt::Windows;
|
||||
using namespace winrt::Windows::UI::Xaml;
|
||||
|
||||
using namespace winrt::Windows::Graphics::Imaging;
|
||||
using namespace winrt::Windows::Storage::Streams;
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
// These are templates that help us figure out which BitmapIconSource/FontIconSource to use for a given IconSource.
|
||||
// We have to do this because some of our code still wants to use WUX/MUX IconSources.
|
||||
#pragma region BitmapIconSource
|
||||
template<typename TIconSource>
|
||||
struct BitmapIconSource
|
||||
{
|
||||
};
|
||||
|
||||
template<>
|
||||
struct BitmapIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource>
|
||||
{
|
||||
using type = winrt::Microsoft::UI::Xaml::Controls::BitmapIconSource;
|
||||
};
|
||||
|
||||
/*template<>
|
||||
struct BitmapIconSource<winrt::Windows::UI::Xaml::Controls::IconSource>
|
||||
{
|
||||
using type = winrt::Windows::UI::Xaml::Controls::BitmapIconSource;
|
||||
};*/
|
||||
#pragma endregion
|
||||
|
||||
#pragma region FontIconSource
|
||||
template<typename TIconSource>
|
||||
struct FontIconSource
|
||||
{
|
||||
};
|
||||
|
||||
template<>
|
||||
struct FontIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource>
|
||||
{
|
||||
using type = winrt::Microsoft::UI::Xaml::Controls::FontIconSource;
|
||||
};
|
||||
|
||||
/*template<>
|
||||
struct FontIconSource<winrt::Windows::UI::Xaml::Controls::IconSource>
|
||||
{
|
||||
using type = winrt::Windows::UI::Xaml::Controls::FontIconSource;
|
||||
};*/
|
||||
#pragma endregion
|
||||
|
||||
#pragma region PathIconSource
|
||||
template<typename TIconSource>
|
||||
struct PathIconSource
|
||||
{
|
||||
};
|
||||
|
||||
template<>
|
||||
struct PathIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource>
|
||||
{
|
||||
using type = winrt::Microsoft::UI::Xaml::Controls::PathIconSource;
|
||||
};
|
||||
#pragma endregion
|
||||
#pragma region ImageIconSource
|
||||
template<typename TIconSource>
|
||||
struct ImageIconSource
|
||||
{
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ImageIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource>
|
||||
{
|
||||
using type = winrt::Microsoft::UI::Xaml::Controls::ImageIconSource;
|
||||
};
|
||||
#pragma endregion
|
||||
|
||||
// Method Description:
|
||||
// - Creates an IconSource for the given path. The icon returned is a colored
|
||||
// icon. If we couldn't create the icon for any reason, we return an empty
|
||||
// IconElement.
|
||||
// Template Types:
|
||||
// - <TIconSource>: The type of IconSource (MUX, WUX) to generate.
|
||||
// Arguments:
|
||||
// - path: the full, expanded path to the icon.
|
||||
// - targetSize: the target size for decoding/rasterizing the icon.
|
||||
// Return Value:
|
||||
// - An IconElement with its IconSource set, if possible.
|
||||
template<typename TIconSource>
|
||||
TIconSource _getColoredBitmapIcon(const winrt::hstring& path, int targetSize)
|
||||
{
|
||||
// FontIcon uses glyphs in the private use area, whereas valid URIs only contain ASCII characters.
|
||||
// To skip throwing on Uri construction, we can quickly check if the first character is ASCII.
|
||||
if (path.empty() || path.front() >= 128)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
winrt::Windows::Foundation::Uri iconUri{ path };
|
||||
|
||||
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
|
||||
{
|
||||
typename ImageIconSource<TIconSource>::type iconSource;
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||
if (targetSize > 0)
|
||||
{
|
||||
source.RasterizePixelWidth(static_cast<double>(targetSize));
|
||||
// Set only single dimension here; the image might not be square and
|
||||
// this will preserve the aspect ratio (for the price of keeping height unbound).
|
||||
// source.RasterizePixelHeight(static_cast<double>(targetSize));
|
||||
}
|
||||
iconSource.ImageSource(source);
|
||||
return iconSource;
|
||||
}
|
||||
else
|
||||
{
|
||||
typename ImageIconSource<TIconSource>::type iconSource;
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::BitmapImage bitmapImage;
|
||||
if (targetSize > 0)
|
||||
{
|
||||
bitmapImage.DecodePixelWidth(targetSize);
|
||||
// Set only single dimension here; the image might not be square and
|
||||
// this will preserve the aspect ratio (for the price of keeping height unbound).
|
||||
// bitmapImage.DecodePixelHeight(targetSize);
|
||||
}
|
||||
bitmapImage.UriSource(iconUri);
|
||||
iconSource.ImageSource(bitmapImage);
|
||||
return iconSource;
|
||||
}
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static winrt::hstring _expandIconPath(const hstring& iconPath)
|
||||
{
|
||||
if (iconPath.empty())
|
||||
{
|
||||
return iconPath;
|
||||
}
|
||||
// winrt::hstring envExpandedPath{ wil::ExpandEnvironmentStringsW<std::wstring>(iconPath.c_str()) };
|
||||
winrt::hstring envExpandedPath{ iconPath };
|
||||
return envExpandedPath;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Creates an IconSource for the given path.
|
||||
// * If the icon is a path to an image, we'll use that.
|
||||
// * If it isn't, then we'll try and use the text as a FontIcon. If the
|
||||
// character is in the range of symbols reserved for the Segoe MDL2
|
||||
// Asserts, well treat it as such. Otherwise, we'll default to a Sego
|
||||
// UI icon, so things like emoji will work.
|
||||
// * If we couldn't create the icon for any reason, we return an empty
|
||||
// IconElement.
|
||||
// Template Types:
|
||||
// - <TIconSource>: The type of IconSource (MUX, WUX) to generate.
|
||||
// Arguments:
|
||||
// - path: the unprocessed path to the icon.
|
||||
// Return Value:
|
||||
// - An IconElement with its IconSource set, if possible.
|
||||
template<typename TIconSource>
|
||||
TIconSource _getIconSource(const winrt::hstring& iconPath, const winrt::hstring& fontFamily, const int targetSize)
|
||||
{
|
||||
TIconSource iconSource{ nullptr };
|
||||
|
||||
if (iconPath.size() != 0)
|
||||
{
|
||||
const auto expandedIconPath{ _expandIconPath(iconPath) };
|
||||
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, targetSize);
|
||||
|
||||
// If we fail to set the icon source using the "icon" as a path,
|
||||
// let's try it as a symbol/emoji.
|
||||
if (!iconSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto glyph_kind = FontIconGlyphClassifier::Classify(iconPath);
|
||||
|
||||
winrt::hstring family;
|
||||
if (glyph_kind == FontIconGlyphKind::Invalid)
|
||||
{
|
||||
family = L"Segoe UI";
|
||||
}
|
||||
else if (!fontFamily.empty())
|
||||
{
|
||||
family = fontFamily;
|
||||
}
|
||||
else if (glyph_kind == FontIconGlyphKind::FluentSymbol)
|
||||
{
|
||||
family = L"Segoe Fluent Icons, Segoe MDL2 Assets";
|
||||
}
|
||||
else if (glyph_kind == FontIconGlyphKind::Emoji)
|
||||
{
|
||||
// Emoji and other symbols go in the Segoe UI Emoji font.
|
||||
// Some emojis (e.g. 2️⃣) would be rendered as emoji glyphs otherwise.
|
||||
family = L"Segoe UI Emoji, Segoe UI";
|
||||
}
|
||||
else
|
||||
{
|
||||
family = L"Segoe UI";
|
||||
}
|
||||
|
||||
typename FontIconSource<TIconSource>::type icon;
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family });
|
||||
icon.FontSize(targetSize > 0 ? targetSize : 8);
|
||||
icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath);
|
||||
iconSource = icon;
|
||||
}
|
||||
CATCH_LOG();
|
||||
}
|
||||
}
|
||||
|
||||
if (!iconSource)
|
||||
{
|
||||
// Set the default IconSource to a BitmapIconSource with a null source
|
||||
// (instead of just nullptr) because there's a really weird crash when swapping
|
||||
// data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette).
|
||||
// Swapping between nullptr IconSources and non-null IconSources causes a crash
|
||||
// to occur, but swapping between IconSources with a null source and non-null IconSources
|
||||
// work perfectly fine :shrug:.
|
||||
typename BitmapIconSource<TIconSource>::type icon;
|
||||
icon.UriSource(nullptr);
|
||||
iconSource = icon;
|
||||
}
|
||||
|
||||
return iconSource;
|
||||
}
|
||||
|
||||
// Windows::UI::Xaml::Controls::IconSource IconPathConverter::IconSourceWUX(const hstring& path)
|
||||
// {
|
||||
// // * If the icon is a path to an image, we'll use that.
|
||||
// // * If it isn't, then we'll try and use the text as a FontIcon. If the
|
||||
// // character is in the range of symbols reserved for the Segoe MDL2
|
||||
// // Asserts, well treat it as such. Otherwise, we'll default to a Segoe
|
||||
// // UI icon, so things like emoji will work.
|
||||
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false);
|
||||
// }
|
||||
|
||||
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, const winrt::hstring& fontFamily, const int targetSize)
|
||||
{
|
||||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, fontFamily, targetSize);
|
||||
}
|
||||
|
||||
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
|
||||
BitmapPixelFormat pixelFormat,
|
||||
BitmapAlphaMode alphaMode,
|
||||
IWICImagingFactory* imagingFactory)
|
||||
{
|
||||
// Load the icon into an IWICBitmap
|
||||
wil::com_ptr<IWICBitmap> iconBitmap;
|
||||
THROW_IF_FAILED(imagingFactory->CreateBitmapFromHICON(hicon, iconBitmap.put()));
|
||||
|
||||
// Put the IWICBitmap into a SoftwareBitmap. This may fail if WICBitmap's format is not supported by
|
||||
// SoftwareBitmap. CreateBitmapFromHICON always creates RGBA8 so we're ok.
|
||||
auto softwareBitmap = winrt::capture<SoftwareBitmap>(
|
||||
winrt::create_instance<ISoftwareBitmapNativeFactory>(CLSID_SoftwareBitmapNativeFactory),
|
||||
&ISoftwareBitmapNativeFactory::CreateFromWICBitmap,
|
||||
iconBitmap.get(),
|
||||
false);
|
||||
|
||||
// Convert the pixel format and alpha mode if necessary
|
||||
if (softwareBitmap.BitmapPixelFormat() != pixelFormat || softwareBitmap.BitmapAlphaMode() != alphaMode)
|
||||
{
|
||||
softwareBitmap = SoftwareBitmap::Convert(softwareBitmap, pixelFormat, alphaMode);
|
||||
}
|
||||
|
||||
return softwareBitmap;
|
||||
}
|
||||
|
||||
static SoftwareBitmap _getBitmapFromIconFileAsync(const winrt::hstring& iconPath,
|
||||
int32_t iconIndex,
|
||||
uint32_t iconSize)
|
||||
{
|
||||
wil::unique_hicon hicon;
|
||||
LOG_IF_FAILED(SHDefExtractIcon(iconPath.c_str(), iconIndex, 0, &hicon, nullptr, iconSize));
|
||||
|
||||
if (!hicon)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
wil::com_ptr<IWICImagingFactory> wicImagingFactory;
|
||||
THROW_IF_FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&wicImagingFactory)));
|
||||
|
||||
return _convertToSoftwareBitmap(hicon.get(),
|
||||
BitmapPixelFormat::Bgra8,
|
||||
BitmapAlphaMode::Premultiplied,
|
||||
wicImagingFactory.get());
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Attempt to get the icon index from the icon path provided
|
||||
// Arguments:
|
||||
// - iconPath: the full icon path, including the index if present
|
||||
// - iconPathWithoutIndex: the place to store the icon path, sans the index if present
|
||||
// Return Value:
|
||||
// - nullopt if the iconPath is not an exe/dll/lnk file in the first place
|
||||
// - 0 if the iconPath is an exe/dll/lnk file but does not contain an index (i.e. we default
|
||||
// to the first icon in the file)
|
||||
// - the icon index if the iconPath is an exe/dll/lnk file and contains an index
|
||||
static std::optional<int> _getIconIndex(const winrt::hstring& iconPath, std::wstring_view& iconPathWithoutIndex)
|
||||
{
|
||||
const auto pathView = std::wstring_view{ iconPath };
|
||||
// Does iconPath have a comma in it? If so, split the string on the
|
||||
// comma and look for the index and extension.
|
||||
const auto commaIndex = pathView.find(L',');
|
||||
|
||||
// split the path on the comma
|
||||
iconPathWithoutIndex = pathView.substr(0, commaIndex);
|
||||
|
||||
// It's an exe, dll, or lnk, so we need to extract the icon from the file.
|
||||
if (!til::ends_with(iconPathWithoutIndex, L".exe") &&
|
||||
!til::ends_with(iconPathWithoutIndex, L".dll") &&
|
||||
!til::ends_with(iconPathWithoutIndex, L".lnk"))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (commaIndex != std::wstring::npos)
|
||||
{
|
||||
// Convert the string iconIndex to a signed int to support negative numbers which represent an Icon's ID.
|
||||
const auto index{ til::to_int(pathView.substr(commaIndex + 1)) };
|
||||
if (index == til::to_int_error)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<int>(index);
|
||||
}
|
||||
|
||||
// We had a binary path, but no index. Default to 0.
|
||||
return 0;
|
||||
}
|
||||
|
||||
static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex,
|
||||
int index,
|
||||
int targetSize)
|
||||
{
|
||||
// Try:
|
||||
// * c:\Windows\System32\SHELL32.dll, 210
|
||||
// * c:\Windows\System32\notepad.exe, 0
|
||||
// * C:\Program Files\PowerShell\6-preview\pwsh.exe, 0 (this doesn't exist for me)
|
||||
// * C:\Program Files\PowerShell\7\pwsh.exe, 0
|
||||
|
||||
const auto swBitmap{ _getBitmapFromIconFileAsync(winrt::hstring{ iconPathWithoutIndex }, index, targetSize >= 0 ? targetSize : 256) };
|
||||
if (swBitmap == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource bitmapSource{};
|
||||
bitmapSource.SetBitmapAsync(swBitmap);
|
||||
return bitmapSource;
|
||||
}
|
||||
|
||||
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
|
||||
const winrt::hstring& fontFamily,
|
||||
const int targetSize)
|
||||
{
|
||||
std::wstring_view iconPathWithoutIndex;
|
||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||
if (!indexOpt.has_value())
|
||||
{
|
||||
return _IconSourceMUX(iconPath, fontFamily, targetSize);
|
||||
}
|
||||
|
||||
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize);
|
||||
|
||||
MUX::Controls::ImageIconSource imageIconSource{};
|
||||
imageIconSource.ImageSource(bitmapSource);
|
||||
|
||||
return imageIconSource;
|
||||
}
|
||||
|
||||
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath)
|
||||
{
|
||||
return IconMUX(iconPath, 24);
|
||||
}
|
||||
|
||||
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath, const int targetSize)
|
||||
{
|
||||
std::wstring_view iconPathWithoutIndex;
|
||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||
if (!indexOpt.has_value())
|
||||
{
|
||||
auto source = IconSourceMUX(iconPath, L"", targetSize);
|
||||
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
|
||||
icon.IconSource(source);
|
||||
return icon;
|
||||
}
|
||||
|
||||
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize);
|
||||
|
||||
winrt::Microsoft::UI::Xaml::Controls::ImageIcon icon{};
|
||||
icon.Source(bitmapSource);
|
||||
icon.Width(targetSize);
|
||||
icon.Height(targetSize);
|
||||
return icon;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "IconPathConverter.g.h"
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
struct IconPathConverter
|
||||
{
|
||||
IconPathConverter() = default;
|
||||
|
||||
//static Windows::UI::Xaml::Controls::IconElement IconWUX(const winrt::hstring& iconPath);
|
||||
//static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(const winrt::hstring& iconPath);
|
||||
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, const winrt::hstring& fontFamily, const int targetSize=24);
|
||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath);
|
||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::factory_implementation
|
||||
{
|
||||
BASIC_FACTORY(IconPathConverter);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft.Terminal.UI
|
||||
{
|
||||
static runtimeclass IconPathConverter
|
||||
{
|
||||
// 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, String fontFamily, Int32 targetSize);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -154,18 +154,12 @@
|
||||
<ClInclude Include="Converters.h">
|
||||
<DependentUpon>Converters.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="IconPathConverter.h">
|
||||
<DependentUpon>IconPathConverter.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="RunHistory.h">
|
||||
<DependentUpon>RunHistory.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ResourceString.h">
|
||||
<DependentUpon>ResourceString.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FontIconGlyphClassifier.h">
|
||||
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="init.cpp" />
|
||||
@@ -175,9 +169,6 @@
|
||||
<ClCompile Include="Converters.cpp">
|
||||
<DependentUpon>Converters.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="IconPathConverter.cpp">
|
||||
<DependentUpon>IconPathConverter.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="RunHistory.cpp">
|
||||
<DependentUpon>RunHistory.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
@@ -185,17 +176,12 @@
|
||||
<DependentUpon>ResourceString.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
|
||||
<ClCompile Include="FontIconGlyphClassifier.cpp">
|
||||
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="Converters.idl" />
|
||||
<Midl Include="IconPathConverter.idl" />
|
||||
<Midl Include="RunHistory.idl" />
|
||||
<Midl Include="IDirectKeyListener.idl" />
|
||||
<Midl Include="ResourceString.idl" />
|
||||
<Midl Include="FontIconGlyphClassifier.idl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Microsoft.Terminal.UI.def" />
|
||||
@@ -205,4 +191,4 @@
|
||||
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class FontIconGlyphClassifierTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void EmptyInputHasNoGlyph()
|
||||
{
|
||||
Assert.AreEqual(FontIconGlyphKind.None, FontIconGlyphClassifier.Classify(null));
|
||||
Assert.AreEqual(FontIconGlyphKind.None, FontIconGlyphClassifier.Classify(string.Empty));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("\uE700")]
|
||||
[DataRow("\uF000")]
|
||||
[DataRow("\uF8FF")]
|
||||
public void FluentPrivateUseCharactersAreSymbols(string text)
|
||||
{
|
||||
Assert.AreEqual(FontIconGlyphKind.FluentSymbol, FontIconGlyphClassifier.Classify(text));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("A")]
|
||||
[DataRow("\uE6FF")]
|
||||
[DataRow("\uF900")]
|
||||
[DataRow("e\u0301")]
|
||||
public void SingleNonEmojiGraphemesUseTheGeneralFont(string text)
|
||||
{
|
||||
Assert.AreEqual(FontIconGlyphKind.Other, FontIconGlyphClassifier.Classify(text));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsolatedLowSurrogatePreservesNativeClassifierBehavior()
|
||||
{
|
||||
var text = new string((char)0xDC00, 1);
|
||||
|
||||
Assert.AreEqual(FontIconGlyphKind.Other, FontIconGlyphClassifier.Classify(text));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("\u231A")]
|
||||
[DataRow("\U0001F600")]
|
||||
[DataRow("\u2764\uFE0F")]
|
||||
[DataRow("2\uFE0F\u20E3")]
|
||||
[DataRow("\U0001F469\u200D\U0001F4BB")]
|
||||
public void EmojiGraphemesUseTheEmojiFont(string text)
|
||||
{
|
||||
Assert.AreEqual(FontIconGlyphKind.Emoji, FontIconGlyphClassifier.Classify(text));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("\u2764")]
|
||||
[DataRow("\u2764\uFE0E")]
|
||||
[DataRow("2\u20E3")]
|
||||
public void TextPresentationRemainsGeneralText(string text)
|
||||
{
|
||||
Assert.AreEqual(FontIconGlyphKind.Other, FontIconGlyphClassifier.Classify(text));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("\uD83D")]
|
||||
[DataRow("ab")]
|
||||
[DataRow("C:\\icon.png")]
|
||||
[DataRow("\U0001F600\U0001F600")]
|
||||
public void InvalidOrMultipleGraphemesAreRejected(string text)
|
||||
{
|
||||
Assert.AreEqual(FontIconGlyphKind.Invalid, FontIconGlyphClassifier.Classify(text));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FontFamilySelectionMatchesTheNativeConverter()
|
||||
{
|
||||
Assert.AreEqual(
|
||||
"Segoe Fluent Icons, Segoe MDL2 Assets",
|
||||
FontIconGlyphClassifier.GetFontFamily(FontIconGlyphKind.FluentSymbol, null));
|
||||
Assert.AreEqual(
|
||||
"Segoe UI Emoji, Segoe UI",
|
||||
FontIconGlyphClassifier.GetFontFamily(FontIconGlyphKind.Emoji, null));
|
||||
Assert.AreEqual(
|
||||
"Custom Font",
|
||||
FontIconGlyphClassifier.GetFontFamily(FontIconGlyphKind.Other, "Custom Font"));
|
||||
Assert.AreEqual(
|
||||
"Segoe UI",
|
||||
FontIconGlyphClassifier.GetFontFamily(FontIconGlyphKind.Invalid, "Custom Font"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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.VisualStudio.TestTools.UnitTesting;
|
||||
using Windows.Graphics.Imaging;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class IconPathConverterTests
|
||||
{
|
||||
[TestMethod]
|
||||
[Timeout(5_000)]
|
||||
public void IndexedShellIconIsPreparedAsSoftwareBitmap()
|
||||
{
|
||||
var shell32Path = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.System),
|
||||
"shell32.dll");
|
||||
|
||||
using var prepared = IconPathConverter.Prepare($"{shell32Path},0", null, 32);
|
||||
|
||||
Assert.AreEqual(IconPathConverter.PreparedIconKind.Binary, prepared.Kind);
|
||||
Assert.IsNotNull(prepared.SoftwareBitmap);
|
||||
var bitmap = prepared.SoftwareBitmap;
|
||||
Assert.IsTrue(bitmap.PixelWidth > 0);
|
||||
Assert.IsTrue(bitmap.PixelHeight > 0);
|
||||
Assert.AreEqual(BitmapPixelFormat.Bgra8, bitmap.BitmapPixelFormat);
|
||||
Assert.AreEqual(BitmapAlphaMode.Premultiplied, bitmap.BitmapAlphaMode);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PreparedBinaryIconTransfersSoftwareBitmapOwnership()
|
||||
{
|
||||
using var bitmap = new SoftwareBitmap(
|
||||
BitmapPixelFormat.Bgra8,
|
||||
1,
|
||||
1,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
using var prepared = IconPathConverter.PreparedIcon.FromBinary(bitmap);
|
||||
|
||||
var transferred = prepared.TakeSoftwareBitmap();
|
||||
prepared.Dispose();
|
||||
|
||||
Assert.IsNotNull(transferred);
|
||||
Assert.AreSame(bitmap, transferred);
|
||||
Assert.AreEqual(1, transferred.PixelWidth);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UriAndInvalidTextPreparationPreserveConverterFallbacks()
|
||||
{
|
||||
using var svg = IconPathConverter.Prepare("ms-appx:///Assets/icon.svg", null, 20);
|
||||
Assert.AreEqual(IconPathConverter.PreparedIconKind.SvgUri, svg.Kind);
|
||||
Assert.AreEqual(20, svg.TargetSize);
|
||||
|
||||
using var invalidText = IconPathConverter.Prepare("not a glyph", "Custom Font", 24);
|
||||
Assert.AreEqual(IconPathConverter.PreparedIconKind.Glyph, invalidText.Kind);
|
||||
Assert.AreEqual("\u25CC", invalidText.Glyph);
|
||||
Assert.AreEqual("Segoe UI", invalidText.FontFamily);
|
||||
|
||||
using var relativeText = IconPathConverter.Prepare("not-a-glyph", null, 24);
|
||||
Assert.AreEqual(IconPathConverter.PreparedIconKind.Glyph, relativeText.Kind);
|
||||
Assert.AreEqual("\u25CC", relativeText.Glyph);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class IconPathParserTests
|
||||
{
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\Windows\notepad.exe", @"C:\Windows\notepad.exe", 0)]
|
||||
[DataRow(@"C:\Windows\System32\shell32.dll,-210", @"C:\Windows\System32\shell32.dll", -210)]
|
||||
[DataRow(@"C:\shortcut.lnk,0", @"C:\shortcut.lnk", 0)]
|
||||
[DataRow(@"C:\icons.dll,010", @"C:\icons.dll", 8)]
|
||||
[DataRow(@"C:\icons.dll,0x10", @"C:\icons.dll", 16)]
|
||||
public void ParsesSupportedBinaryIconReferences(string input, string expectedPath, int expectedIndex)
|
||||
{
|
||||
Assert.IsTrue(IconPathParser.TryParseBinaryIconReference(input, out var result));
|
||||
Assert.AreEqual(expectedPath, result.Path);
|
||||
Assert.AreEqual(expectedIndex, result.Index);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\icon.png")]
|
||||
[DataRow(@"C:\APP.EXE,0")]
|
||||
[DataRow(@"C:\icons.dll,not-an-index")]
|
||||
[DataRow(@"C:\folder,with-comma\icons.dll,1")]
|
||||
public void RejectsInputsTheNativeConverterDidNotTreatAsBinaryIcons(string input)
|
||||
{
|
||||
Assert.IsFalse(IconPathParser.TryParseBinaryIconReference(input, out _));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -28,9 +29,12 @@
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Controls\IconRequestSite.cs" Link="Controls\IconRequestSite.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\AdaptiveCache`2.cs" Link="Helpers\AdaptiveCache`2.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\AdaptiveCacheRemovalReason.cs" Link="Helpers\AdaptiveCacheRemovalReason.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\BinaryIconReference.cs" Link="Helpers\Icons\BinaryIconReference.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\CachedIconSourceProvider.cs" Link="Helpers\Icons\CachedIconSourceProvider.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconDispatcherMaterializationKind.cs" Link="Helpers\Icons\IconDispatcherMaterializationKind.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconDispatcherUiSliceKind.cs" Link="Helpers\Icons\IconDispatcherUiSliceKind.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\FontIconGlyphClassifier.cs" Link="Helpers\Icons\FontIconGlyphClassifier.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\FontIconGlyphKind.cs" Link="Helpers\Icons\FontIconGlyphKind.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\FontIconSizeCalculator.cs" Link="Helpers\Icons\FontIconSizeCalculator.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconLoadDemand.cs" Link="Helpers\Icons\IconLoadDemand.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconLoadDemandStage.cs" Link="Helpers\Icons\IconLoadDemandStage.cs" />
|
||||
@@ -49,6 +53,8 @@
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconRequestOrigin.cs" Link="Helpers\Icons\IconRequestOrigin.cs" />
|
||||
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\Icons\IconRequestReason.cs" Link="Helpers\Icons\IconRequestReason.cs" />
|
||||
<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\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" />
|
||||
|
||||
Reference in New Issue
Block a user