Compare commits

...

10 Commits

Author SHA1 Message Date
Mike Griese
7b8fe03e48 I really really hate the way this clipboard code is structured 2025-09-22 10:13:45 -05:00
Mike Griese
b23d2ea796 sizing is better 2025-09-22 09:54:53 -05:00
Mike Griese
9b5987a30f Merge remote-tracking branch 'origin/main' into dev/migrie/f/emoji-picker-2 2025-09-22 09:23:23 -05:00
Mike Griese
3d6abb07b6 Revert "Is GridView just totally ass? cause it seems like ItemsView is better?"
This reverts commit 44fb9e18dd.
2025-09-17 11:11:09 -05:00
Mike Griese
44fb9e18dd Is GridView just totally ass? cause it seems like ItemsView is better? 2025-09-17 11:10:43 -05:00
Mike Griese
e124388ae2 Leonard is a witch 2025-09-17 11:10:27 -05:00
Mike Griese
910ac6b5de testing: the segoe icons extension 2025-09-17 11:10:08 -05:00
Mike Griese
b7cabea064 Try adding filters, for subsets
my god the emoji rendering is so slow
2025-09-17 06:28:20 -05:00
Mike Griese
4a924571de init once 2025-09-17 05:52:45 -05:00
Mike Griese
b8faacc679 wired: cmdpal emoji picker 2025-09-17 05:52:34 -05:00
11 changed files with 12221 additions and 6 deletions

View File

