diff --git a/assets/logo/logo-figma.fig b/assets/logo/logo-figma.fig index 75fbe112d3..73240d4a8b 100644 Binary files a/assets/logo/logo-figma.fig and b/assets/logo/logo-figma.fig differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-100.png b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-100.png new file mode 100644 index 0000000000..a9a9d76456 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-100.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-125.png b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-125.png new file mode 100644 index 0000000000..32ebac66f3 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-125.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-150.png b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-150.png new file mode 100644 index 0000000000..45f611b452 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-150.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-200.png b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-200.png new file mode 100644 index 0000000000..8b4b223cfc Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-200.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-400.png b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-400.png new file mode 100644 index 0000000000..905cad45ba Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/SmallTile.scale-400.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-100.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-100.png new file mode 100644 index 0000000000..feef346779 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-100.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-125.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-125.png new file mode 100644 index 0000000000..b1b9f17ba7 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-125.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-150.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-150.png new file mode 100644 index 0000000000..76a7b94909 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-150.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-200.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..a908fb03dd Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-200.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-400.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-400.png new file mode 100644 index 0000000000..f724dbac8e Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square150x150Logo.scale-400.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-100.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-100.png new file mode 100644 index 0000000000..7a4424ae4e Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-100.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-125.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-125.png new file mode 100644 index 0000000000..5ad7480289 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-125.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-150.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-150.png new file mode 100644 index 0000000000..225c78d78f Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-150.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-200.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..ea23fca8da Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-200.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-400.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-400.png new file mode 100644 index 0000000000..f44fb293c9 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.scale-400.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-16.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000000..671185fce3 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-16.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-24.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000000..3952f52097 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-24.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-256.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000000..d4ee6caa42 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-256.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-32.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000000..8d5e2f6b84 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-32.png differ diff --git a/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-48.png b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000000..073c753e19 Binary files /dev/null and b/src/dev/impl/DevToys/Assets/TileTemplate/Square44x44Logo.targetsize-48.png differ diff --git a/src/dev/impl/DevToys/Core/UriActivationProtocolService.cs b/src/dev/impl/DevToys/Core/UriActivationProtocolService.cs index 9b19de5ac8..13a0c521be 100644 --- a/src/dev/impl/DevToys/Core/UriActivationProtocolService.cs +++ b/src/dev/impl/DevToys/Core/UriActivationProtocolService.cs @@ -4,14 +4,28 @@ using System.Collections.Generic; using System.Composition; using System.Globalization; +using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using DevToys.Api.Core; using DevToys.Api.Tools; using DevToys.Core.Threading; +using DevToys.Models; using DevToys.Shared.Core; +using DevToys.Shared.Core.Threading; +using Microsoft.Toolkit.Uwp.Helpers; +using Microsoft.UI.Xaml.Controls; using Windows.ApplicationModel; +using Windows.Graphics.Display; +using Windows.Graphics.Imaging; +using Windows.Storage; +using Windows.Storage.Streams; using Windows.System; +using Windows.UI; using Windows.UI.StartScreen; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Imaging; namespace DevToys.Core { @@ -19,6 +33,26 @@ namespace DevToys.Core [Shared] internal sealed class UriActivationProtocolService : IUriActivationProtocolService { + private readonly TileIconSizeDefinition[] ToolTileIconSizeDefinitions + = new[] + { + new TileIconSizeDefinition("SmallTile.scale-100", size: 71, toolIconRatio: 3.5), + new TileIconSizeDefinition("SmallTile.scale-125", size: 89, toolIconRatio: 3.5), + new TileIconSizeDefinition("SmallTile.scale-150", size: 107, toolIconRatio: 3.5), + new TileIconSizeDefinition("SmallTile.scale-200", size: 142, toolIconRatio: 3.5), + new TileIconSizeDefinition("SmallTile.scale-400", size: 284, toolIconRatio: 3.5), + new TileIconSizeDefinition("Square44x44Logo.scale-100", size: 44, toolIconRatio: 2.4), + new TileIconSizeDefinition("Square44x44Logo.scale-125", size: 55, toolIconRatio: 2.5), + new TileIconSizeDefinition("Square44x44Logo.scale-150", size: 66, toolIconRatio: 2.5), + new TileIconSizeDefinition("Square44x44Logo.scale-200", size: 88, toolIconRatio: 2.5), + new TileIconSizeDefinition("Square44x44Logo.scale-400", size: 176, toolIconRatio: 2.6), + new TileIconSizeDefinition("Square150x150Logo.scale-100", size: 150, toolIconRatio: 6), + new TileIconSizeDefinition("Square150x150Logo.scale-125", size: 188, toolIconRatio: 6), + new TileIconSizeDefinition("Square150x150Logo.scale-150", size: 225, toolIconRatio: 6), + new TileIconSizeDefinition("Square150x150Logo.scale-200", size: 300, toolIconRatio: 6), + new TileIconSizeDefinition("Square150x150Logo.scale-400", size: 600, toolIconRatio: 6), + }; + public async Task LaunchNewAppInstance(string? arguments = null) { return await ThreadHelper.RunOnUIThreadAsync(async () => @@ -51,15 +85,35 @@ public async Task PinToolToStart(MatchedToolProvider toolProvider) { try { - var tile - = new SecondaryTile( - tileId: toolProvider.Metadata.ProtocolName, - displayName: toolProvider.ToolProvider.SearchDisplayName, - arguments: GenerateLaunchArguments(toolProvider.Metadata.ProtocolName), - new Uri("ms-appx:///Assets/Logo/Square150x150Logo.png"), - TileSize.Default); - - return await tile.RequestCreateAsync(); + ThreadHelper.ThrowIfNotOnUIThread(); + + var tileIconGenerationTasks = new List(); + for (int i = 0; i < ToolTileIconSizeDefinitions.Length; i++) + { + TileIconSizeDefinition? iconDefinition = ToolTileIconSizeDefinitions[i]; + tileIconGenerationTasks.Add( + GenerateCustomTileIconAsync( + iconDefinition.Size, + iconDefinition.ToolIconRatio, + iconDefinition.IconName, + toolProvider)); + } + + await Task.WhenAll(tileIconGenerationTasks).ConfigureAwait(true); + + var tile = new SecondaryTile( + tileId: toolProvider.Metadata.ProtocolName) + { + DisplayName = toolProvider.ToolProvider.SearchDisplayName, + Arguments = GenerateLaunchArguments(toolProvider.Metadata.ProtocolName), + RoamingEnabled = false + }; + tile.VisualElements.ShowNameOnSquare150x150Logo = true; + tile.VisualElements.Square150x150Logo = new Uri($"ms-appdata:///local/{toolProvider.Metadata.ProtocolName}/Square150x150Logo.scale-100.png"); + tile.VisualElements.Square44x44Logo = new Uri($"ms-appdata:///local/{toolProvider.Metadata.ProtocolName}/Square44x44Logo.scale-100.png"); + tile.VisualElements.Square71x71Logo = new Uri($"ms-appdata:///local/{toolProvider.Metadata.ProtocolName}/SmallTile.scale-100.png"); + + return await tile.RequestCreateForSelectionAsync(Window.Current.Bounds); } catch (Exception ex) { @@ -93,5 +147,136 @@ private string GenerateLaunchUri(string? toolProtocol) return uriToLaunch; } + + private async Task GenerateCustomTileIconAsync(int targetSize, double toolIconRatio, string inputFileName, MatchedToolProvider toolProvider) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + /* + * The code below generates the following equivalent: + * + * + * + * + * + * + * + * + */ + + StorageFolder installationFolder = Package.Current.InstalledLocation; + var backgroundIconImageFile = (StorageFile)await installationFolder.TryGetItemAsync($"Assets\\TileTemplate\\{inputFileName}.png"); + + using (IRandomAccessStream fileStream = await backgroundIconImageFile.OpenAsync(FileAccessMode.Read, StorageOpenOptions.AllowOnlyReaders)) + { + var backgroundIconImageSource = new BitmapImage + { + DecodePixelWidth = targetSize, + DecodePixelHeight = targetSize + }; + await backgroundIconImageSource.SetSourceAsync(fileStream); + + var container = new Grid() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + Height = targetSize, + Width = targetSize, + MaxHeight = targetSize, + MaxWidth = targetSize, + Background = new SolidColorBrush(Colors.Transparent), + Margin = new Thickness(-1 * targetSize, -1 * targetSize, 0, 0), + RequestedTheme = ElementTheme.Dark + }; + + var backgroundIcon = new Image + { + Height = targetSize, + Width = targetSize, + Source = backgroundIconImageSource, + Stretch = Stretch.UniformToFill + }; + + IconElement toolIcon = await toolProvider.Icon.Task!.ConfigureAwait(true); + Assumes.NotNull(toolIcon, nameof(toolIcon)); + + toolIcon.Height = targetSize / toolIconRatio; + toolIcon.Width = targetSize / toolIconRatio; + + var toolIconViewBox = new Viewbox + { + Height = targetSize / toolIconRatio, + Width = targetSize / toolIconRatio, + Margin = new Thickness(0, 3, 0, 0), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Child = toolIcon + }; + + container.Children.Add(backgroundIcon); + container.Children.Add(toolIconViewBox); + + ((Grid)((Page)((Frame)Window.Current.Content).Content).Content).Children.Insert(0, container); + + container.UpdateLayout(); + + var iconSizeUpdatedTask = new TaskCompletionSource(); + long registrationToken = 0; + + var imageIcon = toolIcon as ImageIcon; + if (imageIcon is not null) + { + registrationToken = imageIcon.RegisterPropertyChangedCallback(ImageIcon.SourceProperty, (s, e) => + { + iconSizeUpdatedTask.TrySetResult(null); + }); + } + else + { + iconSizeUpdatedTask.TrySetResult(null); + } + + // Wait that the icon updates its size and maybe its color theme. + await Task.WhenAny(iconSizeUpdatedTask.Task, Task.Delay(500)).ConfigureAwait(true); + + if (imageIcon is not null) + { + imageIcon.UnregisterPropertyChangedCallback(ImageIcon.SourceProperty, registrationToken); + } + + ThreadHelper.ThrowIfNotOnUIThread(); + + // Create an image from the canvas. + var resultBitmap = new RenderTargetBitmap(); + await resultBitmap.RenderAsync(container); + + ((Grid)((Page)((Frame)Window.Current.Content).Content).Content).Children.Remove(container); + + ThreadHelper.ThrowIfNotOnUIThread(); + + // Save the image on the hard drive. + IBuffer pixelBuffer = await resultBitmap.GetPixelsAsync(); + byte[] pixels = pixelBuffer.ToArray(); + var displayInformation = DisplayInformation.GetForCurrentView(); + StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync($"{toolProvider.Metadata.ProtocolName}\\{inputFileName}.png", CreationCollisionOption.ReplaceExisting); + + ThreadHelper.ThrowIfNotOnUIThread(); + + using (IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.ReadWrite)) + { + BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream); + encoder.SetPixelData( + BitmapPixelFormat.Bgra8, + BitmapAlphaMode.Straight, + (uint)resultBitmap.PixelWidth, + (uint)resultBitmap.PixelHeight, + displayInformation.RawDpiX, + displayInformation.RawDpiY, + pixels); + + await encoder.FlushAsync(); + } + } + } } } diff --git a/src/dev/impl/DevToys/DevToys.csproj b/src/dev/impl/DevToys/DevToys.csproj index 10cbab4e52..373fc44dae 100644 --- a/src/dev/impl/DevToys/DevToys.csproj +++ b/src/dev/impl/DevToys/DevToys.csproj @@ -35,6 +35,7 @@ + ToolProvidersGridView.xaml @@ -237,6 +238,26 @@ PreserveNewest + + + + + + + + + + + + + + + + + + + + diff --git a/src/dev/impl/DevToys/Models/TileIconSizeDefinition.cs b/src/dev/impl/DevToys/Models/TileIconSizeDefinition.cs new file mode 100644 index 0000000000..4775cccb04 --- /dev/null +++ b/src/dev/impl/DevToys/Models/TileIconSizeDefinition.cs @@ -0,0 +1,22 @@ +#nullable enable + +using DevToys.Shared.Core; + +namespace DevToys.Models +{ + internal sealed class TileIconSizeDefinition + { + public TileIconSizeDefinition(string iconName, int size, double toolIconRatio) + { + IconName = Arguments.NotNullOrWhiteSpace(iconName, nameof(iconName)); + Size = size; + ToolIconRatio = toolIconRatio; + } + + internal int Size { get; } + + internal double ToolIconRatio { get; } + + internal string IconName { get; } + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/ToolProviderBase.cs b/src/dev/impl/DevToys/ViewModels/Tools/ToolProviderBase.cs index 913987f233..dcf44331ab 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/ToolProviderBase.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/ToolProviderBase.cs @@ -84,14 +84,7 @@ var result IconFileNameToSvgMap[iconFileName] = svgFileContent; } - if (actualTheme == ElementTheme.Dark) - { - svgFileContent = svgFileContent.Replace("#FF000000", "#FFFFFFFF").Replace("#000000", "#FFFFFF"); - } - else - { - svgFileContent = svgFileContent.Replace("#ffffff", "#000000").Replace("#FFFFFFFF", "#000000"); - } + svgFileContent = ApplyThemeToSvgIcon(actualTheme, svgFileContent); return await ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, async () => { @@ -126,6 +119,8 @@ var result svgSource.RasterizePixelHeight = newSize.Height; svgSource.RasterizePixelWidth = newSize.Width; + svgFileContent = ApplyThemeToSvgIcon(imageIcon.ActualTheme, svgFileContent); + using (Stream stream = GenerateStreamFromString(svgFileContent)) { await svgSource.SetSourceAsync(stream.AsRandomAccessStream()); @@ -158,6 +153,18 @@ private Stream GenerateStreamFromString(string input) return stream; } + private string ApplyThemeToSvgIcon(ElementTheme theme, string svg) + { + if (theme == ElementTheme.Dark) + { + return svg.Replace("#FF000000", "#FFFFFFFF").Replace("#000000", "#FFFFFF"); + } + else + { + return svg.Replace("#ffffff", "#000000").Replace("#FFFFFFFF", "#000000"); + } + } + private FrameworkElement? FindParentWithSmallerSize(FrameworkElement origin, Vector2 currentSize) { if (VisualTreeHelper.GetParent(origin) is not FrameworkElement parent)