CmdPal: Plain text viewer and image viewer IContent (#43964)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR introduces new types of IContent:  
- Plain text content – simple and straightforward, with options to
switch between UI and monospace fonts and toggle word wrap.
- It's super safe to display any random text content without having to
worry about escaping the pesky markdown.
- Image viewer content – a more polished way to display images:  
- When placed in the ContentPage, the component automatically resizes to
fit the viewport, ensuring the entire image is visible at once.
- Images can be opened in a built-in mini-viewer that lets you view,
pan, and zoom without leaving the Command Palette. (Doing this directly
on the page proved to be a UX and development headache.) Fully
keyboard-controllable, so there’s no need to take your hands off the
keys.

## Pictures? Pictures!

Plain text content:

<img width="960" height="604" alt="image"
src="https://github.com/user-attachments/assets/a4ec36f3-2f7f-4a2a-a646-53056c512023"
/>

Image viewer content:

<img width="939" height="605" alt="image"
src="https://github.com/user-attachments/assets/c87f5726-8cd0-4015-b2d9-f1457fa1ec96"
/>



https://github.com/user-attachments/assets/915cd9d2-e4e3-4baf-8df6-6a328a3592ba


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41038
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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
This commit is contained in:
Jiří Polášek
2026-03-28 00:45:41 +01:00
committed by GitHub
parent 943c2a1ff5
commit 4cb3359314
37 changed files with 1752 additions and 76 deletions

View File

@@ -16,6 +16,8 @@ namespace Microsoft.CmdPal.UI.Helpers;
internal sealed partial class IconLoaderService : IIconLoaderService
{
public static readonly Size NoResize = Size.Empty;
private const DispatcherQueuePriority LoadingPriorityOnDispatcher = DispatcherQueuePriority.Low;
private const int DefaultIconSize = 256;
private const int MaxWorkerCount = 4;
@@ -214,11 +216,6 @@ internal sealed partial class IconLoaderService : IIconLoaderService
private static IconSource? GetStringIconSource(string iconString, string? fontFamily, Size size)
{
var iconSize = (int)Math.Max(size.Width, size.Height);
if (iconSize == 0)
{
iconSize = DefaultIconSize;
}
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
}
}

View File

@@ -11,7 +11,7 @@ namespace Microsoft.CmdPal.UI.Helpers;
/// <summary>
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
/// </summary>
public static partial class IconCacheProvider
public static partial class IconProvider
{
/*
Memory Usage Considerations (raw estimates):
@@ -29,6 +29,7 @@ public static partial class IconCacheProvider
private static IIconSourceProvider _provider32 = null!;
private static IIconSourceProvider _provider64 = null!;
private static IIconSourceProvider _provider256 = null!;
private static IIconSourceProvider _providerUnbound = null!;
public static void Initialize(IServiceProvider serviceProvider)
{
@@ -37,6 +38,7 @@ public static partial class IconCacheProvider
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
_provider256 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size256);
_providerUnbound = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Unbound);
}
private static async void SourceRequestedCore(IIconSourceProvider service, SourceRequestedEventArgs args)
@@ -80,5 +82,8 @@ public static partial class IconCacheProvider
public static void SourceRequested256(IconBox sender, SourceRequestedEventArgs args)
=> SourceRequestedCore(_provider256, args);
public static void SourceRequestedOriginal(IconBox sender, SourceRequestedEventArgs args)
=> SourceRequestedCore(_providerUnbound, args);
#pragma warning restore IDE0060 // Remove unused parameter
}

View File

@@ -36,6 +36,10 @@ internal static class IconServiceRegistration
WellKnownIconSize.Size256,
(_, _) => new CachedIconSourceProvider(loader, 256, 64));
services.AddKeyedSingleton<IIconSourceProvider>(
WellKnownIconSize.Unbound,
(_, _) => new IconSourceProvider(loader, IconLoaderService.NoResize, isPriority: true));
return services;
}
}

View File

@@ -12,15 +12,17 @@ internal sealed class IconSourceProvider : IIconSourceProvider
{
private readonly IconLoaderService _loader;
private readonly Size _iconSize;
private readonly bool _isPriority;
public IconSourceProvider(IconLoaderService loader, Size iconSize)
public IconSourceProvider(IconLoaderService loader, Size iconSize, bool isPriority = false)
{
_loader = loader;
_iconSize = iconSize;
_isPriority = isPriority;
}
public IconSourceProvider(IconLoaderService loader, int iconSize)
: this(loader, new Size(iconSize, iconSize))
public IconSourceProvider(IconLoaderService loader, int iconSize, bool isPriority = false)
: this(loader, new Size(iconSize, iconSize), isPriority)
{
}
@@ -34,7 +36,8 @@ internal sealed class IconSourceProvider : IIconSourceProvider
icon.Data?.Unsafe,
_iconSize,
scale,
tcs);
tcs,
_isPriority ? IconLoadPriority.High : IconLoadPriority.Low);
return tcs.Task;
}

View File

@@ -11,4 +11,5 @@ internal enum WellKnownIconSize
Size32 = 32,
Size64 = 64,
Size256 = 256,
Unbound = -1,
}