@@ -3,17 +3,35 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.Terminal.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Storage.Streams;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.DirectWrite;
using Windows.Win32.Graphics.Gdi;
namespace Microsoft.CmdPal.UI.Helpers;
public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
public sealed class IconCacheService
{
private readonly DispatcherQueue dispatcherQueue;
private IDWriteFontFace? fontFace;
private IDWriteRenderingParams? renderingParams;
private IDWriteGdiInterop? interop;
public IconCacheService(DispatcherQueue dispatcherQueue)
{
this.dispatcherQueue = dispatcherQueue;
this.InitDwrite();
}
public Task<IconSource?> GetIconSource(IconDataViewModel icon) =>
// todo: actually implement a cache of some sort
@@ -25,6 +43,15 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
{
if (!string.IsNullOrEmpty(icon.Icon))
{
if (FontIconGlyphClassifier.Classify(icon.Icon) == FontIconGlyphKind.Emoji)
{
// use leonard's magic
if (ImageSourceToIcon(MagicEmoji(icon.Icon)) is IconSource ico)
{
return ico;
}
}
var source = IconPathConverter.IconSourceMUX(icon.Icon, false, icon.FontFamily);
return source;
}
@@ -96,4 +123,137 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
return completionSource.Task;
}
/// <summary>
/// Initializes DirectWrite and related objects needed for rendering emoji.
/// </summary>
private void InitDwrite()
{
unsafe
{
var factory = Native.CreateDWriteCoreFactory();
factory.CreateRenderingParams(out renderingParams);
factory.GetGdiInterop(out interop);
interop.CreateBitmapRenderTarget(HDC.Null, 100, 100, out var renderTarget);
var renderTarget3 = (IDWriteBitmapRenderTarget3)renderTarget;
// Get the font face
{
factory.GetSystemFontCollection(out var fontCollection, false);
fontCollection.FindFamilyName("Segoe UI Emoji", out var index, out var exists);
fontCollection.GetFontFamily(index, out var fontFamily);
fontFamily.GetFirstMatchingFont(
DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL,
DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL,
out var font);
font.CreateFontFace(out fontFace);
}
}
}
/// <summary>
/// Renders an emoji glyph to an ImageSource. Unbelievably, it is faster to
/// MANUALLY render the emoji using DirectWrite than it is to ask the normal
/// WinUI font icon renderer to render it.
///
/// Big shoutout to @lhecker for writing this for us
/// </summary>
/// <param name="glyph">The emoji glyph to render.</param>
/// <returns>An ImageSource containing the rendered emoji, or null if
/// rendering failed.</returns>
private ImageSource? MagicEmoji(string glyph)
{
if (string.IsNullOrEmpty(glyph))
{
return null;
}
if (fontFace is null)
{
return null;
}
if (interop is null)
{
return null;
}
var size = 54;
unsafe
{
interop.CreateBitmapRenderTarget(HDC.Null, (uint)size, (uint)size, out var renderTarget);
var renderTarget3 = (IDWriteBitmapRenderTarget3)renderTarget;
var glyphIndices = new ushort[1];
List<uint> codepoints = [];
for (var i = 0; i < glyph.Length; i += char.IsSurrogatePair(glyph, i) ? 2 : 1)
{
var x = char.ConvertToUtf32(glyph, i);
codepoints.Add((uint)x);
}
fontFace.GetGlyphIndices(codepoints.ToArray(), 1, glyphIndices);
var glyphIndex = glyphIndices[0];
var advance = (float)0.0f;
var offset = new DWRITE_GLYPH_OFFSET { };
var run = new DWRITE_GLYPH_RUN
{
fontFace = fontFace,
fontEmSize = 48,
glyphCount = 1,
glyphIndices = &glyphIndex,
glyphAdvances = &advance,
glyphOffsets = &offset,
isSideways = false,
bidiLevel = 0,
};
var rect = new RECT { };
renderTarget3.DrawGlyphRunWithColorSupport(
-6,
(float)45.0f,
DWRITE_MEASURING_MODE.DWRITE_MEASURING_MODE_NATURAL,
in run,
renderingParams,
new COLORREF(0xffffffff),
0,
&rect);
renderTarget3.GetBitmapData(out var bitmapData);
var bitmap = new WriteableBitmap(size, size);
using (var stream = bitmap.PixelBuffer.AsStream())
{
var pixels = new Span<uint>(bitmapData.pixels, size * size);
var bytes = MemoryMarshal.AsBytes(pixels);
stream.Write(bytes.ToArray(), 0, bytes.Length);
}
return bitmap;
}
}
private static IconSource? ImageSourceToIcon(ImageSource? img)
{
return img is null ? null : new ImageIconSource() { ImageSource = img };
}
internal sealed partial class Native
{
[DllImport("DWriteCore.dll", ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
public static extern HRESULT DWriteCoreCreateFactory(DWRITE_FACTORY_TYPE factoryType, in Guid iid, out IntPtr factory);
public static unsafe IDWriteFactory8 CreateDWriteCoreFactory()
{
var iid = typeof(IDWriteFactory8).GUID;
var hr = DWriteCoreCreateFactory(DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED, in iid, out var factory);
#pragma warning disable CA2201 // Do not raise reserved exception types
return hr.Failed
? throw new COMException("DWriteCoreCreateFactory failed", hr)
: (IDWriteFactory8)Marshal.GetObjectForIUnknown(factory);
#pragma warning restore CA2201 // Do not raise reserved exception types
}
}
}

View File

@@ -53,4 +53,9 @@ GetCurrentThreadId
SetWindowsHookEx
UnhookWindowsHookEx
CallNextHookEx
GetModuleHandle
GetModuleHandle
DWRITE_FACTORY_TYPE
IDWriteFactory8
IDWriteBitmapRenderTarget3

View File

@@ -225,6 +225,41 @@ namespace winrt::Microsoft::Terminal::UI::implementation
return iconSource;
}
// static Microsoft::UI::Xaml::Controls::IconSource _leonardsMagicEmojiRenderer(const winrt::hstring& glyph)
// {
// // magic static init dwrite factory
// const static auto dwriteFactory = []() {
// wil::com_ptr<IDWriteFactory> factory;
// THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast<IUnknown**>(factory.put())));
// return factory;
// }();
// // magic static font face
// const static auto emojiFontFace = []() {
// wil::com_ptr<IDWriteFontCollection> fontCollection;
// THROW_IF_FAILED(dwriteFactory->GetSystemFontCollection(fontCollection.put()));
// UINT32 index;
// BOOL exists;
// THROW_IF_FAILED(fontCollection->FindFamilyName(L"Segoe UI Emoji", &index, &exists));
// if (!exists)
// {
// throw winrt::hresult_error(E_FAIL, L"Segoe UI Emoji font not found");
// }
// wil::com_ptr<IDWriteFontFamily> fontFamily;
// THROW_IF_FAILED(fontCollection->GetFontFamily(index, fontFamily.put()));
// wil::com_ptr<IDWriteFont> font;
// THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.put()));
// wil::com_ptr<IDWriteFontFace> fontFace;
// THROW_IF_FAILED(font->CreateFontFace(fontFace.put()));
// return fontFace;
// }();
// }
// Windows::UI::Xaml::Controls::IconSource IconPathConverter::IconSourceWUX(const hstring& path)
// {
// // * If the icon is a path to an image, we'll use that.

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,8 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
public partial class ClipboardHistoryCommandsProvider : CommandProvider
{
private readonly ListItem _clipboardHistoryListItem;
private readonly ListItem _emojiListItem;
private readonly ListItem _segoeListItem;
private readonly SettingsManager _settingsManager = new();
public ClipboardHistoryCommandsProvider()
@@ -25,6 +27,16 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
new CommandContextItem(_settingsManager.Settings.SettingsPage),
],
};
_emojiListItem = new ListItem(new EmojiPage())
{
Title = "Emoji Picker",
Subtitle = "Browse and copy emojis",
};
_segoeListItem = new ListItem(new SegoeIconsExtensionPage())
{
Title = "Segoe Icons",
Subtitle = "Browse and copy Segoe Fluent Icons",
};
DisplayName = Properties.Resources.provider_display_name;
Icon = Icons.ClipboardListIcon;
@@ -35,6 +47,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
public override IListItem[] TopLevelCommands()
{
return [_clipboardHistoryListItem];
return [_clipboardHistoryListItem, _emojiListItem, _segoeListItem];
}
}

