Files
PowerToys/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleIconPage.cs
Jiří Polášek 347c3f1efa CmdPal: Enhance font icon classification and visuals (#41573)
## 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
2025-09-03 13:17:52 -05:00

190 lines
9.6 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.Linq;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension.Pages;
internal sealed partial class SampleIconPage : ListPage
{
private readonly IListItem[] _items =
[
/*
* Quick intro to Unicode in source code:
* - Every character has a code point (e.g., U+0041 = 'A').
* - Code points up to U+FFFF use \u1234 (4 hex digits and lowercase u).
* - Code points above that (up to U+10FFFF) use \U12345678 (8 hex digits and capital letter U).
* - If your source file is UTF-8, you can type the character directly, but it may not display properly in editors,
* and it's harder to see the actual code point.
* - Some symbols (like many emojis) are built from multiple code points
* joined together (e.g., 👋🏻 = U+1F44B + U+1F3FB).
*
* Examples:
* 😍 = "😍" or "\U0001F60D"
* 👋🏻 = "👋🏻" or "\U0001F44B\U0001F3FB"
* 🧙‍♂️ = "🧙‍♂️" or "\U0001F9D9\u200D\u2642\U0000FE0F" (male mage)
* 🧙🏿‍♀️ = "🧙🏿‍♀️" or "\U0001F9D9\U0001F3FF\u200D\u2640\U0000FE0F" (dark-skinned woman mage)
*
*/
// Emoji Smiling Face with Heart-Eyes
// Unicode: \U0001F60D
BuildIconItem("😍", "Standard emoji icon", "Basic emoji character rendered as an icon"),
// Emoji Smiling Face with Heart-Eyes
// Unicode: \U0001F60D\U0001F643\U0001F622
BuildIconItem("😍🙃😢", "Multiple emojis", "Use of multiple emojis for icon is not allowed"),
// Emoji Smiling Face with Sunglasses
// Unicode: \U0001F60E
BuildIconItem("\U0001F60E", "Unicode escape sequence emoji", "Emoji defined using Unicode escape sequence notation"),
// Segoe Fluent Icons font icon
// Unicode: \uE8D4
BuildIconItem("\uE8D4", "Segoe Fluent icon demonstration", "Segoe Fluent/MDL2 icon from system font\nWorks as an icon but won't display properly in button text"),
// Extended pictographic symbol for keyboard
BuildIconItem("\u2328", "Extended pictographic symbol", "Pictographic symbol representing a keyboard"),
// Capital letter A
BuildIconItem("A", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
// Letter 1
// Unicode: \U00000031
BuildIconItem("1", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
// Emoji Keycap Digit Two ... 2
// Unicode: \U00000032\U000020E3
// This is a sequence of three code points: the digit '2' (U+0032), and a combining enclosing keycap (U+20E3). No variation selector is used here.
BuildIconItem("\U00000032\U000020E3", "Emoji without variation selector", "Emoji character doesn't have VS16 variation selector to render as text"),
// Emoji Keycap Digit Three ... 3
// Unicode: \U00000033\U0000FE0F\U000020E3
// This is a sequence of three code points: the digit '3' (U+0033), a variation selector (U+FE0F) to specify emoji presentation, and a combining enclosing keycap (U+20E3).
BuildIconItem("3⃣", "Emoji with variation selector", "Emoji character using a variation selector to specify emoji presentation"),
// Symbol #
// Unicode: \u0023
BuildIconItem("#", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
// Symbol # keycap
// Unicode: \u0023\ufe0f\u20e3
// Sequence of 3 code points: symbol #, a variation selector (U+FE0F) to specify emoji presentation, and a combining enclosing keycap (U+20E3).
BuildIconItem("\u0023\ufe0f\u20e3", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
// Capital letter WM
// This is two characters, which is not a valid icon representation. It will be replaced by a placeholder signalizing an invalid icon.
BuildIconItem("WM", "Invalid icon representation", "String with multiple characters that does not correspond to a valid single icon"),
// Emoji Mage
// Unicode: \U0001F9D9
BuildIconItem("🧙", "Single code-point emoji example", "Simple emoji character using a single Unicode code point"),
// Emoji Male Mage (Mage with gender modifier)
// Unicode: \U0001F9D9\u200D\u2642\uFE0F
BuildIconItem("🧙‍♂️", "Complex emoji with gender modifier", "Composite emoji using Zero-Width Joiner (ZWJ) sequence for male variant"),
// Emoji Woman Mage (Mage with gender modifier)
// Unicode: \U0001F9D9\u200D\u2640\uFE0F
BuildIconItem("\U0001F9D9\u200D\u2640\uFE0F", "Complex emoji with gender modifier", "Composite emoji using Zero-Width Joiner (ZWJ) sequence for female variant"),
// Emoji Waving Hand
// Unicode: \U0001F44B
BuildIconItem("👋", "Basic hand gesture emoji", "Standard emoji character representing a waving hand"),
// Emoji Waving Hand + Light Skin Tone
// Unicode: \U0001F44B\U0001F3FB
BuildIconItem("👋🏻", "Emoji with light skin tone modifier", "Emoji enhanced with Unicode skin tone modifier (light)"),
// Emoji Waving Hand + Dark Skin Tone
// Unicode: \U0001F44B\U0001F3FF
BuildIconItem("\U0001F44B\U0001F3FF", "Emoji with dark skin tone modifier", "Emoji enhanced with Unicode skin tone modifier (dark)"),
// Flag of Czechia (Czech Republic)
// Unicode: \U0001F1E8\U0001F1FF
BuildIconItem("\U0001F1E8\U0001F1FF", "Flag emoji using regional indicators", "Emoji flag constructed from regional indicator symbols for Czechia"),
// Use of ZWJ without emojis
// KA (\u0995) + VIRAMA (\u09CD) + ZWJ (\u200D) - shows the half-form KA
// Unicode: \u0995\u09CD\u200D
BuildIconItem("\u0995\u09CD\u200D", "Use of ZWJ in non-emoji context", "Shows the half-form KA"),
// Use of ZWJ without emojis
// KA (\u0995) + VIRAMA (\u09CD) + Shows full KA with an explicit virama mark (not half-form).
// Unicode: \u0995\u09CD
BuildIconItem("\u0995\u09CD", "Use of ZWJ in non-emoji context", "Shows full KA with an explicit virama mark"),
// mahjong tile red dragon (using Unicode escape sequence)
// https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)
// Unicode: \U0001F004
BuildIconItem("\U0001F004", "Mahjong tile emoji (red dragon)", "Mahjong tile red dragon emoji character using Unicode escape sequence"),
// mahjong tile green dragon (non-emoji)
// https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)
// Unicode: \U0001F005
BuildIconItem("\U0001F005", "Mahjong tile non-emoji (green dragon)", "Mahjong tile character that is not classified as an emoji"),
// Play, PlayPause, Stop
BuildIconItem("\u25B6", "Play symbol (standalone)", "Play symbol"),
BuildIconItem("\u25B6\uFE0E", "Play symbol + VS15 (request text)", "Play symbol with variation specifier requesting rendering as text"),
BuildIconItem("\u25B6\uFE0F", "Play symbol + VS16 (request emoji)", "Play symbol with variation specifier requesting rendering as emoji "),
BuildIconItem("⏯️", "Play/Pause keycap emoji", "Play/Pause keycap emoji doesn't have plain text variant"),
BuildIconItem("⏸️", "Pause keycap emoji", "Pause keycap emoji doesn't have plain text variant"),
// Copyright and emoji copyright:
BuildIconItem("\u00a9", "Copyright symbol (standalone)", "Copyright symbol that is not classified as an emoji"),
BuildIconItem("\u00a9\uFE0E", "Copyright symbol + VS15 (request text)", "Copyright symbol that is not classified as an emoji"),
BuildIconItem("\u00a9\uFE0F", "Copyright symbol + VS16 (request emoji)", "Copyright symbol that is not classified as an emoji"),
// Tag flags
BuildIconItem("🏳️", "White Flag", "White Flag"),
BuildIconItem("\U0001F3F4\u200D\u2620\uFE0F", "Pirate Flag", "Pirate Flag"),
];
public SampleIconPage()
{
Icon = new IconInfo("\uE8BA");
Name = "Sample Icon Page";
ShowDetails = true;
}
public override IListItem[] GetItems() => _items;
private static ListItem BuildIconItem(string icon, string title, string description)
{
var iconInfo = new IconInfo(icon);
return new ListItem(new CopyTextCommand(icon) { Name = "Action with " + icon })
{
Title = title,
Subtitle = description,
Icon = iconInfo,
Tags = [
new Tag("Tag") { Icon = iconInfo },
],
Details = new Details
{
HeroImage = iconInfo,
Title = title,
Body = description,
Metadata = [
new DetailsElement
{
Key = "Unicode Code Points",
Data = new DetailsTags
{
Tags = icon.EnumerateRunes()
.Select(rune => rune.Value <= 0xFFFF ? $"\\u{rune.Value:X4}" : $"\\U{rune.Value:X8}")
.Select(t => new Tag(t))
.ToArray<ITag>(),
},
}
],
},
};
}
}