mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 02:36:19 +02:00
## Summary of the Pull Request - Introduces `FontIconGlyphClassifier` for classifying emojis and symbols. - Correctly recognizes multi-codepoint glyphs (e.g., 🧙🏼♀️ *woman mage with medium-light skin tone*). - Explicitly disallows multi-glyph icons (they would overflow anyway). - Distinguishes between emojis and regular text characters (letters, numbers, symbols), since emojis are slightly larger and require different padding. - Recognizes Unicode [Variation Selectors](https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)) to enforce specific styles: VS15 (U+FE0E) for text style (monochrome) and VS16 (U+FE0F) for emoji style (color). This lets developers choose which variant to display. By default, characters with both representations render as text/monochrome (e.g., ▶ `\u25B6`): <img width="428" height="39" alt="image" src="https://github.com/user-attachments/assets/c5e6865f-61de-4f45-9f3a-4e15e5e5ceb8" /> - Invalid icons are displayed as a dashed circle so extension developers can spot issues, without being overly distracting if they slip into production. - Updates `IconPathConverter` to use the new classifier for improved icon handling. - Adds `SampleIconPage` to demonstrate various icon usages and classifications. - Adjusts icon alignment in `IconBox` so icons are centered. - Scales negative padding for emojis in `IconBox` with control size, fixing misalignment and clipping (noticeable in tags and the details pane hero image). - Applies negative padding to all font icons. This removes the need for classification in these cases and ensures symbols rendered below the baseline remain visible. Based on [microsoft/terminal#19143](https://github.com/microsoft/terminal/pull/19143): Co-authored-by: Dustin L. Howett <duhowett@microsoft.com> Pictures? Pictures! <img width="1912" height="2394" alt="image" src="https://github.com/user-attachments/assets/05a16309-b658-4f21-8f9d-9a3f20db6ad8" /> Keyboard and flag/country emojis may look a bit off, but that’s how they’re actually rendered: <img width="482" height="95" alt="image" src="https://github.com/user-attachments/assets/dc7d4d0d-3dc8-4df5-9b9f-9e977e7e989f" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: - #41489 - #41496 - [ ] **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
399 lines
16 KiB
C++
399 lines
16 KiB
C++
#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.
|
||
// Return Value:
|
||
// - An IconElement with its IconSource set, if possible.
|
||
template<typename TIconSource>
|
||
TIconSource _getColoredBitmapIcon(const winrt::hstring& path, bool monochrome)
|
||
{
|
||
// 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)
|
||
{
|
||
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 };
|
||
iconSource.ImageSource(source);
|
||
return iconSource;
|
||
}
|
||
else
|
||
{
|
||
typename BitmapIconSource<TIconSource>::type iconSource;
|
||
// Make sure to set this to false, so we keep the RGB data of the
|
||
// image. Otherwise, the icon will be white for all the
|
||
// non-transparent pixels in the image.
|
||
iconSource.ShowAsMonochrome(monochrome);
|
||
iconSource.UriSource(iconUri);
|
||
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, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
||
{
|
||
TIconSource iconSource{ nullptr };
|
||
|
||
if (iconPath.size() != 0)
|
||
{
|
||
const auto expandedIconPath{ _expandIconPath(iconPath) };
|
||
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, monochrome);
|
||
|
||
// 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);
|
||
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, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
||
{
|
||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, 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) };
|
||
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 bool monochrome,
|
||
const winrt::hstring& fontFamily,
|
||
const int targetSize)
|
||
{
|
||
std::wstring_view iconPathWithoutIndex;
|
||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||
if (!indexOpt.has_value())
|
||
{
|
||
return _IconSourceMUX(iconPath, monochrome, 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, false, 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;
|
||
}
|
||
|
||
}
|