View File

@@ -15,9 +15,9 @@ internal sealed partial class PasteCommand : InvokableCommand
{
private readonly ClipboardItem _clipboardItem;
private readonly ClipboardFormat _clipboardFormat;
private readonly ISettingOptions _settings;
private readonly ISettingOptions? _settings;
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat, ISettingOptions settings)
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat, ISettingOptions? settings)
{
_clipboardItem = clipboardItem;
_clipboardFormat = clipboardFormat;
@@ -42,7 +42,9 @@ internal sealed partial class PasteCommand : InvokableCommand
ClipboardHelper.SendPasteKeyCombination();
if (!_settings.KeepAfterPaste)
// If there were settings passed in, AND the setting was set to not keep
// after paste, remove the item from history.
if (_settings is not null && !_settings.KeepAfterPaste)
{
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
}

View File

@@ -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.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace", Justification = "OSS", Scope = "namespace", Target = "~N:J3QQ4")]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
// 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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
internal sealed partial class EmojiListItem : ListItem
{
private readonly string _emoji;
private readonly IconInfo _icon;
public override IconInfo Icon => _icon;
public EmojiListItem(string emoji)
: base()
{
_emoji = emoji;
_icon = new IconInfo(emoji);
Title = emoji;
DataPackage textDataPackage = new()
{
RequestedOperation = DataPackageOperation.Copy,
};
textDataPackage.SetText(emoji);
ClipboardItem content = new()
{
Item = textDataPackage,
};
var copyCommand = new CopyTextCommand(emoji) { Icon = _icon };
var pasteCommand = new PasteCommand(content, ClipboardFormat.Text, null)
{
Icon = _icon,
Name = Properties.Resources.paste_command_name,
};
Command = pasteCommand;
MoreCommands = [ new CommandContextItem(copyCommand) ];
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
#pragma warning disable SA1402 // File may only contain a single type
public sealed partial class EmojiPage : ListPage
{
private readonly Dictionary<string, List<ListItem>> _emojiListItems;
public EmojiPage()
{
Icon = new IconInfo("\uE899");
Name = "Emoji"; // Properties.Resources.emoji_page_name;
Id = "com.microsoft.cmdpal.emoji";
ShowDetails = false;
GridProperties = new SmallGridLayout();
_emojiListItems = new Dictionary<string, List<ListItem>>();
foreach (var group in EmojiDict.Data)
{
var listItems = group.Value.Select(s => new EmojiListItem(s.Emoji) { Title = s.Name }).Cast<ListItem>().ToList();
_emojiListItems.Add(group.Key, listItems);
}
var filters = new EmojiFilters();
filters.PropChanged += Filters_PropChanged;
Filters = filters;
}
public override IListItem[] GetItems()
{
if (Filters is null)
{
return [];
}
if (Filters.CurrentFilterId == "all")
{
return _emojiListItems.Values.SelectMany(x => x).ToArray();
}
if (_emojiListItems.TryGetValue(Filters.CurrentFilterId, out var items))
{
return items.ToArray();
}
return [];
}
private void Filters_PropChanged(object sender, IPropChangedEventArgs args) => RaiseItemsChanged();
}
public partial class EmojiFilters : Filters
{
private List<IFilterItem> _allFilters = new()
{
new Filter() { Id = "all", Name = "All Emoji" },
new Separator(),
};
public EmojiFilters()
{
CurrentFilterId = EmojiDict.Data.Keys.First();
foreach (var group in EmojiDict.Data)
{
_allFilters.Add(new Filter() { Id = group.Key, Name = group.Key });
}
}
public override IFilterItem[] GetFilters()
{
return _allFilters.ToArray();
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,175 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
#pragma warning disable SA1402 // File may only contain a single type
internal sealed partial class SegoeIconsExtensionPage : ListPage
{
private readonly Lock _lock = new();
private IListItem[]? _items;
public SegoeIconsExtensionPage()
{
Icon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), "Assets/WinUI3Gallery.png"));
Name = "Segoe Icons";
IsLoading = true;
GridProperties = new SmallGridLayout();
PreloadIcons();
}
public void PreloadIcons()
{
_ = Task.Run(() =>
{
lock (_lock)
{
var t = GenerateIconItems();
t.ConfigureAwait(false);
_items = t.Result;
}
});
}
public override IListItem[] GetItems()
{
lock (_lock)
{
IsLoading = false;
return _items ?? [];
}
}
private async Task<IListItem[]> GenerateIconItems()
{
var timer = new Stopwatch();
timer.Start();
var rawIcons = await IconsDataSource.Instance.LoadIcons()!;
var items = rawIcons.Select(ToItem).ToArray();
IsLoading = false;
timer.Stop();
ExtensionHost.LogMessage($"Generating icon items took {timer.ElapsedMilliseconds}ms");
return items;
}
private IconListItem ToItem(IconData d) => new(d);
}
internal sealed partial class IconListItem : ListItem
{
private readonly IconData _data;
public IconListItem(IconData data)
: base(new CopyTextCommand(data.CodeGlyph) { Name = $"Copy {data.CodeGlyph}" })
{
_data = data;
this.Title = _data.Name;
this.Icon = new IconInfo(data.Character);
this.Subtitle = _data.CodeGlyph;
if (data.Tags != null && data.Tags.Length > 0)
{
this.Tags = data.Tags.Select(t => new Tag() { Text = t }).ToArray();
}
this.MoreCommands =
[
new CommandContextItem(new CopyTextCommand(data.Character)) { Title = $"Copy {data.Character}", Icon = new IconInfo(data.Character) },
new CommandContextItem(new CopyTextCommand(data.TextGlyph)) { Title = $"Copy {data.TextGlyph}" },
new CommandContextItem(new CopyTextCommand(data.Name)) { Title = $"Copy {data.Name}" },
];
}
}
// very shamelessly from
// https://github.com/microsoft/WinUI-Gallery/blob/main/WinUIGallery/DataModel/IconsDataSource.cs
public class IconData
{
public required string Name { get; set; }
public required string Code { get; set; }
public string[] Tags { get; set; } = [];
public string Character => char.ConvertFromUtf32(Convert.ToInt32(Code, 16));
public string CodeGlyph => "\\u" + Code;
public string TextGlyph => "&#x" + Code + ";";
}
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(List<IconData>), TypeInfoPropertyName = "IconList")]
internal sealed partial class IconDataListContext : JsonSerializerContext
{
}
internal sealed class IconsDataSource
{
public static IconsDataSource Instance { get; } = new();
public static List<IconData> Icons => Instance.icons;
private List<IconData> icons = [];
private IconsDataSource()
{
}
private readonly object _lock = new();
public async Task<List<IconData>> LoadIcons()
{
lock (_lock)
{
if (icons.Count != 0)
{
return icons;
}
}
Stopwatch stopwatch = new();
stopwatch.Start();
var jsonText = await LoadText("Microsoft.CmdPal.Ext.ClipboardHistory/Assets/icons.json");
lock (_lock)
{
if (icons.Count == 0 &&
!string.IsNullOrEmpty(jsonText))
{
icons = JsonSerializer.Deserialize<List<IconData>>(jsonText, IconDataListContext.Default.IconList) is List<IconData> i ? i
// icons = JsonSerializer.Deserialize<List<IconData>>(jsonText) is List<IconData> i ? i
: throw new InvalidDataException($"Cannot load icon data: {jsonText}");
}
stopwatch.Stop();
ExtensionHost.LogMessage($"Reading file and parsing JSON took {stopwatch.ElapsedMilliseconds}ms");
return icons;
}
}
public static async Task<string> LoadText(string relativeFilePath)
{
// if the file exists, load it and append the new item
var sourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), relativeFilePath);
return File.Exists(sourcePath) ? await File.ReadAllTextAsync(sourcePath) : string.Empty;
}
}