From 21b0f6be818dd1be794c61a029103f2bada19fec Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Mon, 17 Nov 2025 22:36:53 -0500 Subject: [PATCH 01/15] Try replacing WPF with SkiaSharp --- src/COM/DesktopWallpaper.cs | 11 +- src/SkiaSharp/BitmapCache.cs | 155 +++++++ src/SkiaSharp/RelayCommand.cs | 46 +++ src/SkiaSharp/ThemePreviewer.cs | 490 +++++++++++++++++++++++ src/SkiaSharp/ThemePreviewerViewModel.cs | 468 ++++++++++++++++++++++ src/ThemeDialog.Designer.cs | 4 +- src/ThemeDialog.cs | 11 +- src/WinDynamicDesktop.csproj | 8 +- 8 files changed, 1180 insertions(+), 13 deletions(-) create mode 100644 src/SkiaSharp/BitmapCache.cs create mode 100644 src/SkiaSharp/RelayCommand.cs create mode 100644 src/SkiaSharp/ThemePreviewer.cs create mode 100644 src/SkiaSharp/ThemePreviewerViewModel.cs diff --git a/src/COM/DesktopWallpaper.cs b/src/COM/DesktopWallpaper.cs index bdd042f1..4e1edb8a 100644 --- a/src/COM/DesktopWallpaper.cs +++ b/src/COM/DesktopWallpaper.cs @@ -16,6 +16,15 @@ public struct COLORREF public byte B; } + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + public enum DesktopSlideshowOptions { DSO_SHUFFLEIMAGES = 0x01, @@ -59,7 +68,7 @@ public interface IDesktopWallpaper uint GetMonitorDevicePathCount(); - System.Windows.Rect GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string monitorID); + RECT GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string monitorID); void SetBackgroundColor([MarshalAs(UnmanagedType.U4)] COLORREF color); diff --git a/src/SkiaSharp/BitmapCache.cs b/src/SkiaSharp/BitmapCache.cs new file mode 100644 index 00000000..580cc7ab --- /dev/null +++ b/src/SkiaSharp/BitmapCache.cs @@ -0,0 +1,155 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows.Forms; +using SkiaSharp; + +namespace WinDynamicDesktop.SkiaSharp +{ + sealed class BitmapCache + { + readonly int maxWidth; + readonly int maxHeight; + readonly object cacheLock = new object(); + readonly Dictionary images = new Dictionary(); + + public SKBitmap this[Uri uri] + { + get + { + lock (cacheLock) + { + if (images.ContainsKey(uri)) + { + return images[uri]; + } + else + { + var img = CreateImage(uri); + if (img != null) + { + images.Add(uri, img); + } + return img; + } + } + } + } + + public void Clear() + { + lock (cacheLock) + { + foreach (var bitmap in images.Values) + { + bitmap?.Dispose(); + } + images.Clear(); + } + GC.Collect(); + } + + public BitmapCache(bool limitDecodeSize = true) + { + if (limitDecodeSize) + { + int maxArea = 0; + foreach (Screen screen in Screen.AllScreens) + { + int area = screen.Bounds.Width * screen.Bounds.Height; + if (area > maxArea) + { + maxArea = area; + maxWidth = screen.Bounds.Width; + maxHeight = screen.Bounds.Height; + } + } + } + else + { + maxWidth = int.MaxValue; + maxHeight = int.MaxValue; + } + } + + private SKBitmap CreateImage(Uri uri) + { + try + { + Stream stream = null; + + if (uri.IsAbsoluteUri && uri.Scheme == "file") + { + string path = uri.LocalPath; + if (File.Exists(path)) + { + stream = File.OpenRead(path); + } + } + else if (!uri.IsAbsoluteUri) + { + // Embedded resource + stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(uri.OriginalString); + } + + if (stream == null) + { + return null; + } + + using (stream) + { + using (var codec = SKCodec.Create(stream)) + { + if (codec == null) + { + return null; + } + + var info = codec.Info; + + // Calculate scaled dimensions + int targetWidth = info.Width; + int targetHeight = info.Height; + + if (info.Width > maxWidth || info.Height > maxHeight) + { + float scale = Math.Min((float)maxWidth / info.Width, (float)maxHeight / info.Height); + targetWidth = (int)(info.Width * scale); + targetHeight = (int)(info.Height * scale); + } + + var bitmap = new SKBitmap(targetWidth, targetHeight, info.ColorType, info.AlphaType); + + if (targetWidth == info.Width && targetHeight == info.Height) + { + // No scaling needed + codec.GetPixels(bitmap.Info, bitmap.GetPixels()); + } + else + { + // Decode at full size then scale down with high quality + using (var fullBitmap = new SKBitmap(info)) + { + codec.GetPixels(fullBitmap.Info, fullBitmap.GetPixels()); + fullBitmap.ScalePixels(bitmap, SKFilterQuality.High); + } + } + + return bitmap; + } + } + } + catch + { + return null; + } + } + } +} diff --git a/src/SkiaSharp/RelayCommand.cs b/src/SkiaSharp/RelayCommand.cs new file mode 100644 index 00000000..ab7bf257 --- /dev/null +++ b/src/SkiaSharp/RelayCommand.cs @@ -0,0 +1,46 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace WinDynamicDesktop.SkiaSharp +{ + public class RelayCommand : ICommand + { + private readonly Action execute; + private readonly Func canExecute; + + public event EventHandler CanExecuteChanged; + + public RelayCommand(Action execute, Func canExecute = null) + { + this.execute = execute; + this.canExecute = canExecute; + } + + public bool CanExecute(object parameter) + { + return canExecute?.Invoke() ?? true; + } + + public void Execute(object parameter) + { + execute?.Invoke(); + } + + public void RaiseCanExecuteChanged() + { + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } + + public interface ICommand + { + event EventHandler CanExecuteChanged; + bool CanExecute(object parameter); + void Execute(object parameter); + } +} diff --git a/src/SkiaSharp/ThemePreviewer.cs b/src/SkiaSharp/ThemePreviewer.cs new file mode 100644 index 00000000..c9f5bf47 --- /dev/null +++ b/src/SkiaSharp/ThemePreviewer.cs @@ -0,0 +1,490 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using System.Drawing; +using System.IO; +using System.Reflection; +using System.Windows.Forms; +using SkiaSharp; +using SkiaSharp.Views.Desktop; + +namespace WinDynamicDesktop.SkiaSharp +{ + public class ThemePreviewer : SKControl + { + public ThemePreviewerViewModel ViewModel { get; } + + private readonly Timer animationTimer; + private readonly Timer fadeTimer; + private float fadeProgress = 0f; + private bool isAnimating = false; + private const int ANIMATION_DURATION_MS = 600; + private const int ANIMATION_FPS = 60; + private DateTime animationStartTime; + private static SKTypeface fontAwesome; + private Point mousePosition; + private bool isMouseOverPlay = false; + private bool isMouseOverLeft = false; + private bool isMouseOverRight = false; + private bool isMouseOverDownload = false; + + public ThemePreviewer() + { + ViewModel = new ThemePreviewerViewModel(StartAnimation, StopAnimation); + DoubleBuffered = true; + + // Load FontAwesome font once + if (fontAwesome == null) + { + using (Stream fontStream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("WinDynamicDesktop.resources.fonts.fontawesome-webfont.ttf")) + { + if (fontStream != null) + { + fontAwesome = SKTypeface.FromStream(fontStream); + } + } + } + + // Timer for smooth fade animations + fadeTimer = new Timer + { + Interval = 1000 / ANIMATION_FPS + }; + fadeTimer.Tick += FadeTimer_Tick; + + // Timer for auto-advance + animationTimer = new Timer + { + Interval = 1000 / 60 + }; + + MouseEnter += (s, e) => ViewModel.IsMouseOver = true; + MouseLeave += (s, e) => ViewModel.IsMouseOver = false; + + ViewModel.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(ThemePreviewerViewModel.BackImage) || + e.PropertyName == nameof(ThemePreviewerViewModel.FrontImage)) + { + Invalidate(); + } + }; + + KeyDown += ThemePreviewer_KeyDown; + } + + private void ThemePreviewer_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Left) + { + ViewModel.PreviousCommand.Execute(null); + e.Handled = true; + } + else if (e.KeyCode == Keys.Right) + { + ViewModel.NextCommand.Execute(null); + e.Handled = true; + } + } + + protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) + { + base.OnPaintSurface(e); + + var canvas = e.Surface.Canvas; + canvas.Clear(SKColors.Gray); + + var info = e.Info; + + // Draw back image + if (ViewModel.BackImage != null) + { + DrawImage(canvas, ViewModel.BackImage, info, 1.0f); + } + + // Draw front image with fade animation + if (ViewModel.FrontImage != null && isAnimating) + { + DrawImage(canvas, ViewModel.FrontImage, info, fadeProgress); + } + + // Draw UI overlay + if (ViewModel.ControlsVisible) + { + DrawOverlay(canvas, info); + } + } + + private void DrawImage(SKCanvas canvas, SKBitmap bitmap, SKImageInfo info, float opacity) + { + using (var paint = new SKPaint()) + { + paint.IsAntialias = true; + paint.FilterQuality = SKFilterQuality.High; + paint.Color = paint.Color.WithAlpha((byte)(255 * opacity)); + + var destRect = new SKRect(0, 0, info.Width, info.Height); + canvas.DrawBitmap(bitmap, destRect, paint); + } + } + + private void DrawOverlay(SKCanvas canvas, SKImageInfo info) + { + using (var paint = new SKPaint()) + { + paint.IsAntialias = true; + + // Draw left and right arrow button areas + if (ViewModel.ControlsVisible) + { + DrawArrowArea(canvas, info, true, paint); + DrawArrowArea(canvas, info, false, paint); + } + + // Title and preview text box (top left) - Border with Margin=20, StackPanel with Margin=10 + paint.Color = new SKColor(0, 0, 0, 127); + paint.Style = SKPaintStyle.Fill; + + // Measure text to calculate proper box size + paint.TextSize = 19; + paint.Typeface = SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); + var titleBounds = new SKRect(); + paint.MeasureText(ViewModel.Title ?? "", ref titleBounds); + + paint.TextSize = 16; + paint.Typeface = SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); + var previewBounds = new SKRect(); + paint.MeasureText(ViewModel.PreviewText ?? "", ref previewBounds); + + float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + 20; // 10 margin each side + float boxHeight = 19 + 4 + 16 + 20; // title size + margin + preview size + top/bottom margin + + var titleRect = SKRect.Create(20, 20, boxWidth, boxHeight); + paint.Color = new SKColor(0, 0, 0, 127); + canvas.DrawRoundRect(titleRect, 5, 5, paint); + + // Title text - 10px margin from border + paint.Color = SKColors.White; + paint.TextSize = 19; + paint.Typeface = SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); + canvas.DrawText(ViewModel.Title ?? "", 30, 20 + 10 + 19, paint); // margin + top padding + font size + + // Preview text - 4px below title, 16px font + paint.TextSize = 16; + paint.Typeface = SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); + canvas.DrawText(ViewModel.PreviewText ?? "", 30, 20 + 10 + 19 + 4 + 16, paint); // add 4px margin + 16px for text + + // Play/Pause button (top right) - MinWidth=40, MinHeight=40, Margin=20 + paint.Color = new SKColor(0, 0, 0, 127); + var playButtonRect = SKRect.Create(info.Width - 40 - 20, 20, 40, 40); + canvas.DrawRoundRect(playButtonRect, 5, 5, paint); + + float playOpacity = isMouseOverPlay ? 1.0f : 0.5f; + paint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); + paint.TextSize = 16; + if (fontAwesome != null) + { + paint.Typeface = fontAwesome; + string playIcon = ViewModel.IsPlaying ? "\uf04c" : "\uf04b"; + var textBounds = new SKRect(); + paint.MeasureText(playIcon, ref textBounds); + float centerX = info.Width - 20 - 20; + float centerY = 20 + 20; + canvas.DrawText(playIcon, centerX - textBounds.MidX, centerY - textBounds.MidY, paint); + } + else + { + string playIcon = ViewModel.IsPlaying ? "❚❚" : "▶"; + canvas.DrawText(playIcon, info.Width - 48, 48, paint); + } + + paint.Typeface = SKTypeface.FromFamilyName("Segoe UI"); + + // Author text (bottom right) - Margin="-3,0,0,-3", TextBlock Margin="8,4,11,9" + if (!string.IsNullOrEmpty(ViewModel.Author)) + { + paint.Color = new SKColor(0, 0, 0, 127); + paint.TextSize = 16; + var authorBounds = new SKRect(); + paint.MeasureText(ViewModel.Author, ref authorBounds); + // TextBlock margin: left=8, top=4, right=11, bottom=9 + float borderWidth = authorBounds.Width + 8 + 11; + float borderHeight = 4 + 16 + 9; // top margin + text height + bottom margin + var authorRect = SKRect.Create(info.Width - borderWidth + 3, info.Height - borderHeight + 3, borderWidth, borderHeight); + canvas.DrawRoundRect(authorRect, 5, 5, paint); + + paint.Color = SKColors.White.WithAlpha(127); + // Text positioned: border top + top margin + font baseline + canvas.DrawText(ViewModel.Author, info.Width - authorBounds.Width - 11 + 3, info.Height - borderHeight + 3 + 4 + 16, paint); + } + + // Download size (bottom left) - Margin="-3,0,0,-3", TextBlock Margin="11,4,8,9" + if (!string.IsNullOrEmpty(ViewModel.DownloadSize)) + { + paint.Color = new SKColor(0, 0, 0, 127); + paint.TextSize = 16; + var sizeBounds = new SKRect(); + paint.MeasureText(ViewModel.DownloadSize, ref sizeBounds); + // TextBlock margin: left=11, top=4, right=8, bottom=9 + float borderWidth = sizeBounds.Width + 11 + 8; + float borderHeight = 4 + 16 + 9; // top margin + text height + bottom margin + var sizeRect = SKRect.Create(-3, info.Height - borderHeight + 3, borderWidth, borderHeight); + canvas.DrawRoundRect(sizeRect, 5, 5, paint); + + paint.Color = SKColors.White.WithAlpha(127); + // Text positioned: border top + top margin + font baseline + canvas.DrawText(ViewModel.DownloadSize, 11 - 3, info.Height - borderHeight + 3 + 4 + 16, paint); + } + + // Download message (centered bottom) - Margin="0,0,0,15", TextBlock Margin="8,6,8,6" + if (!string.IsNullOrEmpty(ViewModel.Message)) + { + paint.TextSize = 16; + var msgBounds = new SKRect(); + paint.MeasureText(ViewModel.Message, ref msgBounds); + // TextBlock margin: 8,6,8,6 = left+right=16, top+bottom=12 + float msgWidth = msgBounds.Width + 16; + float msgHeight = 6 + 16 + 6; // top margin + text height + bottom margin + var msgRect = SKRect.Create(info.Width / 2 - msgWidth / 2, info.Height - msgHeight - 15, msgWidth, msgHeight); + + paint.Color = new SKColor(0, 0, 0, 127); + canvas.DrawRoundRect(msgRect, 5, 5, paint); + + float msgOpacity = isMouseOverDownload ? 1.0f : 0.8f; + paint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); + // Text positioned: border top + top margin + font baseline + canvas.DrawText(ViewModel.Message, info.Width / 2 - msgBounds.Width / 2, info.Height - msgHeight - 15 + 6 + 16, paint); + } + + // Carousel indicators - Margin=16, Height=32, Rectangle Height=3, Width=30, Margin="3,0" + if (ViewModel.CarouselIndicatorsVisible && ViewModel.Items.Count > 0) + { + DrawCarouselIndicators(canvas, info, paint); + } + } + } + + private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft, SKPaint paint) + { + bool isHovered = isLeft ? isMouseOverLeft : isMouseOverRight; + float opacity = isHovered ? 1.0f : 0.5f; + + float x = isLeft ? 40 : info.Width - 40; + float y = info.Height / 2; + + paint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); + paint.TextSize = 20; + + if (fontAwesome != null) + { + paint.Typeface = fontAwesome; + string icon = isLeft ? "\uf053" : "\uf054"; + var textBounds = new SKRect(); + paint.MeasureText(icon, ref textBounds); + canvas.DrawText(icon, x - textBounds.MidX, y - textBounds.MidY, paint); + paint.Typeface = SKTypeface.FromFamilyName("Segoe UI"); + } + else + { + string icon = isLeft ? "◀" : "▶"; + canvas.DrawText(icon, x - 10, y + 7, paint); + } + } + + private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info, SKPaint paint) + { + int count = ViewModel.Items.Count; + int indicatorWidth = 30; // Rectangle Width=30 + int indicatorHeight = 3; // Rectangle Height=3 + int itemSpacing = 6; // Margin="3,0" means 3px on each side = 6px spacing + int totalWidth = count * indicatorWidth + (count - 1) * itemSpacing; + int startX = (info.Width - totalWidth) / 2; + int y = info.Height - 16 - 32 / 2; // Margin=16 from bottom, Height=32, centered vertically + + for (int i = 0; i < count; i++) + { + float opacity = (i == ViewModel.SelectedIndex) ? 1.0f : 0.5f; + paint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); + + int rectX = startX + i * (indicatorWidth + itemSpacing); + var rect = SKRect.Create(rectX, y - indicatorHeight / 2, indicatorWidth, indicatorHeight); + canvas.DrawRect(rect, paint); + } + } + + protected override void OnMouseClick(MouseEventArgs e) + { + base.OnMouseClick(e); + + if (!ViewModel.ControlsVisible) return; + + // Check if play button was clicked - MinWidth=40, MinHeight=40, Margin=20 + var playButtonRect = new Rectangle(Width - 40 - 20, 20, 40, 40); + if (playButtonRect.Contains(e.Location)) + { + ViewModel.PlayCommand.Execute(null); + return; + } + + // Check if left arrow area was clicked + if (e.X < 80) + { + ViewModel.PreviousCommand.Execute(null); + return; + } + + // Check if right arrow area was clicked + if (e.X > Width - 80) + { + ViewModel.NextCommand.Execute(null); + return; + } + + // Check if download message was clicked - Margin="0,0,0,15", TextBlock Margin="8,6,8,6" + if (!string.IsNullOrEmpty(ViewModel.Message)) + { + using (var paint = new SKPaint()) + { + paint.TextSize = 16; + var msgBounds = new SKRect(); + paint.MeasureText(ViewModel.Message, ref msgBounds); + float msgWidth = msgBounds.Width + 16; // 8+8 + float msgHeight = 6 + 16 + 6; // top + text + bottom margin + var msgRect = new Rectangle((int)(Width / 2 - msgWidth / 2), Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); + if (msgRect.Contains(e.Location)) + { + ViewModel.DownloadCommand.Execute(null); + return; + } + } + } + + // Check if carousel indicator was clicked - Margin=16, Height=32 + if (ViewModel.CarouselIndicatorsVisible && ViewModel.Items.Count > 0) + { + int count = ViewModel.Items.Count; + int indicatorWidth = 30; + int itemSpacing = 6; + int totalWidth = count * indicatorWidth + (count - 1) * itemSpacing; + int startX = (Width - totalWidth) / 2; + int y = Height - 16 - 32 / 2; + + for (int i = 0; i < count; i++) + { + int rectX = startX + i * (indicatorWidth + itemSpacing); + var rect = new Rectangle(rectX, y - 16, indicatorWidth, 32); // Full clickable height + if (rect.Contains(e.Location)) + { + ViewModel.SelectedIndex = i; + return; + } + } + } + } + + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + + mousePosition = e.Location; + bool needsRedraw = false; + + // Update cursor based on location + if (!ViewModel.ControlsVisible) + { + Cursor = Cursors.Default; + return; + } + + bool wasOverPlay = isMouseOverPlay; + bool wasOverLeft = isMouseOverLeft; + bool wasOverRight = isMouseOverRight; + bool wasOverDownload = isMouseOverDownload; + + // Play button - MinWidth=40, MinHeight=40, Margin=20 + var playButtonRect = new Rectangle(Width - 40 - 20, 20, 40, 40); + isMouseOverPlay = playButtonRect.Contains(e.Location); + + // Left arrow (80px wide area on left side, full height) + isMouseOverLeft = e.X < 80; + + // Right arrow (80px wide area on right side, full height) + isMouseOverRight = e.X > Width - 80; + + // Download message + isMouseOverDownload = false; + if (!string.IsNullOrEmpty(ViewModel.Message)) + { + using (var paint = new SKPaint()) + { + paint.TextSize = 16; + var msgBounds = new SKRect(); + paint.MeasureText(ViewModel.Message, ref msgBounds); + float msgWidth = msgBounds.Width + 16; // 8+8 + float msgHeight = 6 + 16 + 6; // top + text + bottom margin + var msgRect = new Rectangle((int)(Width / 2 - msgWidth / 2), Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); + isMouseOverDownload = msgRect.Contains(e.Location); + } + } + + needsRedraw = (isMouseOverPlay != wasOverPlay) || (isMouseOverLeft != wasOverLeft) || + (isMouseOverRight != wasOverRight) || (isMouseOverDownload != wasOverDownload); + + bool isOverClickable = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload; + Cursor = isOverClickable ? Cursors.Hand : Cursors.Default; + + if (needsRedraw) + { + Invalidate(); + } + } + + private void StartAnimation() + { + fadeProgress = 0f; + isAnimating = true; + animationStartTime = DateTime.Now; + fadeTimer.Start(); + } + + private void StopAnimation() + { + fadeTimer.Stop(); + isAnimating = false; + fadeProgress = 0f; + Invalidate(); + } + + private void FadeTimer_Tick(object sender, EventArgs e) + { + var elapsed = DateTime.Now - animationStartTime; + fadeProgress = Math.Min(1.0f, (float)(elapsed.TotalMilliseconds / ANIMATION_DURATION_MS)); + + // Ease in-out sine function + fadeProgress = (float)(Math.Sin((fadeProgress - 0.5) * Math.PI) / 2 + 0.5); + + Invalidate(); + + if (fadeProgress >= 1.0f) + { + fadeTimer.Stop(); + isAnimating = false; + ViewModel.OnAnimationComplete(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + fadeTimer?.Dispose(); + animationTimer?.Dispose(); + ViewModel?.Stop(); + } + base.Dispose(disposing); + } + } +} diff --git a/src/SkiaSharp/ThemePreviewerViewModel.cs b/src/SkiaSharp/ThemePreviewerViewModel.cs new file mode 100644 index 00000000..7c245acb --- /dev/null +++ b/src/SkiaSharp/ThemePreviewerViewModel.cs @@ -0,0 +1,468 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using SkiaSharp; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace WinDynamicDesktop.SkiaSharp +{ + public class ThemePreviewItem + { + public string PreviewText { get; set; } + public Uri Uri { get; set; } + + public ThemePreviewItem(string previewText, string path) + { + PreviewText = previewText; + + string fullPath = Path.GetFullPath(path); + if (File.Exists(fullPath)) + { + Uri = new Uri(fullPath, UriKind.Absolute); + } + else + { + Uri = new Uri(path, UriKind.Relative); + } + } + } + + public class ThemePreviewerViewModel : INotifyPropertyChanged + { + #region Properties + + public bool ControlsVisible => !string.IsNullOrEmpty(Title); + public bool MessageVisible => !string.IsNullOrEmpty(Message); + public bool CarouselIndicatorsVisible => string.IsNullOrEmpty(Message); + public bool DownloadSizeVisible => !string.IsNullOrEmpty(DownloadSize); + + private string title; + public string Title + { + get => title; + set + { + SetProperty(ref title, value); + OnPropertyChanged(nameof(ControlsVisible)); + } + } + + private string author; + public string Author + { + get => author; + set => SetProperty(ref author, value); + } + + private string previewText; + public string PreviewText + { + get => previewText; + set => SetProperty(ref previewText, value); + } + + private string message; + public string Message + { + get => message; + set + { + SetProperty(ref message, value); + OnPropertyChanged(nameof(MessageVisible)); + OnPropertyChanged(nameof(CarouselIndicatorsVisible)); + } + } + + private Action downloadAction; + public Action DownloadAction + { + get => downloadAction; + set => SetProperty(ref downloadAction, value); + } + + private string downloadSize; + public string DownloadSize + { + get => downloadSize; + set => SetProperty(ref downloadSize, value); + } + + private SKBitmap backImage; + public SKBitmap BackImage + { + get => backImage; + set => SetProperty(ref backImage, value); + } + + private SKBitmap frontImage; + public SKBitmap FrontImage + { + get => frontImage; + set => SetProperty(ref frontImage, value); + } + + private bool isPlaying; + public bool IsPlaying + { + get => isPlaying; + set => SetProperty(ref isPlaying, value); + } + + private bool isMouseOver; + public bool IsMouseOver + { + get => isMouseOver; + set + { + SetProperty(ref isMouseOver, value); + if (value) + { + transitionTimer.Stop(); + } + else if (IsPlaying && fadeQueue.IsEmpty) + { + transitionTimer.Start(); + } + } + } + + private int selectedIndex; + public int SelectedIndex + { + get => selectedIndex; + set + { + if (value != selectedIndex) + { + GoTo(value); + } + SetProperty(ref selectedIndex, value); + } + } + + public ObservableCollection Items { get; } = new ObservableCollection(); + + #endregion + + #region Commands + + public ICommand PlayCommand => new RelayCommand(() => + { + IsPlaying = !IsPlaying; + if (IsPlaying && fadeQueue.IsEmpty) + { + transitionTimer.Start(); + } + }); + + public ICommand PreviousCommand => new RelayCommand(Previous); + + public ICommand NextCommand => new RelayCommand(Next); + + public ICommand DownloadCommand => new RelayCommand(() => DownloadAction?.Invoke()); + + #endregion + + private static readonly Func _ = Localization.GetTranslation; + + private const int TRANSITION_TIME = 5; + + private readonly BitmapCache cache = new BitmapCache(); + private readonly System.Windows.Forms.Timer transitionTimer; + private readonly ConcurrentQueue fadeQueue = new ConcurrentQueue(); + private readonly SemaphoreSlim fadeSemaphore = new SemaphoreSlim(1, 1); + private readonly Action startAnimation; + private readonly Action stopAnimation; + + public ThemePreviewerViewModel(Action startAnimation, Action stopAnimation) + { + this.startAnimation = startAnimation; + this.stopAnimation = stopAnimation; + + transitionTimer = new System.Windows.Forms.Timer() + { + Interval = TRANSITION_TIME * 1000 + }; + transitionTimer.Tick += (s, e) => Next(); + + IsPlaying = true; + } + + public void OnAnimationComplete() + { + BackImage = FrontImage; + FrontImage = null; + + int nextIndex = -1; + while (fadeQueue.TryDequeue(out int index)) + { + nextIndex = index; + } + + if (nextIndex != -1) + { + FrontImage = cache[Items[nextIndex].Uri]; + startAnimation(); + } + else + { + TryRelease(fadeSemaphore); + + if (IsPlaying && !IsMouseOver) + { + transitionTimer.Start(); + } + } + } + + public void PreviewTheme(ThemeConfig theme, Action downloadAction, string imagePath = null) + { + Stop(); + + int activeImage = 0; + string[] sunrise = null; + string[] day = null; + string[] sunset = null; + string[] night = null; + + if (theme != null) + { + DownloadAction = () => downloadAction.Invoke(theme); + Title = ThemeManager.GetThemeName(theme); + Author = ThemeManager.GetThemeAuthor(theme); + bool isDownloaded = ThemeManager.IsThemeDownloaded(theme); + + if (isDownloaded) + { + ThemeManager.CalcThemeInstallSize(theme, size => { DownloadSize = size; }); + + List imageTimes = SolarScheduler.GetAllImageTimes(theme); + activeImage = imageTimes.FindLastIndex((time) => time <= DateTime.Now); + if (activeImage == -1) + { + activeImage = imageTimes.FindLastIndex((time) => time.AddDays(-1) <= DateTime.Now); + } + + if (theme.sunriseImageList != null && !theme.sunriseImageList.SequenceEqual(theme.dayImageList)) + { + sunrise = ImagePaths(theme, theme.sunriseImageList); + AddItems(_("Sunrise"), sunrise, imageTimes.Take(theme.sunriseImageList.Length).ToArray()); + imageTimes.RemoveRange(0, theme.sunriseImageList.Length); + } + + day = ImagePaths(theme, theme.dayImageList); + AddItems(_("Day"), day, imageTimes.Take(theme.dayImageList.Length).ToArray()); + imageTimes.RemoveRange(0, theme.dayImageList.Length); + + if (theme.sunsetImageList != null && !theme.sunsetImageList.SequenceEqual(theme.dayImageList)) + { + sunset = ImagePaths(theme, theme.sunsetImageList); + AddItems(_("Sunset"), sunset, imageTimes.Take(theme.sunsetImageList.Length).ToArray()); + imageTimes.RemoveRange(0, theme.sunsetImageList.Length); + } + + night = ImagePaths(theme, theme.nightImageList); + AddItems(_("Night"), night, imageTimes.Take(theme.nightImageList.Length).ToArray()); + imageTimes.RemoveRange(0, theme.nightImageList.Length); + } + else + { + Message = _("Theme is not downloaded. Click here to download and enable full preview."); + ThemeManager.CalcThemeDownloadSize(theme, size => { DownloadSize = size; }); + + string[] resourceNames = Assembly.GetExecutingAssembly().GetManifestResourceNames(); + string path = "WinDynamicDesktop.resources.images." + theme.themeId + "_{0}.jpg"; + + string rsrcName = string.Format(path, "sunrise"); + if (resourceNames.Contains(rsrcName)) + { + sunrise = new[] { rsrcName }; + } + + rsrcName = string.Format(path, "day"); + if (resourceNames.Contains(rsrcName)) + { + day = new[] { rsrcName }; + } + + rsrcName = string.Format(path, "sunset"); + if (resourceNames.Contains(rsrcName)) + { + sunset = new[] { rsrcName }; + } + + rsrcName = string.Format(path, "night"); + if (resourceNames.Contains(rsrcName)) + { + night = new[] { rsrcName }; + } + + AddItems(_("Sunrise"), sunrise, null); + AddItems(_("Day"), day, null); + AddItems(_("Sunset"), sunset, null); + AddItems(_("Night"), night, null); + + SolarData solarData = SunriseSunsetService.GetSolarData(DateTime.Today); + DaySegmentData segmentData = SolarScheduler.GetDaySegmentData(solarData, DateTime.Now); + activeImage = (sunrise != null && sunset != null) ? segmentData.segment4 : segmentData.segment2; + } + } + else + { + Author = "Microsoft"; + Items.Add(new ThemePreviewItem(string.Empty, imagePath)); + activeImage = 0; + } + + Start(activeImage); + } + + private void Previous() + { + if (SelectedIndex == 0) + { + SelectedIndex = Items.Count - 1; + } + else + { + SelectedIndex--; + } + } + + private void Next() + { + if (SelectedIndex == Items.Count - 1) + { + SelectedIndex = 0; + } + else + { + SelectedIndex++; + } + } + + private void GoTo(int index) + { + if (index < 0 || index >= Items.Count) return; + + transitionTimer.Stop(); + + if (fadeSemaphore.Wait(0)) + { + FrontImage = cache[Items[index].Uri]; + startAnimation(); + } + else + { + fadeQueue.Enqueue(index); + } + + PreviewText = Items[index].PreviewText; + } + + public void Stop() + { + stopAnimation(); + while (fadeQueue.TryDequeue(out int temp)) { } + TryRelease(fadeSemaphore); + + transitionTimer.Stop(); + + Title = null; + Author = null; + PreviewText = null; + Message = null; + DownloadSize = null; + BackImage = null; + FrontImage = null; + SelectedIndex = -1; + + Items.Clear(); + cache.Clear(); + } + + private void AddItems(string previewName, string[] items, DateTime[] imageTimes) + { + if (items == null) return; + + for (int i = 0; i < items.Length; i++) + { + string previewText = previewName; + + if (imageTimes == null) // Theme not downloaded + { + previewText = string.Format(_("Previewing {0}"), previewName); + } + else if (imageTimes[i] == DateTime.MinValue) // Image not active + { + previewText = string.Format(_("Previewing {0} ({1}/{2})"), previewName, i + 1, items.Length); + } + else + { + previewText = string.Format(_("Previewing {0} at {1}"), previewName, imageTimes[i].ToShortTimeString()); + } + + Items.Add(new ThemePreviewItem(previewText, items[i])); + } + } + + private void Start(int index) + { + var item = Items[index]; + + PreviewText = item.PreviewText; + BackImage = cache[item.Uri]; + + selectedIndex = index; + OnPropertyChanged(nameof(SelectedIndex)); + + if (IsPlaying && !IsMouseOver) + { + transitionTimer.Start(); + } + } + + private static string[] ImagePaths(ThemeConfig theme, int[] imageList) + { + string themePath = ThemeManager.GetThemeDirectory(theme); + return imageList.Select(id => + Path.Combine(themePath, theme.imageFilename.Replace("*", id.ToString()))).ToArray(); + } + + private static void TryRelease(SemaphoreSlim semaphore) + { + try + { + semaphore.Release(); + } + catch (SemaphoreFullException) { } + } + + #region INotifyPropertyChanged + + public event PropertyChangedEventHandler PropertyChanged; + + private void SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") + { + field = value; + OnPropertyChanged(propertyName); + } + + private void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + } +} diff --git a/src/ThemeDialog.Designer.cs b/src/ThemeDialog.Designer.cs index 14d53c5b..bd37ac4f 100644 --- a/src/ThemeDialog.Designer.cs +++ b/src/ThemeDialog.Designer.cs @@ -40,7 +40,7 @@ private void InitializeComponent() toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); showInstalledMenuItem = new System.Windows.Forms.ToolStripMenuItem(); displayComboBox = new System.Windows.Forms.ComboBox(); - previewerHost = new System.Windows.Forms.Integration.ElementHost(); + previewerHost = new WinDynamicDesktop.SkiaSharp.ThemePreviewer(); listView1 = new System.Windows.Forms.ListView(); advancedButton = new System.Windows.Forms.Button(); searchBox = new System.Windows.Forms.TextBox(); @@ -248,7 +248,7 @@ private void InitializeComponent() private System.Windows.Forms.OpenFileDialog openFileDialog1; private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; private System.Windows.Forms.ToolStripMenuItem favoriteThemeMenuItem; - private System.Windows.Forms.Integration.ElementHost previewerHost; + private WinDynamicDesktop.SkiaSharp.ThemePreviewer previewerHost; private System.Windows.Forms.ListView listView1; private System.Windows.Forms.ToolStripMenuItem deleteThemeMenuItem; private System.Windows.Forms.ComboBox displayComboBox; diff --git a/src/ThemeDialog.cs b/src/ThemeDialog.cs index 788d5eb3..4869ad24 100644 --- a/src/ThemeDialog.cs +++ b/src/ThemeDialog.cs @@ -24,8 +24,6 @@ public partial class ThemeDialog : Form private readonly string windowsWallpaper = ThemeThumbLoader.GetWindowsWallpaper(false); private readonly string windowsLockScreen = ThemeThumbLoader.GetWindowsWallpaper(true); - private WPF.ThemePreviewer previewer; - public ThemeDialog() { InitializeComponent(); @@ -190,7 +188,7 @@ private void UpdateSelectedItem() { if (listView1.Items.Count == 0) { - previewer.ViewModel.PreviewTheme(null, null, + previewerHost.ViewModel.PreviewTheme(null, null, ThemeThumbLoader.GetWindowsWallpaper(IsLockScreenSelected)); } applyButton.Enabled = false; @@ -210,15 +208,12 @@ private void UpdateSelectedItem() } applyButton.Enabled = true; - previewer.ViewModel.PreviewTheme(theme, new Action((theme) => DownloadTheme(theme)), + previewerHost.ViewModel.PreviewTheme(theme, new Action((theme) => DownloadTheme(theme)), ThemeThumbLoader.GetWindowsWallpaper(IsLockScreenSelected)); } private void ThemeDialog_Load(object sender, EventArgs e) { - previewer = new WPF.ThemePreviewer(); - previewerHost.Child = previewer; - listView1.ContextMenuStrip = contextMenuStrip1; listView1.ListViewItemSorter = new CompareByItemText(); ThemeDialogUtils.SetWindowTheme(listView1.Handle, "Explorer", null); @@ -534,7 +529,7 @@ private void OnFormClosing(object sender, FormClosingEventArgs e) private void OnFormClosed(object sender, FormClosedEventArgs e) { - this.Invoke(previewer.ViewModel.Stop); + this.Invoke(previewerHost.ViewModel.Stop); } } diff --git a/src/WinDynamicDesktop.csproj b/src/WinDynamicDesktop.csproj index 61ac9fe0..12723f12 100644 --- a/src/WinDynamicDesktop.csproj +++ b/src/WinDynamicDesktop.csproj @@ -16,12 +16,14 @@ true - true - + + + + @@ -37,6 +39,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + From fceba515ea4d6ca4706e7d9e44172b1679f7a2c3 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Tue, 18 Nov 2025 06:40:50 -0500 Subject: [PATCH 02/15] Optimize Skia rendering for 120 fps --- src/{SkiaSharp => Skia}/BitmapCache.cs | 6 +- src/{SkiaSharp => Skia}/ThemePreviewer.cs | 345 +++++++++--------- .../ThemePreviewerViewModel.cs | 21 +- src/SkiaSharp/RelayCommand.cs | 46 --- src/ThemeDialog.Designer.cs | 4 +- 5 files changed, 186 insertions(+), 236 deletions(-) rename src/{SkiaSharp => Skia}/BitmapCache.cs (96%) rename src/{SkiaSharp => Skia}/ThemePreviewer.cs (53%) rename src/{SkiaSharp => Skia}/ThemePreviewerViewModel.cs (97%) delete mode 100644 src/SkiaSharp/RelayCommand.cs diff --git a/src/SkiaSharp/BitmapCache.cs b/src/Skia/BitmapCache.cs similarity index 96% rename from src/SkiaSharp/BitmapCache.cs rename to src/Skia/BitmapCache.cs index 580cc7ab..c3a48d9b 100644 --- a/src/SkiaSharp/BitmapCache.cs +++ b/src/Skia/BitmapCache.cs @@ -10,7 +10,7 @@ using System.Windows.Forms; using SkiaSharp; -namespace WinDynamicDesktop.SkiaSharp +namespace WinDynamicDesktop.Skia { sealed class BitmapCache { @@ -138,10 +138,10 @@ private SKBitmap CreateImage(Uri uri) using (var fullBitmap = new SKBitmap(info)) { codec.GetPixels(fullBitmap.Info, fullBitmap.GetPixels()); - fullBitmap.ScalePixels(bitmap, SKFilterQuality.High); + fullBitmap.ScalePixels(bitmap, new SKSamplingOptions(SKCubicResampler.Mitchell)); } } - + return bitmap; } } diff --git a/src/SkiaSharp/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs similarity index 53% rename from src/SkiaSharp/ThemePreviewer.cs rename to src/Skia/ThemePreviewer.cs index c9f5bf47..fcc62e53 100644 --- a/src/SkiaSharp/ThemePreviewer.cs +++ b/src/Skia/ThemePreviewer.cs @@ -2,28 +2,45 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +using SkiaSharp; +using SkiaSharp.Views.Desktop; using System; using System.Drawing; using System.IO; using System.Reflection; using System.Windows.Forms; -using SkiaSharp; -using SkiaSharp.Views.Desktop; -namespace WinDynamicDesktop.SkiaSharp +namespace WinDynamicDesktop.Skia { public class ThemePreviewer : SKControl { + private const int ANIMATION_DURATION_MS = 600; + private const int ANIMATION_FPS = 120; + private const int MARGIN_STANDARD = 20; + private const int BORDER_RADIUS = 5; + private const byte OVERLAY_ALPHA = 127; + private const float OPACITY_NORMAL = 0.5f; + private const float OPACITY_HOVER = 1.0f; + private const float OPACITY_MESSAGE = 0.8f; + public ThemePreviewerViewModel ViewModel { get; } private readonly Timer animationTimer; private readonly Timer fadeTimer; private float fadeProgress = 0f; private bool isAnimating = false; - private const int ANIMATION_DURATION_MS = 600; - private const int ANIMATION_FPS = 60; private DateTime animationStartTime; private static SKTypeface fontAwesome; + + // Cached objects to reduce allocations + private readonly SKPaint basePaint = new SKPaint { IsAntialias = true }; + private readonly SKFont titleFont; + private readonly SKFont previewFont; + private readonly SKFont textFont; + private readonly SKFont iconFont16; + private readonly SKFont iconFont20; + private readonly SKSamplingOptions samplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); + private Point mousePosition; private bool isMouseOverPlay = false; private bool isMouseOverLeft = false; @@ -41,13 +58,17 @@ public ThemePreviewer() using (Stream fontStream = Assembly.GetExecutingAssembly() .GetManifestResourceStream("WinDynamicDesktop.resources.fonts.fontawesome-webfont.ttf")) { - if (fontStream != null) - { - fontAwesome = SKTypeface.FromStream(fontStream); - } + fontAwesome = SKTypeface.FromStream(fontStream); } } + // Initialize cached fonts + titleFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 19); + previewFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 16); + textFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 16); + iconFont16 = new SKFont(fontAwesome, 16); + iconFont20 = new SKFont(fontAwesome, 20); + // Timer for smooth fade animations fadeTimer = new Timer { @@ -67,7 +88,9 @@ public ThemePreviewer() ViewModel.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(ThemePreviewerViewModel.BackImage) || - e.PropertyName == nameof(ThemePreviewerViewModel.FrontImage)) + e.PropertyName == nameof(ThemePreviewerViewModel.FrontImage) || + e.PropertyName == nameof(ThemePreviewerViewModel.IsPlaying) || + e.PropertyName == nameof(ThemePreviewerViewModel.DownloadSize)) { Invalidate(); } @@ -80,12 +103,12 @@ private void ThemePreviewer_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Left) { - ViewModel.PreviousCommand.Execute(null); + ViewModel.Previous(); e.Handled = true; } else if (e.KeyCode == Keys.Right) { - ViewModel.NextCommand.Execute(null); + ViewModel.Next(); e.Handled = true; } } @@ -120,181 +143,157 @@ protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) private void DrawImage(SKCanvas canvas, SKBitmap bitmap, SKImageInfo info, float opacity) { - using (var paint = new SKPaint()) + var destRect = new SKRect(0, 0, info.Width, info.Height); + + if (opacity >= 1.0f) + { + // Fast path for fully opaque images + using (var image = SKImage.FromBitmap(bitmap)) + { + canvas.DrawImage(image, destRect, samplingOptions, null); + } + } + else { - paint.IsAntialias = true; - paint.FilterQuality = SKFilterQuality.High; - paint.Color = paint.Color.WithAlpha((byte)(255 * opacity)); + // Apply opacity with color filter + using (var paint = new SKPaint()) + { + paint.IsAntialias = true; + paint.ColorFilter = SKColorFilter.CreateBlendMode( + SKColors.White.WithAlpha((byte)(255 * opacity)), + SKBlendMode.DstIn); - var destRect = new SKRect(0, 0, info.Width, info.Height); - canvas.DrawBitmap(bitmap, destRect, paint); + using (var image = SKImage.FromBitmap(bitmap)) + { + canvas.DrawImage(image, destRect, samplingOptions, paint); + } + } } } private void DrawOverlay(SKCanvas canvas, SKImageInfo info) { - using (var paint = new SKPaint()) + basePaint.Style = SKPaintStyle.Fill; + + // Draw left and right arrow button areas + if (ViewModel.ControlsVisible) { - paint.IsAntialias = true; + DrawArrowArea(canvas, info, true); + DrawArrowArea(canvas, info, false); + } - // Draw left and right arrow button areas - if (ViewModel.ControlsVisible) - { - DrawArrowArea(canvas, info, true, paint); - DrawArrowArea(canvas, info, false, paint); - } + // Title and preview text box (top left) - Border with Margin=20, StackPanel with Margin=10 + basePaint.Color = new SKColor(0, 0, 0, 127); - // Title and preview text box (top left) - Border with Margin=20, StackPanel with Margin=10 - paint.Color = new SKColor(0, 0, 0, 127); - paint.Style = SKPaintStyle.Fill; - - // Measure text to calculate proper box size - paint.TextSize = 19; - paint.Typeface = SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - var titleBounds = new SKRect(); - paint.MeasureText(ViewModel.Title ?? "", ref titleBounds); - - paint.TextSize = 16; - paint.Typeface = SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - var previewBounds = new SKRect(); - paint.MeasureText(ViewModel.PreviewText ?? "", ref previewBounds); - - float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + 20; // 10 margin each side - float boxHeight = 19 + 4 + 16 + 20; // title size + margin + preview size + top/bottom margin - - var titleRect = SKRect.Create(20, 20, boxWidth, boxHeight); - paint.Color = new SKColor(0, 0, 0, 127); - canvas.DrawRoundRect(titleRect, 5, 5, paint); - - // Title text - 10px margin from border - paint.Color = SKColors.White; - paint.TextSize = 19; - paint.Typeface = SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - canvas.DrawText(ViewModel.Title ?? "", 30, 20 + 10 + 19, paint); // margin + top padding + font size - - // Preview text - 4px below title, 16px font - paint.TextSize = 16; - paint.Typeface = SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - canvas.DrawText(ViewModel.PreviewText ?? "", 30, 20 + 10 + 19 + 4 + 16, paint); // add 4px margin + 16px for text - - // Play/Pause button (top right) - MinWidth=40, MinHeight=40, Margin=20 - paint.Color = new SKColor(0, 0, 0, 127); - var playButtonRect = SKRect.Create(info.Width - 40 - 20, 20, 40, 40); - canvas.DrawRoundRect(playButtonRect, 5, 5, paint); - - float playOpacity = isMouseOverPlay ? 1.0f : 0.5f; - paint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); - paint.TextSize = 16; - if (fontAwesome != null) - { - paint.Typeface = fontAwesome; - string playIcon = ViewModel.IsPlaying ? "\uf04c" : "\uf04b"; - var textBounds = new SKRect(); - paint.MeasureText(playIcon, ref textBounds); - float centerX = info.Width - 20 - 20; - float centerY = 20 + 20; - canvas.DrawText(playIcon, centerX - textBounds.MidX, centerY - textBounds.MidY, paint); - } - else - { - string playIcon = ViewModel.IsPlaying ? "❚❚" : "▶"; - canvas.DrawText(playIcon, info.Width - 48, 48, paint); - } + // Measure text to calculate proper box size + var titleBounds = new SKRect(); + titleFont.MeasureText(ViewModel.Title ?? "", out titleBounds); - paint.Typeface = SKTypeface.FromFamilyName("Segoe UI"); + var previewBounds = new SKRect(); + previewFont.MeasureText(ViewModel.PreviewText ?? "", out previewBounds); - // Author text (bottom right) - Margin="-3,0,0,-3", TextBlock Margin="8,4,11,9" - if (!string.IsNullOrEmpty(ViewModel.Author)) - { - paint.Color = new SKColor(0, 0, 0, 127); - paint.TextSize = 16; - var authorBounds = new SKRect(); - paint.MeasureText(ViewModel.Author, ref authorBounds); - // TextBlock margin: left=8, top=4, right=11, bottom=9 - float borderWidth = authorBounds.Width + 8 + 11; - float borderHeight = 4 + 16 + 9; // top margin + text height + bottom margin - var authorRect = SKRect.Create(info.Width - borderWidth + 3, info.Height - borderHeight + 3, borderWidth, borderHeight); - canvas.DrawRoundRect(authorRect, 5, 5, paint); - - paint.Color = SKColors.White.WithAlpha(127); - // Text positioned: border top + top margin + font baseline - canvas.DrawText(ViewModel.Author, info.Width - authorBounds.Width - 11 + 3, info.Height - borderHeight + 3 + 4 + 16, paint); - } + float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + 20; // 10 margin each side + float boxHeight = 19 + 4 + 16 + 20; // title size + margin + preview size + top/bottom margin - // Download size (bottom left) - Margin="-3,0,0,-3", TextBlock Margin="11,4,8,9" - if (!string.IsNullOrEmpty(ViewModel.DownloadSize)) - { - paint.Color = new SKColor(0, 0, 0, 127); - paint.TextSize = 16; - var sizeBounds = new SKRect(); - paint.MeasureText(ViewModel.DownloadSize, ref sizeBounds); - // TextBlock margin: left=11, top=4, right=8, bottom=9 - float borderWidth = sizeBounds.Width + 11 + 8; - float borderHeight = 4 + 16 + 9; // top margin + text height + bottom margin - var sizeRect = SKRect.Create(-3, info.Height - borderHeight + 3, borderWidth, borderHeight); - canvas.DrawRoundRect(sizeRect, 5, 5, paint); - - paint.Color = SKColors.White.WithAlpha(127); - // Text positioned: border top + top margin + font baseline - canvas.DrawText(ViewModel.DownloadSize, 11 - 3, info.Height - borderHeight + 3 + 4 + 16, paint); - } + var titleRect = SKRect.Create(20, 20, boxWidth, boxHeight); + basePaint.Color = new SKColor(0, 0, 0, 127); + canvas.DrawRoundRect(titleRect, 5, 5, basePaint); - // Download message (centered bottom) - Margin="0,0,0,15", TextBlock Margin="8,6,8,6" - if (!string.IsNullOrEmpty(ViewModel.Message)) - { - paint.TextSize = 16; - var msgBounds = new SKRect(); - paint.MeasureText(ViewModel.Message, ref msgBounds); - // TextBlock margin: 8,6,8,6 = left+right=16, top+bottom=12 - float msgWidth = msgBounds.Width + 16; - float msgHeight = 6 + 16 + 6; // top margin + text height + bottom margin - var msgRect = SKRect.Create(info.Width / 2 - msgWidth / 2, info.Height - msgHeight - 15, msgWidth, msgHeight); - - paint.Color = new SKColor(0, 0, 0, 127); - canvas.DrawRoundRect(msgRect, 5, 5, paint); - - float msgOpacity = isMouseOverDownload ? 1.0f : 0.8f; - paint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); - // Text positioned: border top + top margin + font baseline - canvas.DrawText(ViewModel.Message, info.Width / 2 - msgBounds.Width / 2, info.Height - msgHeight - 15 + 6 + 16, paint); - } + // Title text - 10px margin from border + basePaint.Color = SKColors.White; + canvas.DrawText(ViewModel.Title ?? "", 30, 20 + 10 + 19, titleFont, basePaint); // margin + top padding + font size - // Carousel indicators - Margin=16, Height=32, Rectangle Height=3, Width=30, Margin="3,0" - if (ViewModel.CarouselIndicatorsVisible && ViewModel.Items.Count > 0) - { - DrawCarouselIndicators(canvas, info, paint); - } + // Preview text - 4px below title, 16px font + canvas.DrawText(ViewModel.PreviewText ?? "", 30, 20 + 10 + 19 + 4 + 16, previewFont, basePaint); // add 4px margin + 16px for text + + // Play/Pause button (top right) - MinWidth=40, MinHeight=40, Margin=20 + basePaint.Color = new SKColor(0, 0, 0, 127); + var playButtonRect = SKRect.Create(info.Width - 40 - 20, 20, 40, 40); + canvas.DrawRoundRect(playButtonRect, 5, 5, basePaint); + + float playOpacity = isMouseOverPlay ? 1.0f : 0.5f; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); + string playIcon = ViewModel.IsPlaying ? "\uf04c" : "\uf04b"; + var textBounds = new SKRect(); + iconFont16.MeasureText(playIcon, out textBounds); + float centerX = info.Width - 20 - 20; + float centerY = 20 + 20; + canvas.DrawText(playIcon, centerX - textBounds.MidX, centerY - textBounds.MidY, iconFont16, basePaint); + + // Corner labels + DrawCornerLabel(canvas, info, ViewModel.Author, isBottomRight: true); + DrawCornerLabel(canvas, info, ViewModel.DownloadSize, isBottomRight: false); + + // Download message (centered bottom) - Margin="0,0,0,15", TextBlock Margin="8,6,8,6" + if (!string.IsNullOrEmpty(ViewModel.Message)) + { + var msgBounds = new SKRect(); + textFont.MeasureText(ViewModel.Message, out msgBounds); + // TextBlock margin: 8,6,8,6 = left+right=16, top+bottom=12 + float msgWidth = msgBounds.Width + 16; + float msgHeight = 6 + 16 + 6; // top margin + text height + bottom margin + var msgRect = SKRect.Create(info.Width / 2 - msgWidth / 2, info.Height - msgHeight - 15, msgWidth, msgHeight); + + basePaint.Color = new SKColor(0, 0, 0, 127); + canvas.DrawRoundRect(msgRect, 5, 5, basePaint); + + float msgOpacity = isMouseOverDownload ? 1.0f : 0.8f; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); + // Text positioned: border top + top margin + font baseline + canvas.DrawText(ViewModel.Message, info.Width / 2 - msgBounds.Width / 2, info.Height - msgHeight - 15 + 6 + 16, textFont, basePaint); + } + + // Carousel indicators - Margin=16, Height=32, Rectangle Height=3, Width=30, Margin="3,0" + if (ViewModel.CarouselIndicatorsVisible && ViewModel.Items.Count > 0) + { + DrawCarouselIndicators(canvas, info); } } - private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft, SKPaint paint) + private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft) { bool isHovered = isLeft ? isMouseOverLeft : isMouseOverRight; float opacity = isHovered ? 1.0f : 0.5f; - + float x = isLeft ? 40 : info.Width - 40; float y = info.Height / 2; - paint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); - paint.TextSize = 20; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); - if (fontAwesome != null) - { - paint.Typeface = fontAwesome; - string icon = isLeft ? "\uf053" : "\uf054"; - var textBounds = new SKRect(); - paint.MeasureText(icon, ref textBounds); - canvas.DrawText(icon, x - textBounds.MidX, y - textBounds.MidY, paint); - paint.Typeface = SKTypeface.FromFamilyName("Segoe UI"); - } - else - { - string icon = isLeft ? "◀" : "▶"; - canvas.DrawText(icon, x - 10, y + 7, paint); - } + string icon = isLeft ? "\uf053" : "\uf054"; + var textBounds = new SKRect(); + iconFont20.MeasureText(icon, out textBounds); + canvas.DrawText(icon, x - textBounds.MidX, y - textBounds.MidY, iconFont20, basePaint); + } + + private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, bool isBottomRight) + { + if (string.IsNullOrEmpty(text)) return; + + basePaint.Color = new SKColor(0, 0, 0, OVERLAY_ALPHA); + var textBounds = new SKRect(); + textFont.MeasureText(text, out textBounds); + + // Calculate margins and dimensions + float leftMargin = isBottomRight ? 8 : 11; + float rightMargin = isBottomRight ? 11 : 8; + float borderWidth = textBounds.Width + leftMargin + rightMargin; + float borderHeight = 4 + 16 + 9; // top + text + bottom + + // Position rect + float rectX = isBottomRight ? info.Width - borderWidth + 3 : -3; + float rectY = info.Height - borderHeight + 3; + var rect = SKRect.Create(rectX, rectY, borderWidth, borderHeight); + canvas.DrawRoundRect(rect, BORDER_RADIUS, BORDER_RADIUS, basePaint); + + // Draw text + basePaint.Color = SKColors.White.WithAlpha(OVERLAY_ALPHA); + float textX = isBottomRight ? info.Width - textBounds.Width - rightMargin + 3 : leftMargin - 3; + float textY = rectY + 4 + 16; // top margin + baseline + canvas.DrawText(text, textX, textY, textFont, basePaint); } - private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info, SKPaint paint) + private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info) { int count = ViewModel.Items.Count; int indicatorWidth = 30; // Rectangle Width=30 @@ -307,11 +306,11 @@ private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info, SKPaint p for (int i = 0; i < count; i++) { float opacity = (i == ViewModel.SelectedIndex) ? 1.0f : 0.5f; - paint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); - + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); + int rectX = startX + i * (indicatorWidth + itemSpacing); var rect = SKRect.Create(rectX, y - indicatorHeight / 2, indicatorWidth, indicatorHeight); - canvas.DrawRect(rect, paint); + canvas.DrawRect(rect, basePaint); } } @@ -325,38 +324,37 @@ protected override void OnMouseClick(MouseEventArgs e) var playButtonRect = new Rectangle(Width - 40 - 20, 20, 40, 40); if (playButtonRect.Contains(e.Location)) { - ViewModel.PlayCommand.Execute(null); + ViewModel.TogglePlayPause(); return; } // Check if left arrow area was clicked if (e.X < 80) { - ViewModel.PreviousCommand.Execute(null); + ViewModel.Previous(); return; } // Check if right arrow area was clicked if (e.X > Width - 80) { - ViewModel.NextCommand.Execute(null); + ViewModel.Next(); return; } // Check if download message was clicked - Margin="0,0,0,15", TextBlock Margin="8,6,8,6" if (!string.IsNullOrEmpty(ViewModel.Message)) { - using (var paint = new SKPaint()) + using (var textFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 16)) { - paint.TextSize = 16; var msgBounds = new SKRect(); - paint.MeasureText(ViewModel.Message, ref msgBounds); + textFont.MeasureText(ViewModel.Message, out msgBounds); float msgWidth = msgBounds.Width + 16; // 8+8 float msgHeight = 6 + 16 + 6; // top + text + bottom margin var msgRect = new Rectangle((int)(Width / 2 - msgWidth / 2), Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); if (msgRect.Contains(e.Location)) { - ViewModel.DownloadCommand.Execute(null); + ViewModel.InvokeDownload(); return; } } @@ -418,11 +416,10 @@ protected override void OnMouseMove(MouseEventArgs e) isMouseOverDownload = false; if (!string.IsNullOrEmpty(ViewModel.Message)) { - using (var paint = new SKPaint()) + using (var textFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 16)) { - paint.TextSize = 16; var msgBounds = new SKRect(); - paint.MeasureText(ViewModel.Message, ref msgBounds); + textFont.MeasureText(ViewModel.Message, out msgBounds); float msgWidth = msgBounds.Width + 16; // 8+8 float msgHeight = 6 + 16 + 6; // top + text + bottom margin var msgRect = new Rectangle((int)(Width / 2 - msgWidth / 2), Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); @@ -430,7 +427,7 @@ protected override void OnMouseMove(MouseEventArgs e) } } - needsRedraw = (isMouseOverPlay != wasOverPlay) || (isMouseOverLeft != wasOverLeft) || + needsRedraw = (isMouseOverPlay != wasOverPlay) || (isMouseOverLeft != wasOverLeft) || (isMouseOverRight != wasOverRight) || (isMouseOverDownload != wasOverDownload); bool isOverClickable = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload; diff --git a/src/SkiaSharp/ThemePreviewerViewModel.cs b/src/Skia/ThemePreviewerViewModel.cs similarity index 97% rename from src/SkiaSharp/ThemePreviewerViewModel.cs rename to src/Skia/ThemePreviewerViewModel.cs index 7c245acb..5faacdd8 100644 --- a/src/SkiaSharp/ThemePreviewerViewModel.cs +++ b/src/Skia/ThemePreviewerViewModel.cs @@ -14,7 +14,7 @@ using System.Runtime.CompilerServices; using System.Threading; -namespace WinDynamicDesktop.SkiaSharp +namespace WinDynamicDesktop.Skia { public class ThemePreviewItem { @@ -154,22 +154,21 @@ public int SelectedIndex #endregion - #region Commands + #region Public Methods - public ICommand PlayCommand => new RelayCommand(() => + public void TogglePlayPause() { IsPlaying = !IsPlaying; if (IsPlaying && fadeQueue.IsEmpty) { transitionTimer.Start(); } - }); - - public ICommand PreviousCommand => new RelayCommand(Previous); - - public ICommand NextCommand => new RelayCommand(Next); + } - public ICommand DownloadCommand => new RelayCommand(() => DownloadAction?.Invoke()); + public void InvokeDownload() + { + DownloadAction?.Invoke(); + } #endregion @@ -327,7 +326,7 @@ public void PreviewTheme(ThemeConfig theme, Action downloadAction, Start(activeImage); } - private void Previous() + public void Previous() { if (SelectedIndex == 0) { @@ -339,7 +338,7 @@ private void Previous() } } - private void Next() + public void Next() { if (SelectedIndex == Items.Count - 1) { diff --git a/src/SkiaSharp/RelayCommand.cs b/src/SkiaSharp/RelayCommand.cs deleted file mode 100644 index ab7bf257..00000000 --- a/src/SkiaSharp/RelayCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.ComponentModel; -using System.Runtime.CompilerServices; - -namespace WinDynamicDesktop.SkiaSharp -{ - public class RelayCommand : ICommand - { - private readonly Action execute; - private readonly Func canExecute; - - public event EventHandler CanExecuteChanged; - - public RelayCommand(Action execute, Func canExecute = null) - { - this.execute = execute; - this.canExecute = canExecute; - } - - public bool CanExecute(object parameter) - { - return canExecute?.Invoke() ?? true; - } - - public void Execute(object parameter) - { - execute?.Invoke(); - } - - public void RaiseCanExecuteChanged() - { - CanExecuteChanged?.Invoke(this, EventArgs.Empty); - } - } - - public interface ICommand - { - event EventHandler CanExecuteChanged; - bool CanExecute(object parameter); - void Execute(object parameter); - } -} diff --git a/src/ThemeDialog.Designer.cs b/src/ThemeDialog.Designer.cs index bd37ac4f..4c513ebd 100644 --- a/src/ThemeDialog.Designer.cs +++ b/src/ThemeDialog.Designer.cs @@ -40,7 +40,7 @@ private void InitializeComponent() toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); showInstalledMenuItem = new System.Windows.Forms.ToolStripMenuItem(); displayComboBox = new System.Windows.Forms.ComboBox(); - previewerHost = new WinDynamicDesktop.SkiaSharp.ThemePreviewer(); + previewerHost = new WinDynamicDesktop.Skia.ThemePreviewer(); listView1 = new System.Windows.Forms.ListView(); advancedButton = new System.Windows.Forms.Button(); searchBox = new System.Windows.Forms.TextBox(); @@ -248,7 +248,7 @@ private void InitializeComponent() private System.Windows.Forms.OpenFileDialog openFileDialog1; private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; private System.Windows.Forms.ToolStripMenuItem favoriteThemeMenuItem; - private WinDynamicDesktop.SkiaSharp.ThemePreviewer previewerHost; + private Skia.ThemePreviewer previewerHost; private System.Windows.Forms.ListView listView1; private System.Windows.Forms.ToolStripMenuItem deleteThemeMenuItem; private System.Windows.Forms.ComboBox displayComboBox; From dae9c55023d1fb0632869324bc829404696da0a1 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Tue, 18 Nov 2025 06:41:44 -0500 Subject: [PATCH 03/15] Remove WPF renderer code --- src/WPF/BitmapCache.cs | 99 ------ src/WPF/RelayCommand.cs | 37 --- src/WPF/ThemePreviewer.xaml | 157 ---------- src/WPF/ThemePreviewer.xaml.cs | 54 ---- src/WPF/ThemePreviewerViewModel.cs | 470 ----------------------------- src/WinDynamicDesktop.csproj | 5 +- 6 files changed, 1 insertion(+), 821 deletions(-) delete mode 100644 src/WPF/BitmapCache.cs delete mode 100644 src/WPF/RelayCommand.cs delete mode 100644 src/WPF/ThemePreviewer.xaml delete mode 100644 src/WPF/ThemePreviewer.xaml.cs delete mode 100644 src/WPF/ThemePreviewerViewModel.cs diff --git a/src/WPF/BitmapCache.cs b/src/WPF/BitmapCache.cs deleted file mode 100644 index 309e4c31..00000000 --- a/src/WPF/BitmapCache.cs +++ /dev/null @@ -1,99 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Windows.Forms; -using System.Windows.Media.Imaging; - -namespace WinDynamicDesktop.WPF -{ - sealed class BitmapCache - { - readonly int decodeWidth; - readonly int decodeHeight; - readonly object cacheLock = new object(); - readonly Dictionary images = new Dictionary(); - - public BitmapImage this[Uri uri] - { - get - { - lock (cacheLock) - { - if (images.ContainsKey(uri)) - { - return images[uri]; - } - else - { - var img = CreateImage(uri); - images.Add(uri, img); - return img; - } - } - } - } - - public void Clear() - { - foreach (var uri in images.Keys.ToList()) - { - images.Remove(uri); - } - GC.Collect(); - } - - public BitmapCache(bool limitDecodeSize = true) - { - if (limitDecodeSize) - { - int maxArea = 0; - foreach (Screen screen in Screen.AllScreens) - { - int area = screen.Bounds.Width * screen.Bounds.Height; - if (area > maxArea) - { - maxArea = area; - decodeWidth = screen.Bounds.Width; - decodeHeight = screen.Bounds.Height; - } - } - } - } - - private BitmapImage CreateImage(Uri uri) - { - BitmapImage img = new BitmapImage(); - img.BeginInit(); - img.CacheOption = BitmapCacheOption.OnLoad; - img.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; - - if (uri.IsAbsoluteUri) - { - img.UriSource = uri; - } - else - { - img.StreamSource = Assembly.GetExecutingAssembly().GetManifestResourceStream(uri.OriginalString); - } - - if (decodeWidth >= decodeHeight) - { - img.DecodePixelWidth = decodeWidth; - } - else - { - img.DecodePixelHeight = decodeHeight; - } - - img.EndInit(); - img.StreamSource?.Dispose(); - img.Freeze(); - return img; - } - } -} diff --git a/src/WPF/RelayCommand.cs b/src/WPF/RelayCommand.cs deleted file mode 100644 index e4871d7e..00000000 --- a/src/WPF/RelayCommand.cs +++ /dev/null @@ -1,37 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.Windows.Input; - -namespace WinDynamicDesktop.WPF -{ - public class RelayCommand : ICommand - { - private readonly Action execute; - private readonly Func canExecute; - - public event EventHandler CanExecuteChanged - { - add => CommandManager.RequerySuggested += value; - remove => CommandManager.RequerySuggested -= value; - } - - public RelayCommand(Action execute, Func canExecute = null) - { - this.execute = execute; - this.canExecute = canExecute; - } - - public bool CanExecute(object parameter) - { - return canExecute?.Invoke() ?? true; - } - - public void Execute(object parameter) - { - execute?.Invoke(); - } - } -} diff --git a/src/WPF/ThemePreviewer.xaml b/src/WPF/ThemePreviewer.xaml deleted file mode 100644 index 541a1769..00000000 --- a/src/WPF/ThemePreviewer.xaml +++ /dev/null @@ -1,157 +0,0 @@ - - - pack://application:,,,/resources/fonts/fontawesome-webfont.ttf#FontAwesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/WPF/ThemePreviewer.xaml.cs b/src/WPF/ThemePreviewer.xaml.cs deleted file mode 100644 index 851a5570..00000000 --- a/src/WPF/ThemePreviewer.xaml.cs +++ /dev/null @@ -1,54 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.ComponentModel; -using System.Windows; -using System.Windows.Media.Animation; -using System.Windows.Threading; - -namespace WinDynamicDesktop.WPF -{ - public partial class ThemePreviewer - { - public ThemePreviewerViewModel ViewModel { get; } - - private readonly Storyboard fadeAnimation; - private readonly DispatcherTimer triggerTimer; - - public ThemePreviewer() - { - ViewModel = new ThemePreviewerViewModel(StartAnimation, StopAnimation); - DataContext = ViewModel; - - InitializeComponent(); - - fadeAnimation = FindResource("FadeAnimation") as Storyboard; - fadeAnimation.Completed += (s, e) => ViewModel.OnAnimationComplete(); - - triggerTimer = new DispatcherTimer - { - Interval = TimeSpan.FromSeconds(1.0 / 60) - }; - triggerTimer.Tick += (s, e) => - { - triggerTimer.Stop(); - fadeAnimation.Begin(FrontImage, true); - }; - - DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(IsMouseOverProperty, typeof(UIElement)); - descriptor.AddValueChanged(this, (s, e) => ViewModel.IsMouseOver = IsMouseOver); - } - - private void StartAnimation() - { - triggerTimer.Start(); - } - - private void StopAnimation() - { - fadeAnimation.Stop(FrontImage); - } - } -} diff --git a/src/WPF/ThemePreviewerViewModel.cs b/src/WPF/ThemePreviewerViewModel.cs deleted file mode 100644 index 7dba82f8..00000000 --- a/src/WPF/ThemePreviewerViewModel.cs +++ /dev/null @@ -1,470 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Windows.Input; -using System.Windows.Media.Imaging; -using System.Windows.Threading; - -namespace WinDynamicDesktop.WPF -{ - public class ThemePreviewItem - { - public string PreviewText { get; set; } - public Uri Uri { get; set; } - - public ThemePreviewItem(string previewText, string path) - { - PreviewText = previewText; - - string fullPath = Path.GetFullPath(path); - if (File.Exists(fullPath)) - { - Uri = new Uri(fullPath, UriKind.Absolute); - } - else - { - Uri = new Uri(path, UriKind.Relative); - } - } - } - - public class ThemePreviewerViewModel : INotifyPropertyChanged - { - #region Properties - - public bool ControlsVisible => !string.IsNullOrEmpty(Title); - public bool MessageVisible => !string.IsNullOrEmpty(Message); - public bool CarouselIndicatorsVisible => string.IsNullOrEmpty(Message); - public bool DownloadSizeVisible => !string.IsNullOrEmpty(DownloadSize); - - private string title; - public string Title - { - get => title; - set - { - SetProperty(ref title, value); - OnPropertyChanged(nameof(ControlsVisible)); - } - } - - private string author; - public string Author - { - get => author; - set => SetProperty(ref author, value); - } - - private string previewText; - public string PreviewText - { - get => previewText; - set => SetProperty(ref previewText, value); - } - - private string message; - public string Message - { - get => message; - set - { - SetProperty(ref message, value); - OnPropertyChanged(nameof(MessageVisible)); - OnPropertyChanged(nameof(CarouselIndicatorsVisible)); - } - } - - private Action downloadAction; - public Action DownloadAction - { - get => downloadAction; - set => SetProperty(ref downloadAction, value); - } - - private string downloadSize; - public string DownloadSize - { - get => downloadSize; - set => SetProperty(ref downloadSize, value); - } - - private BitmapImage backImage; - public BitmapImage BackImage - { - get => backImage; - set => SetProperty(ref backImage, value); - } - - private BitmapImage frontImage; - public BitmapImage FrontImage - { - get => frontImage; - set => SetProperty(ref frontImage, value); - } - - private bool isPlaying; - public bool IsPlaying - { - get => isPlaying; - set => SetProperty(ref isPlaying, value); - } - - private bool isMouseOver; - public bool IsMouseOver - { - get => isMouseOver; - set - { - SetProperty(ref isMouseOver, value); - if (value) - { - transitionTimer.Stop(); - } - else if (IsPlaying && fadeQueue.IsEmpty) - { - transitionTimer.Start(); - } - } - } - - private int selectedIndex; - public int SelectedIndex - { - get => selectedIndex; - set - { - if (value != selectedIndex) - { - GoTo(value); - } - SetProperty(ref selectedIndex, value); - } - } - - public ObservableCollection Items { get; } = new ObservableCollection(); - - #endregion - - #region Commands - - public ICommand PlayCommand => new RelayCommand(() => - { - IsPlaying = !IsPlaying; - if (IsPlaying && fadeQueue.IsEmpty) - { - transitionTimer.Start(); - } - }); - - public ICommand PreviousCommand => new RelayCommand(Previous); - - public ICommand NextCommand => new RelayCommand(Next); - - public ICommand DownloadCommand => new RelayCommand(() => DownloadAction?.Invoke()); - - #endregion - - private static readonly Func _ = Localization.GetTranslation; - - private const int TRANSITION_TIME = 5; - - private readonly BitmapCache cache = new BitmapCache(); - private readonly DispatcherTimer transitionTimer; - private readonly ConcurrentQueue fadeQueue = new ConcurrentQueue(); - private readonly SemaphoreSlim fadeSemaphore = new SemaphoreSlim(1, 1); - private readonly Action startAnimation; - private readonly Action stopAnimation; - - public ThemePreviewerViewModel(Action startAnimation, Action stopAnimation) - { - this.startAnimation = startAnimation; - this.stopAnimation = stopAnimation; - - transitionTimer = new DispatcherTimer(DispatcherPriority.Send) - { - Interval = TimeSpan.FromSeconds(TRANSITION_TIME) - }; - transitionTimer.Tick += (s, e) => Next(); - - IsPlaying = true; - } - - public void OnAnimationComplete() - { - BackImage = FrontImage; - FrontImage = null; - - int nextIndex = -1; - while (fadeQueue.TryDequeue(out int index)) - { - nextIndex = index; - } - - if (nextIndex != -1) - { - FrontImage = cache[Items[nextIndex].Uri]; - startAnimation(); - } - else - { - TryRelease(fadeSemaphore); - - if (IsPlaying && !IsMouseOver) - { - transitionTimer.Start(); - } - } - } - - public void PreviewTheme(ThemeConfig theme, Action downloadAction, string imagePath = null) - { - Stop(); - - int activeImage = 0; - string[] sunrise = null; - string[] day = null; - string[] sunset = null; - string[] night = null; - - if (theme != null) - { - DownloadAction = () => downloadAction.Invoke(theme); - Title = ThemeManager.GetThemeName(theme); - Author = ThemeManager.GetThemeAuthor(theme); - bool isDownloaded = ThemeManager.IsThemeDownloaded(theme); - - if (isDownloaded) - { - ThemeManager.CalcThemeInstallSize(theme, size => { DownloadSize = size; }); - - List imageTimes = SolarScheduler.GetAllImageTimes(theme); - activeImage = imageTimes.FindLastIndex((time) => time <= DateTime.Now); - if (activeImage == -1) - { - activeImage = imageTimes.FindLastIndex((time) => time.AddDays(-1) <= DateTime.Now); - } - - if (theme.sunriseImageList != null && !theme.sunriseImageList.SequenceEqual(theme.dayImageList)) - { - sunrise = ImagePaths(theme, theme.sunriseImageList); - AddItems(_("Sunrise"), sunrise, imageTimes.Take(theme.sunriseImageList.Length).ToArray()); - imageTimes.RemoveRange(0, theme.sunriseImageList.Length); - } - - day = ImagePaths(theme, theme.dayImageList); - AddItems(_("Day"), day, imageTimes.Take(theme.dayImageList.Length).ToArray()); - imageTimes.RemoveRange(0, theme.dayImageList.Length); - - if (theme.sunsetImageList != null && !theme.sunsetImageList.SequenceEqual(theme.dayImageList)) - { - sunset = ImagePaths(theme, theme.sunsetImageList); - AddItems(_("Sunset"), sunset, imageTimes.Take(theme.sunsetImageList.Length).ToArray()); - imageTimes.RemoveRange(0, theme.sunsetImageList.Length); - } - - night = ImagePaths(theme, theme.nightImageList); - AddItems(_("Night"), night, imageTimes.Take(theme.nightImageList.Length).ToArray()); - imageTimes.RemoveRange(0, theme.nightImageList.Length); - } - else - { - Message = _("Theme is not downloaded. Click here to download and enable full preview."); - ThemeManager.CalcThemeDownloadSize(theme, size => { DownloadSize = size; }); - - string[] resourceNames = Assembly.GetExecutingAssembly().GetManifestResourceNames(); - string path = "WinDynamicDesktop.resources.images." + theme.themeId + "_{0}.jpg"; - - string rsrcName = string.Format(path, "sunrise"); - if (resourceNames.Contains(rsrcName)) - { - sunrise = new[] { rsrcName }; - } - - rsrcName = string.Format(path, "day"); - if (resourceNames.Contains(rsrcName)) - { - day = new[] { rsrcName }; - } - - rsrcName = string.Format(path, "sunset"); - if (resourceNames.Contains(rsrcName)) - { - sunset = new[] { rsrcName }; - } - - rsrcName = string.Format(path, "night"); - if (resourceNames.Contains(rsrcName)) - { - night = new[] { rsrcName }; - } - - AddItems(_("Sunrise"), sunrise, null); - AddItems(_("Day"), day, null); - AddItems(_("Sunset"), sunset, null); - AddItems(_("Night"), night, null); - - SolarData solarData = SunriseSunsetService.GetSolarData(DateTime.Today); - DaySegmentData segmentData = SolarScheduler.GetDaySegmentData(solarData, DateTime.Now); - activeImage = (sunrise != null && sunset != null) ? segmentData.segment4 : segmentData.segment2; - } - } - else - { - Author = "Microsoft"; - Items.Add(new ThemePreviewItem(string.Empty, imagePath)); - activeImage = 0; - } - - Start(activeImage); - } - - private void Previous() - { - if (SelectedIndex == 0) - { - SelectedIndex = Items.Count - 1; - } - else - { - SelectedIndex--; - } - } - - private void Next() - { - if (SelectedIndex == Items.Count - 1) - { - SelectedIndex = 0; - } - else - { - SelectedIndex++; - } - } - - private void GoTo(int index) - { - if (index < 0 || index >= Items.Count) return; - - transitionTimer.Stop(); - - if (fadeSemaphore.Wait(0)) - { - FrontImage = cache[Items[index].Uri]; - startAnimation(); - } - else - { - fadeQueue.Enqueue(index); - } - - PreviewText = Items[index].PreviewText; - } - - public void Stop() - { - stopAnimation(); - while (fadeQueue.TryDequeue(out int temp)) { } - TryRelease(fadeSemaphore); - - transitionTimer.Stop(); - - Title = null; - Author = null; - PreviewText = null; - Message = null; - DownloadSize = null; - BackImage = null; - FrontImage = null; - SelectedIndex = -1; - - Items.Clear(); - cache.Clear(); - } - - private void AddItems(string previewName, string[] items, DateTime[] imageTimes) - { - if (items == null) return; - - for (int i = 0; i < items.Length; i++) - { - string previewText = previewName; - - if (imageTimes == null) // Theme not downloaded - { - previewText = string.Format(_("Previewing {0}"), previewName); - } - else if (imageTimes[i] == DateTime.MinValue) // Image not active - { - previewText = string.Format(_("Previewing {0} ({1}/{2})"), previewName, i + 1, items.Length); - } - else - { - previewText = string.Format(_("Previewing {0} at {1}"), previewName, imageTimes[i].ToShortTimeString()); - } - - Items.Add(new ThemePreviewItem(previewText, items[i])); - } - } - - private void Start(int index) - { - var item = Items[index]; - - PreviewText = item.PreviewText; - BackImage = cache[item.Uri]; - - selectedIndex = index; - OnPropertyChanged(nameof(SelectedIndex)); - - if (IsPlaying && !IsMouseOver) - { - transitionTimer.Start(); - } - } - - private static string[] ImagePaths(ThemeConfig theme, int[] imageList) - { - string themePath = ThemeManager.GetThemeDirectory(theme); - return imageList.Select(id => - Path.Combine(themePath, theme.imageFilename.Replace("*", id.ToString()))).ToArray(); - } - - private static void TryRelease(SemaphoreSlim semaphore) - { - try - { - semaphore.Release(); - } - catch (SemaphoreFullException) { } - } - - #region INotifyPropertyChanged - - public event PropertyChangedEventHandler PropertyChanged; - - private void SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") - { - field = value; - OnPropertyChanged(propertyName); - } - - private void OnPropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - #endregion - } -} diff --git a/src/WinDynamicDesktop.csproj b/src/WinDynamicDesktop.csproj index 12723f12..73ba17c4 100644 --- a/src/WinDynamicDesktop.csproj +++ b/src/WinDynamicDesktop.csproj @@ -19,11 +19,8 @@ - - - - + From 1b7289d4d4b1c15ca9798e9d163b6693cdafa86c Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 19 Nov 2025 07:06:51 -0500 Subject: [PATCH 04/15] Improve hitbox testing for theme previewer --- src/Skia/ThemePreviewer.cs | 202 +++++++++++++++++++------------------ 1 file changed, 106 insertions(+), 96 deletions(-) diff --git a/src/Skia/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs index fcc62e53..2b411000 100644 --- a/src/Skia/ThemePreviewer.cs +++ b/src/Skia/ThemePreviewer.cs @@ -18,6 +18,7 @@ public class ThemePreviewer : SKControl private const int ANIMATION_FPS = 120; private const int MARGIN_STANDARD = 20; private const int BORDER_RADIUS = 5; + private const int ARROW_AREA_WIDTH = 80; private const byte OVERLAY_ALPHA = 127; private const float OPACITY_NORMAL = 0.5f; private const float OPACITY_HOVER = 1.0f; @@ -25,7 +26,6 @@ public class ThemePreviewer : SKControl public ThemePreviewerViewModel ViewModel { get; } - private readonly Timer animationTimer; private readonly Timer fadeTimer; private float fadeProgress = 0f; private bool isAnimating = false; @@ -41,12 +41,19 @@ public class ThemePreviewer : SKControl private readonly SKFont iconFont20; private readonly SKSamplingOptions samplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); - private Point mousePosition; private bool isMouseOverPlay = false; private bool isMouseOverLeft = false; private bool isMouseOverRight = false; private bool isMouseOverDownload = false; + // Cached UI element rectangles for rendering and hit testing + private Rectangle titleBoxRect; + private Rectangle playButtonRect; + private Rectangle downloadMessageRect; + private Rectangle authorLabelRect; + private Rectangle downloadSizeLabelRect; + private Rectangle[] carouselIndicatorRects; + public ThemePreviewer() { ViewModel = new ThemePreviewerViewModel(StartAnimation, StopAnimation); @@ -76,12 +83,6 @@ public ThemePreviewer() }; fadeTimer.Tick += FadeTimer_Tick; - // Timer for auto-advance - animationTimer = new Timer - { - Interval = 1000 / 60 - }; - MouseEnter += (s, e) => ViewModel.IsMouseOver = true; MouseLeave += (s, e) => ViewModel.IsMouseOver = false; @@ -182,65 +183,61 @@ private void DrawOverlay(SKCanvas canvas, SKImageInfo info) DrawArrowArea(canvas, info, false); } - // Title and preview text box (top left) - Border with Margin=20, StackPanel with Margin=10 - basePaint.Color = new SKColor(0, 0, 0, 127); - - // Measure text to calculate proper box size + // Title and preview text box (top left) var titleBounds = new SKRect(); titleFont.MeasureText(ViewModel.Title ?? "", out titleBounds); - var previewBounds = new SKRect(); previewFont.MeasureText(ViewModel.PreviewText ?? "", out previewBounds); - float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + 20; // 10 margin each side - float boxHeight = 19 + 4 + 16 + 20; // title size + margin + preview size + top/bottom margin + float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + 20; + float boxHeight = 19 + 4 + 16 + 20; + titleBoxRect = new Rectangle(20, 20, (int)boxWidth, (int)boxHeight); - var titleRect = SKRect.Create(20, 20, boxWidth, boxHeight); basePaint.Color = new SKColor(0, 0, 0, 127); - canvas.DrawRoundRect(titleRect, 5, 5, basePaint); + canvas.DrawRoundRect(SKRect.Create(titleBoxRect.X, titleBoxRect.Y, titleBoxRect.Width, titleBoxRect.Height), 5, 5, basePaint); - // Title text - 10px margin from border basePaint.Color = SKColors.White; - canvas.DrawText(ViewModel.Title ?? "", 30, 20 + 10 + 19, titleFont, basePaint); // margin + top padding + font size - - // Preview text - 4px below title, 16px font - canvas.DrawText(ViewModel.PreviewText ?? "", 30, 20 + 10 + 19 + 4 + 16, previewFont, basePaint); // add 4px margin + 16px for text + canvas.DrawText(ViewModel.Title ?? "", titleBoxRect.X + 10, titleBoxRect.Y + 8 + 19, titleFont, basePaint); + canvas.DrawText(ViewModel.PreviewText ?? "", titleBoxRect.X + 10, titleBoxRect.Y + 8 + 19 + 5 + 16, previewFont, basePaint); - // Play/Pause button (top right) - MinWidth=40, MinHeight=40, Margin=20 + // Play/Pause button (top right) + playButtonRect = new Rectangle(info.Width - 40 - 20, 20, 40, 40); + basePaint.Color = new SKColor(0, 0, 0, 127); - var playButtonRect = SKRect.Create(info.Width - 40 - 20, 20, 40, 40); - canvas.DrawRoundRect(playButtonRect, 5, 5, basePaint); + canvas.DrawRoundRect(SKRect.Create(playButtonRect.X, playButtonRect.Y, playButtonRect.Width, playButtonRect.Height), 5, 5, basePaint); float playOpacity = isMouseOverPlay ? 1.0f : 0.5f; basePaint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); string playIcon = ViewModel.IsPlaying ? "\uf04c" : "\uf04b"; var textBounds = new SKRect(); iconFont16.MeasureText(playIcon, out textBounds); - float centerX = info.Width - 20 - 20; - float centerY = 20 + 20; + float centerX = playButtonRect.X + playButtonRect.Width / 2; + float centerY = playButtonRect.Y + playButtonRect.Height / 2; canvas.DrawText(playIcon, centerX - textBounds.MidX, centerY - textBounds.MidY, iconFont16, basePaint); // Corner labels DrawCornerLabel(canvas, info, ViewModel.Author, isBottomRight: true); DrawCornerLabel(canvas, info, ViewModel.DownloadSize, isBottomRight: false); - // Download message (centered bottom) - Margin="0,0,0,15", TextBlock Margin="8,6,8,6" + // Download message (centered bottom) if (!string.IsNullOrEmpty(ViewModel.Message)) { var msgBounds = new SKRect(); textFont.MeasureText(ViewModel.Message, out msgBounds); - // TextBlock margin: 8,6,8,6 = left+right=16, top+bottom=12 float msgWidth = msgBounds.Width + 16; - float msgHeight = 6 + 16 + 6; // top margin + text height + bottom margin - var msgRect = SKRect.Create(info.Width / 2 - msgWidth / 2, info.Height - msgHeight - 15, msgWidth, msgHeight); + float msgHeight = 6 + 16 + 6; + downloadMessageRect = new Rectangle((int)(info.Width / 2 - msgWidth / 2), info.Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); basePaint.Color = new SKColor(0, 0, 0, 127); - canvas.DrawRoundRect(msgRect, 5, 5, basePaint); + canvas.DrawRoundRect(SKRect.Create(downloadMessageRect.X, downloadMessageRect.Y, downloadMessageRect.Width, downloadMessageRect.Height), 5, 5, basePaint); float msgOpacity = isMouseOverDownload ? 1.0f : 0.8f; basePaint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); - // Text positioned: border top + top margin + font baseline - canvas.DrawText(ViewModel.Message, info.Width / 2 - msgBounds.Width / 2, info.Height - msgHeight - 15 + 6 + 16, textFont, basePaint); + canvas.DrawText(ViewModel.Message, downloadMessageRect.X + 8, downloadMessageRect.Y + 5 + 16, textFont, basePaint); + } + else + { + downloadMessageRect = Rectangle.Empty; } // Carousel indicators - Margin=16, Height=32, Rectangle Height=3, Width=30, Margin="3,0" @@ -268,9 +265,15 @@ private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft) private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, bool isBottomRight) { - if (string.IsNullOrEmpty(text)) return; + if (string.IsNullOrEmpty(text)) + { + if (isBottomRight) + authorLabelRect = Rectangle.Empty; + else + downloadSizeLabelRect = Rectangle.Empty; + return; + } - basePaint.Color = new SKColor(0, 0, 0, OVERLAY_ALPHA); var textBounds = new SKRect(); textFont.MeasureText(text, out textBounds); @@ -278,30 +281,40 @@ private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, boo float leftMargin = isBottomRight ? 8 : 11; float rightMargin = isBottomRight ? 11 : 8; float borderWidth = textBounds.Width + leftMargin + rightMargin; - float borderHeight = 4 + 16 + 9; // top + text + bottom + float borderHeight = 4 + 16 + 9; - // Position rect + // Position rect and cache it float rectX = isBottomRight ? info.Width - borderWidth + 3 : -3; float rectY = info.Height - borderHeight + 3; - var rect = SKRect.Create(rectX, rectY, borderWidth, borderHeight); - canvas.DrawRoundRect(rect, BORDER_RADIUS, BORDER_RADIUS, basePaint); + Rectangle labelRect = new Rectangle((int)rectX, (int)rectY, (int)borderWidth, (int)borderHeight); + + if (isBottomRight) + authorLabelRect = labelRect; + else + downloadSizeLabelRect = labelRect; + + basePaint.Color = new SKColor(0, 0, 0, OVERLAY_ALPHA); + canvas.DrawRoundRect(SKRect.Create(rectX, rectY, borderWidth, borderHeight), BORDER_RADIUS, BORDER_RADIUS, basePaint); // Draw text basePaint.Color = SKColors.White.WithAlpha(OVERLAY_ALPHA); float textX = isBottomRight ? info.Width - textBounds.Width - rightMargin + 3 : leftMargin - 3; - float textY = rectY + 4 + 16; // top margin + baseline + float textY = rectY + 4 + 16; canvas.DrawText(text, textX, textY, textFont, basePaint); } private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info) { int count = ViewModel.Items.Count; - int indicatorWidth = 30; // Rectangle Width=30 - int indicatorHeight = 3; // Rectangle Height=3 - int itemSpacing = 6; // Margin="3,0" means 3px on each side = 6px spacing + int indicatorWidth = 30; + int indicatorHeight = 3; + int itemSpacing = 6; int totalWidth = count * indicatorWidth + (count - 1) * itemSpacing; int startX = (info.Width - totalWidth) / 2; - int y = info.Height - 16 - 32 / 2; // Margin=16 from bottom, Height=32, centered vertically + int y = info.Height - 16 - 32 / 2; + + // Cache clickable rectangles for hit testing + carouselIndicatorRects = new Rectangle[count]; for (int i = 0; i < count; i++) { @@ -311,6 +324,9 @@ private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info) int rectX = startX + i * (indicatorWidth + itemSpacing); var rect = SKRect.Create(rectX, y - indicatorHeight / 2, indicatorWidth, indicatorHeight); canvas.DrawRect(rect, basePaint); + + // Cache full clickable height for hit testing + carouselIndicatorRects[i] = new Rectangle(rectX, y - 16, indicatorWidth, 32); } } @@ -320,8 +336,7 @@ protected override void OnMouseClick(MouseEventArgs e) if (!ViewModel.ControlsVisible) return; - // Check if play button was clicked - MinWidth=40, MinHeight=40, Margin=20 - var playButtonRect = new Rectangle(Width - 40 - 20, 20, 40, 40); + // Check if play button was clicked if (playButtonRect.Contains(e.Location)) { ViewModel.TogglePlayPause(); @@ -329,52 +344,32 @@ protected override void OnMouseClick(MouseEventArgs e) } // Check if left arrow area was clicked - if (e.X < 80) + if (e.X < ARROW_AREA_WIDTH) { ViewModel.Previous(); return; } // Check if right arrow area was clicked - if (e.X > Width - 80) + if (e.X > Width - ARROW_AREA_WIDTH) { ViewModel.Next(); return; } - // Check if download message was clicked - Margin="0,0,0,15", TextBlock Margin="8,6,8,6" - if (!string.IsNullOrEmpty(ViewModel.Message)) + // Check if download message was clicked + if (downloadMessageRect.Contains(e.Location)) { - using (var textFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 16)) - { - var msgBounds = new SKRect(); - textFont.MeasureText(ViewModel.Message, out msgBounds); - float msgWidth = msgBounds.Width + 16; // 8+8 - float msgHeight = 6 + 16 + 6; // top + text + bottom margin - var msgRect = new Rectangle((int)(Width / 2 - msgWidth / 2), Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); - if (msgRect.Contains(e.Location)) - { - ViewModel.InvokeDownload(); - return; - } - } + ViewModel.InvokeDownload(); + return; } - // Check if carousel indicator was clicked - Margin=16, Height=32 - if (ViewModel.CarouselIndicatorsVisible && ViewModel.Items.Count > 0) + // Check if carousel indicator was clicked + if (carouselIndicatorRects != null) { - int count = ViewModel.Items.Count; - int indicatorWidth = 30; - int itemSpacing = 6; - int totalWidth = count * indicatorWidth + (count - 1) * itemSpacing; - int startX = (Width - totalWidth) / 2; - int y = Height - 16 - 32 / 2; - - for (int i = 0; i < count; i++) + for (int i = 0; i < carouselIndicatorRects.Length; i++) { - int rectX = startX + i * (indicatorWidth + itemSpacing); - var rect = new Rectangle(rectX, y - 16, indicatorWidth, 32); // Full clickable height - if (rect.Contains(e.Location)) + if (carouselIndicatorRects[i].Contains(e.Location)) { ViewModel.SelectedIndex = i; return; @@ -387,7 +382,6 @@ protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); - mousePosition = e.Location; bool needsRedraw = false; // Update cursor based on location @@ -402,35 +396,52 @@ protected override void OnMouseMove(MouseEventArgs e) bool wasOverRight = isMouseOverRight; bool wasOverDownload = isMouseOverDownload; - // Play button - MinWidth=40, MinHeight=40, Margin=20 - var playButtonRect = new Rectangle(Width - 40 - 20, 20, 40, 40); + // Reset all hover states + isMouseOverPlay = false; + isMouseOverLeft = false; + isMouseOverRight = false; + isMouseOverDownload = false; + + // Check UI elements in priority order using cached rectangles isMouseOverPlay = playButtonRect.Contains(e.Location); + + if (!isMouseOverPlay) + isMouseOverDownload = downloadMessageRect.Contains(e.Location); - // Left arrow (80px wide area on left side, full height) - isMouseOverLeft = e.X < 80; + // Check if over any UI box (title, corner labels) + bool isOverUIBox = false; + if (!isMouseOverPlay && !isMouseOverDownload) + { + isOverUIBox = titleBoxRect.Contains(e.Location) || + authorLabelRect.Contains(e.Location) || + downloadSizeLabelRect.Contains(e.Location); + } - // Right arrow (80px wide area on right side, full height) - isMouseOverRight = e.X > Width - 80; + // Arrows only if not over other UI elements + if (!isMouseOverPlay && !isMouseOverDownload && !isOverUIBox) + { + isMouseOverLeft = e.X < ARROW_AREA_WIDTH; + isMouseOverRight = e.X > Width - ARROW_AREA_WIDTH; + } - // Download message - isMouseOverDownload = false; - if (!string.IsNullOrEmpty(ViewModel.Message)) + // Check carousel indicators for hand cursor + bool isOverCarouselIndicator = false; + if (carouselIndicatorRects != null) { - using (var textFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 16)) + for (int i = 0; i < carouselIndicatorRects.Length; i++) { - var msgBounds = new SKRect(); - textFont.MeasureText(ViewModel.Message, out msgBounds); - float msgWidth = msgBounds.Width + 16; // 8+8 - float msgHeight = 6 + 16 + 6; // top + text + bottom margin - var msgRect = new Rectangle((int)(Width / 2 - msgWidth / 2), Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); - isMouseOverDownload = msgRect.Contains(e.Location); + if (carouselIndicatorRects[i].Contains(e.Location)) + { + isOverCarouselIndicator = true; + break; + } } } needsRedraw = (isMouseOverPlay != wasOverPlay) || (isMouseOverLeft != wasOverLeft) || (isMouseOverRight != wasOverRight) || (isMouseOverDownload != wasOverDownload); - bool isOverClickable = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload; + bool isOverClickable = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload || isOverCarouselIndicator; Cursor = isOverClickable ? Cursors.Hand : Cursors.Default; if (needsRedraw) @@ -478,7 +489,6 @@ protected override void Dispose(bool disposing) if (disposing) { fadeTimer?.Dispose(); - animationTimer?.Dispose(); ViewModel?.Stop(); } base.Dispose(disposing); From abd58caf59c1c0a47b051768c95dae7151f5231c Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 19 Nov 2025 07:30:18 -0500 Subject: [PATCH 05/15] Cache images rather than bitmaps --- src/Skia/{BitmapCache.cs => ImageCache.cs} | 51 ++++++++++++---------- src/Skia/ThemePreviewer.cs | 12 ++--- src/Skia/ThemePreviewerViewModel.cs | 10 ++--- 3 files changed, 37 insertions(+), 36 deletions(-) rename src/Skia/{BitmapCache.cs => ImageCache.cs} (69%) diff --git a/src/Skia/BitmapCache.cs b/src/Skia/ImageCache.cs similarity index 69% rename from src/Skia/BitmapCache.cs rename to src/Skia/ImageCache.cs index c3a48d9b..1e25b12d 100644 --- a/src/Skia/BitmapCache.cs +++ b/src/Skia/ImageCache.cs @@ -12,14 +12,14 @@ namespace WinDynamicDesktop.Skia { - sealed class BitmapCache + sealed class ImageCache { readonly int maxWidth; readonly int maxHeight; readonly object cacheLock = new object(); - readonly Dictionary images = new Dictionary(); + readonly Dictionary images = new Dictionary(); - public SKBitmap this[Uri uri] + public SKImage this[Uri uri] { get { @@ -46,16 +46,16 @@ public void Clear() { lock (cacheLock) { - foreach (var bitmap in images.Values) + foreach (var image in images.Values) { - bitmap?.Dispose(); + image?.Dispose(); } images.Clear(); } GC.Collect(); } - public BitmapCache(bool limitDecodeSize = true) + public ImageCache(bool limitDecodeSize = true) { if (limitDecodeSize) { @@ -78,7 +78,7 @@ public BitmapCache(bool limitDecodeSize = true) } } - private SKBitmap CreateImage(Uri uri) + private SKImage CreateImage(Uri uri) { try { @@ -114,7 +114,7 @@ private SKBitmap CreateImage(Uri uri) var info = codec.Info; - // Calculate scaled dimensions + // Calculate target dimensions int targetWidth = info.Width; int targetHeight = info.Height; @@ -125,24 +125,31 @@ private SKBitmap CreateImage(Uri uri) targetHeight = (int)(info.Height * scale); } - var bitmap = new SKBitmap(targetWidth, targetHeight, info.ColorType, info.AlphaType); - - if (targetWidth == info.Width && targetHeight == info.Height) - { - // No scaling needed - codec.GetPixels(bitmap.Info, bitmap.GetPixels()); - } - else + // Decode at native size + using (var sourceBitmap = new SKBitmap(info)) { - // Decode at full size then scale down with high quality - using (var fullBitmap = new SKBitmap(info)) + if (codec.GetPixels(sourceBitmap.Info, sourceBitmap.GetPixels()) != SKCodecResult.Success) { - codec.GetPixels(fullBitmap.Info, fullBitmap.GetPixels()); - fullBitmap.ScalePixels(bitmap, new SKSamplingOptions(SKCubicResampler.Mitchell)); + return null; + } + + // If scaling is needed, create scaled version with high quality + if (targetWidth != sourceBitmap.Width || targetHeight != sourceBitmap.Height) + { + using (var scaledBitmap = new SKBitmap(targetWidth, targetHeight, info.ColorType, info.AlphaType)) + { + sourceBitmap.ScalePixels(scaledBitmap, new SKSamplingOptions(SKCubicResampler.Mitchell)); + var image = SKImage.FromBitmap(scaledBitmap); + return image; + } + } + else + { + // No scaling needed + var image = SKImage.FromBitmap(sourceBitmap); + return image; } } - - return bitmap; } } } diff --git a/src/Skia/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs index 2b411000..7aa1b9a9 100644 --- a/src/Skia/ThemePreviewer.cs +++ b/src/Skia/ThemePreviewer.cs @@ -142,17 +142,14 @@ protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) } } - private void DrawImage(SKCanvas canvas, SKBitmap bitmap, SKImageInfo info, float opacity) + private void DrawImage(SKCanvas canvas, SKImage image, SKImageInfo info, float opacity) { var destRect = new SKRect(0, 0, info.Width, info.Height); if (opacity >= 1.0f) { // Fast path for fully opaque images - using (var image = SKImage.FromBitmap(bitmap)) - { - canvas.DrawImage(image, destRect, samplingOptions, null); - } + canvas.DrawImage(image, destRect, samplingOptions, null); } else { @@ -164,10 +161,7 @@ private void DrawImage(SKCanvas canvas, SKBitmap bitmap, SKImageInfo info, float SKColors.White.WithAlpha((byte)(255 * opacity)), SKBlendMode.DstIn); - using (var image = SKImage.FromBitmap(bitmap)) - { - canvas.DrawImage(image, destRect, samplingOptions, paint); - } + canvas.DrawImage(image, destRect, samplingOptions, paint); } } } diff --git a/src/Skia/ThemePreviewerViewModel.cs b/src/Skia/ThemePreviewerViewModel.cs index 5faacdd8..17eecf0c 100644 --- a/src/Skia/ThemePreviewerViewModel.cs +++ b/src/Skia/ThemePreviewerViewModel.cs @@ -97,15 +97,15 @@ public string DownloadSize set => SetProperty(ref downloadSize, value); } - private SKBitmap backImage; - public SKBitmap BackImage + private SKImage backImage; + public SKImage BackImage { get => backImage; set => SetProperty(ref backImage, value); } - private SKBitmap frontImage; - public SKBitmap FrontImage + private SKImage frontImage; + public SKImage FrontImage { get => frontImage; set => SetProperty(ref frontImage, value); @@ -176,7 +176,7 @@ public void InvokeDownload() private const int TRANSITION_TIME = 5; - private readonly BitmapCache cache = new BitmapCache(); + private readonly ImageCache cache = new ImageCache(); private readonly System.Windows.Forms.Timer transitionTimer; private readonly ConcurrentQueue fadeQueue = new ConcurrentQueue(); private readonly SemaphoreSlim fadeSemaphore = new SemaphoreSlim(1, 1); From 960a9fea7225d5bd5179948e45bca3b91245612d Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 19 Nov 2025 17:58:47 -0500 Subject: [PATCH 06/15] Remove unnecessary OpenGL DLLs from publish output --- src/WinDynamicDesktop.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/WinDynamicDesktop.csproj b/src/WinDynamicDesktop.csproj index 73ba17c4..d1708d8c 100644 --- a/src/WinDynamicDesktop.csproj +++ b/src/WinDynamicDesktop.csproj @@ -58,4 +58,10 @@ + + + + + + \ No newline at end of file From 54d5a49a01f52fb1cd551425cfdab74fd125cc64 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 19 Nov 2025 18:40:15 -0500 Subject: [PATCH 07/15] Try another method to remove OpenGL DLLs --- src/WinDynamicDesktop.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/WinDynamicDesktop.csproj b/src/WinDynamicDesktop.csproj index d1708d8c..afc72737 100644 --- a/src/WinDynamicDesktop.csproj +++ b/src/WinDynamicDesktop.csproj @@ -58,10 +58,10 @@ - + - - + + - \ No newline at end of file + From 42f6c848b00fa35cb57cf447881188e4fd84bdc0 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Fri, 21 Nov 2025 21:24:11 -0500 Subject: [PATCH 08/15] Update CodeQL workflow to include .NET setup --- .github/workflows/codeql.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0bbd7f92..57091ec1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,6 +30,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: @@ -44,3 +49,4 @@ jobs: uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" + From 522f679c29636a1c34caafc4b53026d2eb78cff6 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Fri, 21 Nov 2025 21:34:26 -0500 Subject: [PATCH 09/15] Update CodeQL workflow to include security queries Added security-extended queries to CodeQL initialization. --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 57091ec1..45f43289 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -39,6 +39,7 @@ jobs: uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} + queries: +security-extended - name: Autobuild run: | @@ -49,4 +50,3 @@ jobs: uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" - From e211b45a297ba9c426c1a00d3c4dadf7bfb32b61 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Sat, 22 Nov 2025 14:28:39 -0500 Subject: [PATCH 10/15] Refactor Skia code to use constants and dispose resources --- src/Skia/ThemePreviewer.cs | 66 +++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Skia/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs index 7aa1b9a9..74497529 100644 --- a/src/Skia/ThemePreviewer.cs +++ b/src/Skia/ThemePreviewer.cs @@ -34,6 +34,7 @@ public class ThemePreviewer : SKControl // Cached objects to reduce allocations private readonly SKPaint basePaint = new SKPaint { IsAntialias = true }; + private readonly SKColor overlayColor = new SKColor(0, 0, 0, OVERLAY_ALPHA); private readonly SKFont titleFont; private readonly SKFont previewFont; private readonly SKFont textFont; @@ -168,14 +169,9 @@ private void DrawImage(SKCanvas canvas, SKImage image, SKImageInfo info, float o private void DrawOverlay(SKCanvas canvas, SKImageInfo info) { - basePaint.Style = SKPaintStyle.Fill; - // Draw left and right arrow button areas - if (ViewModel.ControlsVisible) - { - DrawArrowArea(canvas, info, true); - DrawArrowArea(canvas, info, false); - } + DrawArrowArea(canvas, info, true); + DrawArrowArea(canvas, info, false); // Title and preview text box (top left) var titleBounds = new SKRect(); @@ -183,24 +179,25 @@ private void DrawOverlay(SKCanvas canvas, SKImageInfo info) var previewBounds = new SKRect(); previewFont.MeasureText(ViewModel.PreviewText ?? "", out previewBounds); - float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + 20; - float boxHeight = 19 + 4 + 16 + 20; - titleBoxRect = new Rectangle(20, 20, (int)boxWidth, (int)boxHeight); + float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + MARGIN_STANDARD; + float boxHeight = 19 + 4 + 16 + MARGIN_STANDARD; + titleBoxRect = new Rectangle(MARGIN_STANDARD, MARGIN_STANDARD, (int)boxWidth, (int)boxHeight); - basePaint.Color = new SKColor(0, 0, 0, 127); - canvas.DrawRoundRect(SKRect.Create(titleBoxRect.X, titleBoxRect.Y, titleBoxRect.Width, titleBoxRect.Height), 5, 5, basePaint); + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(titleBoxRect.X, titleBoxRect.Y, titleBoxRect.Width, titleBoxRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); basePaint.Color = SKColors.White; canvas.DrawText(ViewModel.Title ?? "", titleBoxRect.X + 10, titleBoxRect.Y + 8 + 19, titleFont, basePaint); canvas.DrawText(ViewModel.PreviewText ?? "", titleBoxRect.X + 10, titleBoxRect.Y + 8 + 19 + 5 + 16, previewFont, basePaint); // Play/Pause button (top right) - playButtonRect = new Rectangle(info.Width - 40 - 20, 20, 40, 40); - - basePaint.Color = new SKColor(0, 0, 0, 127); - canvas.DrawRoundRect(SKRect.Create(playButtonRect.X, playButtonRect.Y, playButtonRect.Width, playButtonRect.Height), 5, 5, basePaint); + int playButtonSize = 40; + playButtonRect = new Rectangle(info.Width - playButtonSize - MARGIN_STANDARD, MARGIN_STANDARD, playButtonSize, playButtonSize); - float playOpacity = isMouseOverPlay ? 1.0f : 0.5f; + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(playButtonRect.X, playButtonRect.Y, playButtonRect.Width, playButtonRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + float playOpacity = isMouseOverPlay ? OPACITY_HOVER : OPACITY_NORMAL; basePaint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); string playIcon = ViewModel.IsPlaying ? "\uf04c" : "\uf04b"; var textBounds = new SKRect(); @@ -222,10 +219,10 @@ private void DrawOverlay(SKCanvas canvas, SKImageInfo info) float msgHeight = 6 + 16 + 6; downloadMessageRect = new Rectangle((int)(info.Width / 2 - msgWidth / 2), info.Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); - basePaint.Color = new SKColor(0, 0, 0, 127); - canvas.DrawRoundRect(SKRect.Create(downloadMessageRect.X, downloadMessageRect.Y, downloadMessageRect.Width, downloadMessageRect.Height), 5, 5, basePaint); + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(downloadMessageRect.X, downloadMessageRect.Y, downloadMessageRect.Width, downloadMessageRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); - float msgOpacity = isMouseOverDownload ? 1.0f : 0.8f; + float msgOpacity = isMouseOverDownload ? OPACITY_HOVER : OPACITY_MESSAGE; basePaint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); canvas.DrawText(ViewModel.Message, downloadMessageRect.X + 8, downloadMessageRect.Y + 5 + 16, textFont, basePaint); } @@ -244,7 +241,7 @@ private void DrawOverlay(SKCanvas canvas, SKImageInfo info) private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft) { bool isHovered = isLeft ? isMouseOverLeft : isMouseOverRight; - float opacity = isHovered ? 1.0f : 0.5f; + float opacity = isHovered ? OPACITY_HOVER : OPACITY_NORMAL; float x = isLeft ? 40 : info.Width - 40; float y = info.Height / 2; @@ -271,26 +268,23 @@ private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, boo var textBounds = new SKRect(); textFont.MeasureText(text, out textBounds); - // Calculate margins and dimensions float leftMargin = isBottomRight ? 8 : 11; float rightMargin = isBottomRight ? 11 : 8; float borderWidth = textBounds.Width + leftMargin + rightMargin; float borderHeight = 4 + 16 + 9; - // Position rect and cache it float rectX = isBottomRight ? info.Width - borderWidth + 3 : -3; float rectY = info.Height - borderHeight + 3; Rectangle labelRect = new Rectangle((int)rectX, (int)rectY, (int)borderWidth, (int)borderHeight); - + if (isBottomRight) authorLabelRect = labelRect; else downloadSizeLabelRect = labelRect; - basePaint.Color = new SKColor(0, 0, 0, OVERLAY_ALPHA); + basePaint.Color = overlayColor; canvas.DrawRoundRect(SKRect.Create(rectX, rectY, borderWidth, borderHeight), BORDER_RADIUS, BORDER_RADIUS, basePaint); - // Draw text basePaint.Color = SKColors.White.WithAlpha(OVERLAY_ALPHA); float textX = isBottomRight ? info.Width - textBounds.Width - rightMargin + 3 : leftMargin - 3; float textY = rectY + 4 + 16; @@ -305,21 +299,19 @@ private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info) int itemSpacing = 6; int totalWidth = count * indicatorWidth + (count - 1) * itemSpacing; int startX = (info.Width - totalWidth) / 2; - int y = info.Height - 16 - 32 / 2; + int y = info.Height - 16 - 16; // bottom margin - half of clickable height - // Cache clickable rectangles for hit testing carouselIndicatorRects = new Rectangle[count]; for (int i = 0; i < count; i++) { - float opacity = (i == ViewModel.SelectedIndex) ? 1.0f : 0.5f; + float opacity = (i == ViewModel.SelectedIndex) ? OPACITY_HOVER : OPACITY_NORMAL; basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); int rectX = startX + i * (indicatorWidth + itemSpacing); - var rect = SKRect.Create(rectX, y - indicatorHeight / 2, indicatorWidth, indicatorHeight); - canvas.DrawRect(rect, basePaint); + canvas.DrawRect(rectX, y - indicatorHeight / 2, indicatorWidth, indicatorHeight, basePaint); - // Cache full clickable height for hit testing + // Cache full clickable area for hit testing carouselIndicatorRects[i] = new Rectangle(rectX, y - 16, indicatorWidth, 32); } } @@ -398,7 +390,7 @@ protected override void OnMouseMove(MouseEventArgs e) // Check UI elements in priority order using cached rectangles isMouseOverPlay = playButtonRect.Contains(e.Location); - + if (!isMouseOverPlay) isMouseOverDownload = downloadMessageRect.Contains(e.Location); @@ -484,6 +476,14 @@ protected override void Dispose(bool disposing) { fadeTimer?.Dispose(); ViewModel?.Stop(); + + // Dispose cached SkiaSharp objects + basePaint?.Dispose(); + titleFont?.Dispose(); + previewFont?.Dispose(); + textFont?.Dispose(); + iconFont16?.Dispose(); + iconFont20?.Dispose(); } base.Dispose(disposing); } From 72902bd7eef0871a0342dab4e22049b4a09edc93 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 26 Nov 2025 21:12:13 -0500 Subject: [PATCH 11/15] Handle mouse leave event for theme previewer --- src/Skia/ThemePreviewer.cs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Skia/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs index 74497529..928d42a2 100644 --- a/src/Skia/ThemePreviewer.cs +++ b/src/Skia/ThemePreviewer.cs @@ -85,7 +85,7 @@ public ThemePreviewer() fadeTimer.Tick += FadeTimer_Tick; MouseEnter += (s, e) => ViewModel.IsMouseOver = true; - MouseLeave += (s, e) => ViewModel.IsMouseOver = false; + MouseLeave += OnMouseLeave; ViewModel.PropertyChanged += (s, e) => { @@ -436,6 +436,26 @@ protected override void OnMouseMove(MouseEventArgs e) } } + private void OnMouseLeave(object sender, EventArgs e) + { + ViewModel.IsMouseOver = false; + + // Reset all hover states + bool needsRedraw = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload; + + isMouseOverPlay = false; + isMouseOverLeft = false; + isMouseOverRight = false; + isMouseOverDownload = false; + + Cursor = Cursors.Default; + + if (needsRedraw) + { + Invalidate(); + } + } + private void StartAnimation() { fadeProgress = 0f; From ba21f1fc5005652e8d1fdfb24b76df39c00618c7 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Fri, 28 Nov 2025 22:37:02 -0500 Subject: [PATCH 12/15] Extract rendering code into ThemePreviewRenderer --- src/Skia/ThemePreviewRenderer.cs | 236 ++++++++++++++++++++++ src/Skia/ThemePreviewer.cs | 322 +++++-------------------------- 2 files changed, 282 insertions(+), 276 deletions(-) create mode 100644 src/Skia/ThemePreviewRenderer.cs diff --git a/src/Skia/ThemePreviewRenderer.cs b/src/Skia/ThemePreviewRenderer.cs new file mode 100644 index 00000000..cf2b49fb --- /dev/null +++ b/src/Skia/ThemePreviewRenderer.cs @@ -0,0 +1,236 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using SkiaSharp; +using System; +using System.Drawing; + +namespace WinDynamicDesktop.Skia +{ + internal class ThemePreviewRenderer + { + private const int MARGIN_STANDARD = 20; + private const int BORDER_RADIUS = 5; + private const int ARROW_AREA_WIDTH = 80; + private const byte OVERLAY_ALPHA = 127; + private const float OPACITY_NORMAL = 0.5f; + private const float OPACITY_HOVER = 1.0f; + private const float OPACITY_MESSAGE = 0.8f; + + private readonly SKPaint basePaint; + private readonly SKColor overlayColor; + private readonly SKFont titleFont; + private readonly SKFont previewFont; + private readonly SKFont textFont; + private readonly SKFont iconFont16; + private readonly SKFont iconFont20; + private readonly SKSamplingOptions samplingOptions; + + // Hit test regions (updated during rendering) + public Rectangle TitleBoxRect { get; private set; } + public Rectangle PlayButtonRect { get; private set; } + public Rectangle DownloadMessageRect { get; private set; } + public Rectangle AuthorLabelRect { get; private set; } + public Rectangle DownloadSizeLabelRect { get; private set; } + public Rectangle LeftArrowRect { get; private set; } + public Rectangle RightArrowRect { get; private set; } + public Rectangle[] CarouselIndicatorRects { get; private set; } + + public ThemePreviewRenderer(SKTypeface fontAwesome) + { + basePaint = new SKPaint { IsAntialias = true }; + overlayColor = new SKColor(0, 0, 0, OVERLAY_ALPHA); + titleFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 19); + previewFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 16); + textFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 16); + iconFont16 = new SKFont(fontAwesome, 16); + iconFont20 = new SKFont(fontAwesome, 20); + samplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); + } + + public void DrawImage(SKCanvas canvas, SKImage image, SKImageInfo info, float opacity) + { + var destRect = new SKRect(0, 0, info.Width, info.Height); + + if (opacity >= 1.0f) + { + // Fast path for fully opaque images + canvas.DrawImage(image, destRect, samplingOptions, null); + } + else + { + // Apply opacity with color filter + using (var paint = new SKPaint()) + { + paint.IsAntialias = true; + paint.ColorFilter = SKColorFilter.CreateBlendMode( + SKColors.White.WithAlpha((byte)(255 * opacity)), + SKBlendMode.DstIn); + + canvas.DrawImage(image, destRect, samplingOptions, paint); + } + } + } + + public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewModel viewModel, + ThemePreviewer.HoveredItem hoveredItem) + { + // Set arrow hit regions + LeftArrowRect = new Rectangle(0, 0, ARROW_AREA_WIDTH, info.Height); + RightArrowRect = new Rectangle(info.Width - ARROW_AREA_WIDTH, 0, ARROW_AREA_WIDTH, info.Height); + + // Draw left and right arrow button areas + DrawArrowArea(canvas, info, true, hoveredItem == ThemePreviewer.HoveredItem.LeftArrow); + DrawArrowArea(canvas, info, false, hoveredItem == ThemePreviewer.HoveredItem.RightArrow); + + // Title and preview text box (top left) + var titleBounds = new SKRect(); + titleFont.MeasureText(viewModel.Title ?? "", out titleBounds); + var previewBounds = new SKRect(); + previewFont.MeasureText(viewModel.PreviewText ?? "", out previewBounds); + + float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + MARGIN_STANDARD; + float boxHeight = 19 + 4 + 16 + MARGIN_STANDARD; + TitleBoxRect = new Rectangle(MARGIN_STANDARD, MARGIN_STANDARD, (int)boxWidth, (int)boxHeight); + + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(TitleBoxRect.X, TitleBoxRect.Y, TitleBoxRect.Width, TitleBoxRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + basePaint.Color = SKColors.White; + canvas.DrawText(viewModel.Title ?? "", TitleBoxRect.X + 10, TitleBoxRect.Y + 8 + 19, titleFont, basePaint); + canvas.DrawText(viewModel.PreviewText ?? "", TitleBoxRect.X + 10, TitleBoxRect.Y + 8 + 19 + 5 + 16, previewFont, basePaint); + + // Play/Pause button (top right) + int playButtonSize = 40; + PlayButtonRect = new Rectangle(info.Width - playButtonSize - MARGIN_STANDARD, MARGIN_STANDARD, playButtonSize, playButtonSize); + + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(PlayButtonRect.X, PlayButtonRect.Y, PlayButtonRect.Width, PlayButtonRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + float playOpacity = hoveredItem == ThemePreviewer.HoveredItem.PlayButton ? OPACITY_HOVER : OPACITY_NORMAL; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); + string playIcon = viewModel.IsPlaying ? "\uf04c" : "\uf04b"; + var textBounds = new SKRect(); + iconFont16.MeasureText(playIcon, out textBounds); + float centerX = PlayButtonRect.X + PlayButtonRect.Width / 2; + float centerY = PlayButtonRect.Y + PlayButtonRect.Height / 2; + canvas.DrawText(playIcon, centerX - textBounds.MidX, centerY - textBounds.MidY, iconFont16, basePaint); + + // Corner labels + DrawCornerLabel(canvas, info, viewModel.Author, isBottomRight: true, out var authorRect); + AuthorLabelRect = authorRect; + DrawCornerLabel(canvas, info, viewModel.DownloadSize, isBottomRight: false, out var downloadSizeRect); + DownloadSizeLabelRect = downloadSizeRect; + + // Download message (centered bottom) + if (!string.IsNullOrEmpty(viewModel.Message)) + { + var msgBounds = new SKRect(); + textFont.MeasureText(viewModel.Message, out msgBounds); + float msgWidth = msgBounds.Width + 16; + float msgHeight = 6 + 16 + 6; + DownloadMessageRect = new Rectangle((int)(info.Width / 2 - msgWidth / 2), info.Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); + + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(DownloadMessageRect.X, DownloadMessageRect.Y, DownloadMessageRect.Width, DownloadMessageRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + float msgOpacity = hoveredItem == ThemePreviewer.HoveredItem.DownloadButton ? OPACITY_HOVER : OPACITY_MESSAGE; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); + canvas.DrawText(viewModel.Message, DownloadMessageRect.X + 8, DownloadMessageRect.Y + 5 + 16, textFont, basePaint); + } + else + { + DownloadMessageRect = Rectangle.Empty; + } + + // Carousel indicators + if (viewModel.CarouselIndicatorsVisible && viewModel.Items.Count > 0) + { + DrawCarouselIndicators(canvas, info, viewModel.Items.Count, viewModel.SelectedIndex); + } + else + { + CarouselIndicatorRects = null; + } + } + + private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft, bool isHovered) + { + float opacity = isHovered ? OPACITY_HOVER : OPACITY_NORMAL; + + float x = isLeft ? 40 : info.Width - 40; + float y = info.Height / 2; + + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); + + string icon = isLeft ? "\uf053" : "\uf054"; + var textBounds = new SKRect(); + iconFont20.MeasureText(icon, out textBounds); + canvas.DrawText(icon, x - textBounds.MidX, y - textBounds.MidY, iconFont20, basePaint); + } + + private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, bool isBottomRight, out Rectangle labelRect) + { + if (string.IsNullOrEmpty(text)) + { + labelRect = Rectangle.Empty; + return; + } + + var textBounds = new SKRect(); + textFont.MeasureText(text, out textBounds); + + float leftMargin = isBottomRight ? 8 : 11; + float rightMargin = isBottomRight ? 11 : 8; + float borderWidth = textBounds.Width + leftMargin + rightMargin; + float borderHeight = 4 + 16 + 9; + + float rectX = isBottomRight ? info.Width - borderWidth + 3 : -3; + float rectY = info.Height - borderHeight + 3; + labelRect = new Rectangle((int)rectX, (int)rectY, (int)borderWidth, (int)borderHeight); + + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(rectX, rectY, borderWidth, borderHeight), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + basePaint.Color = SKColors.White.WithAlpha(OVERLAY_ALPHA); + float textX = isBottomRight ? info.Width - textBounds.Width - rightMargin + 3 : leftMargin - 3; + float textY = rectY + 4 + 16; + canvas.DrawText(text, textX, textY, textFont, basePaint); + } + + private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info, int count, int selectedIndex) + { + int indicatorWidth = 30; + int indicatorHeight = 3; + int itemSpacing = 6; + int totalWidth = count * indicatorWidth + (count - 1) * itemSpacing; + int startX = (info.Width - totalWidth) / 2; + int y = info.Height - 16 - 16; // bottom margin - half of clickable height + + CarouselIndicatorRects = new Rectangle[count]; + + for (int i = 0; i < count; i++) + { + float opacity = (i == selectedIndex) ? OPACITY_HOVER : OPACITY_NORMAL; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); + + int rectX = startX + i * (indicatorWidth + itemSpacing); + canvas.DrawRect(rectX, y - indicatorHeight / 2, indicatorWidth, indicatorHeight, basePaint); + + // Cache full clickable area for hit testing + CarouselIndicatorRects[i] = new Rectangle(rectX, y - 16, indicatorWidth, 32); + } + } + + public void Dispose() + { + basePaint?.Dispose(); + titleFont?.Dispose(); + previewFont?.Dispose(); + textFont?.Dispose(); + iconFont16?.Dispose(); + iconFont20?.Dispose(); + } + } +} diff --git a/src/Skia/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs index 928d42a2..5ea511f7 100644 --- a/src/Skia/ThemePreviewer.cs +++ b/src/Skia/ThemePreviewer.cs @@ -5,7 +5,6 @@ using SkiaSharp; using SkiaSharp.Views.Desktop; using System; -using System.Drawing; using System.IO; using System.Reflection; using System.Windows.Forms; @@ -16,13 +15,6 @@ public class ThemePreviewer : SKControl { private const int ANIMATION_DURATION_MS = 600; private const int ANIMATION_FPS = 120; - private const int MARGIN_STANDARD = 20; - private const int BORDER_RADIUS = 5; - private const int ARROW_AREA_WIDTH = 80; - private const byte OVERLAY_ALPHA = 127; - private const float OPACITY_NORMAL = 0.5f; - private const float OPACITY_HOVER = 1.0f; - private const float OPACITY_MESSAGE = 0.8f; public ThemePreviewerViewModel ViewModel { get; } @@ -30,30 +22,19 @@ public class ThemePreviewer : SKControl private float fadeProgress = 0f; private bool isAnimating = false; private DateTime animationStartTime; + private static SKTypeface fontAwesome; + private readonly ThemePreviewRenderer renderer; + private HoveredItem hoveredItem = HoveredItem.None; - // Cached objects to reduce allocations - private readonly SKPaint basePaint = new SKPaint { IsAntialias = true }; - private readonly SKColor overlayColor = new SKColor(0, 0, 0, OVERLAY_ALPHA); - private readonly SKFont titleFont; - private readonly SKFont previewFont; - private readonly SKFont textFont; - private readonly SKFont iconFont16; - private readonly SKFont iconFont20; - private readonly SKSamplingOptions samplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); - - private bool isMouseOverPlay = false; - private bool isMouseOverLeft = false; - private bool isMouseOverRight = false; - private bool isMouseOverDownload = false; - - // Cached UI element rectangles for rendering and hit testing - private Rectangle titleBoxRect; - private Rectangle playButtonRect; - private Rectangle downloadMessageRect; - private Rectangle authorLabelRect; - private Rectangle downloadSizeLabelRect; - private Rectangle[] carouselIndicatorRects; + public enum HoveredItem + { + None, + PlayButton, + LeftArrow, + RightArrow, + DownloadButton + } public ThemePreviewer() { @@ -70,12 +51,7 @@ public ThemePreviewer() } } - // Initialize cached fonts - titleFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 19); - previewFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 16); - textFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 16); - iconFont16 = new SKFont(fontAwesome, 16); - iconFont20 = new SKFont(fontAwesome, 20); + renderer = new ThemePreviewRenderer(fontAwesome); // Timer for smooth fade animations fadeTimer = new Timer @@ -127,192 +103,19 @@ protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) // Draw back image if (ViewModel.BackImage != null) { - DrawImage(canvas, ViewModel.BackImage, info, 1.0f); + renderer.DrawImage(canvas, ViewModel.BackImage, info, 1.0f); } // Draw front image with fade animation if (ViewModel.FrontImage != null && isAnimating) { - DrawImage(canvas, ViewModel.FrontImage, info, fadeProgress); + renderer.DrawImage(canvas, ViewModel.FrontImage, info, fadeProgress); } // Draw UI overlay if (ViewModel.ControlsVisible) { - DrawOverlay(canvas, info); - } - } - - private void DrawImage(SKCanvas canvas, SKImage image, SKImageInfo info, float opacity) - { - var destRect = new SKRect(0, 0, info.Width, info.Height); - - if (opacity >= 1.0f) - { - // Fast path for fully opaque images - canvas.DrawImage(image, destRect, samplingOptions, null); - } - else - { - // Apply opacity with color filter - using (var paint = new SKPaint()) - { - paint.IsAntialias = true; - paint.ColorFilter = SKColorFilter.CreateBlendMode( - SKColors.White.WithAlpha((byte)(255 * opacity)), - SKBlendMode.DstIn); - - canvas.DrawImage(image, destRect, samplingOptions, paint); - } - } - } - - private void DrawOverlay(SKCanvas canvas, SKImageInfo info) - { - // Draw left and right arrow button areas - DrawArrowArea(canvas, info, true); - DrawArrowArea(canvas, info, false); - - // Title and preview text box (top left) - var titleBounds = new SKRect(); - titleFont.MeasureText(ViewModel.Title ?? "", out titleBounds); - var previewBounds = new SKRect(); - previewFont.MeasureText(ViewModel.PreviewText ?? "", out previewBounds); - - float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + MARGIN_STANDARD; - float boxHeight = 19 + 4 + 16 + MARGIN_STANDARD; - titleBoxRect = new Rectangle(MARGIN_STANDARD, MARGIN_STANDARD, (int)boxWidth, (int)boxHeight); - - basePaint.Color = overlayColor; - canvas.DrawRoundRect(SKRect.Create(titleBoxRect.X, titleBoxRect.Y, titleBoxRect.Width, titleBoxRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); - - basePaint.Color = SKColors.White; - canvas.DrawText(ViewModel.Title ?? "", titleBoxRect.X + 10, titleBoxRect.Y + 8 + 19, titleFont, basePaint); - canvas.DrawText(ViewModel.PreviewText ?? "", titleBoxRect.X + 10, titleBoxRect.Y + 8 + 19 + 5 + 16, previewFont, basePaint); - - // Play/Pause button (top right) - int playButtonSize = 40; - playButtonRect = new Rectangle(info.Width - playButtonSize - MARGIN_STANDARD, MARGIN_STANDARD, playButtonSize, playButtonSize); - - basePaint.Color = overlayColor; - canvas.DrawRoundRect(SKRect.Create(playButtonRect.X, playButtonRect.Y, playButtonRect.Width, playButtonRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); - - float playOpacity = isMouseOverPlay ? OPACITY_HOVER : OPACITY_NORMAL; - basePaint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); - string playIcon = ViewModel.IsPlaying ? "\uf04c" : "\uf04b"; - var textBounds = new SKRect(); - iconFont16.MeasureText(playIcon, out textBounds); - float centerX = playButtonRect.X + playButtonRect.Width / 2; - float centerY = playButtonRect.Y + playButtonRect.Height / 2; - canvas.DrawText(playIcon, centerX - textBounds.MidX, centerY - textBounds.MidY, iconFont16, basePaint); - - // Corner labels - DrawCornerLabel(canvas, info, ViewModel.Author, isBottomRight: true); - DrawCornerLabel(canvas, info, ViewModel.DownloadSize, isBottomRight: false); - - // Download message (centered bottom) - if (!string.IsNullOrEmpty(ViewModel.Message)) - { - var msgBounds = new SKRect(); - textFont.MeasureText(ViewModel.Message, out msgBounds); - float msgWidth = msgBounds.Width + 16; - float msgHeight = 6 + 16 + 6; - downloadMessageRect = new Rectangle((int)(info.Width / 2 - msgWidth / 2), info.Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); - - basePaint.Color = overlayColor; - canvas.DrawRoundRect(SKRect.Create(downloadMessageRect.X, downloadMessageRect.Y, downloadMessageRect.Width, downloadMessageRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); - - float msgOpacity = isMouseOverDownload ? OPACITY_HOVER : OPACITY_MESSAGE; - basePaint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); - canvas.DrawText(ViewModel.Message, downloadMessageRect.X + 8, downloadMessageRect.Y + 5 + 16, textFont, basePaint); - } - else - { - downloadMessageRect = Rectangle.Empty; - } - - // Carousel indicators - Margin=16, Height=32, Rectangle Height=3, Width=30, Margin="3,0" - if (ViewModel.CarouselIndicatorsVisible && ViewModel.Items.Count > 0) - { - DrawCarouselIndicators(canvas, info); - } - } - - private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft) - { - bool isHovered = isLeft ? isMouseOverLeft : isMouseOverRight; - float opacity = isHovered ? OPACITY_HOVER : OPACITY_NORMAL; - - float x = isLeft ? 40 : info.Width - 40; - float y = info.Height / 2; - - basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); - - string icon = isLeft ? "\uf053" : "\uf054"; - var textBounds = new SKRect(); - iconFont20.MeasureText(icon, out textBounds); - canvas.DrawText(icon, x - textBounds.MidX, y - textBounds.MidY, iconFont20, basePaint); - } - - private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, bool isBottomRight) - { - if (string.IsNullOrEmpty(text)) - { - if (isBottomRight) - authorLabelRect = Rectangle.Empty; - else - downloadSizeLabelRect = Rectangle.Empty; - return; - } - - var textBounds = new SKRect(); - textFont.MeasureText(text, out textBounds); - - float leftMargin = isBottomRight ? 8 : 11; - float rightMargin = isBottomRight ? 11 : 8; - float borderWidth = textBounds.Width + leftMargin + rightMargin; - float borderHeight = 4 + 16 + 9; - - float rectX = isBottomRight ? info.Width - borderWidth + 3 : -3; - float rectY = info.Height - borderHeight + 3; - Rectangle labelRect = new Rectangle((int)rectX, (int)rectY, (int)borderWidth, (int)borderHeight); - - if (isBottomRight) - authorLabelRect = labelRect; - else - downloadSizeLabelRect = labelRect; - - basePaint.Color = overlayColor; - canvas.DrawRoundRect(SKRect.Create(rectX, rectY, borderWidth, borderHeight), BORDER_RADIUS, BORDER_RADIUS, basePaint); - - basePaint.Color = SKColors.White.WithAlpha(OVERLAY_ALPHA); - float textX = isBottomRight ? info.Width - textBounds.Width - rightMargin + 3 : leftMargin - 3; - float textY = rectY + 4 + 16; - canvas.DrawText(text, textX, textY, textFont, basePaint); - } - - private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info) - { - int count = ViewModel.Items.Count; - int indicatorWidth = 30; - int indicatorHeight = 3; - int itemSpacing = 6; - int totalWidth = count * indicatorWidth + (count - 1) * itemSpacing; - int startX = (info.Width - totalWidth) / 2; - int y = info.Height - 16 - 16; // bottom margin - half of clickable height - - carouselIndicatorRects = new Rectangle[count]; - - for (int i = 0; i < count; i++) - { - float opacity = (i == ViewModel.SelectedIndex) ? OPACITY_HOVER : OPACITY_NORMAL; - basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); - - int rectX = startX + i * (indicatorWidth + itemSpacing); - canvas.DrawRect(rectX, y - indicatorHeight / 2, indicatorWidth, indicatorHeight, basePaint); - - // Cache full clickable area for hit testing - carouselIndicatorRects[i] = new Rectangle(rectX, y - 16, indicatorWidth, 32); + renderer.DrawOverlay(canvas, info, ViewModel, hoveredItem); } } @@ -323,39 +126,39 @@ protected override void OnMouseClick(MouseEventArgs e) if (!ViewModel.ControlsVisible) return; // Check if play button was clicked - if (playButtonRect.Contains(e.Location)) + if (renderer.PlayButtonRect.Contains(e.Location)) { ViewModel.TogglePlayPause(); return; } // Check if left arrow area was clicked - if (e.X < ARROW_AREA_WIDTH) + if (renderer.LeftArrowRect.Contains(e.Location)) { ViewModel.Previous(); return; } // Check if right arrow area was clicked - if (e.X > Width - ARROW_AREA_WIDTH) + if (renderer.RightArrowRect.Contains(e.Location)) { ViewModel.Next(); return; } // Check if download message was clicked - if (downloadMessageRect.Contains(e.Location)) + if (renderer.DownloadMessageRect.Contains(e.Location)) { ViewModel.InvokeDownload(); return; } // Check if carousel indicator was clicked - if (carouselIndicatorRects != null) + if (renderer.CarouselIndicatorRects != null) { - for (int i = 0; i < carouselIndicatorRects.Length; i++) + for (int i = 0; i < renderer.CarouselIndicatorRects.Length; i++) { - if (carouselIndicatorRects[i].Contains(e.Location)) + if (renderer.CarouselIndicatorRects[i].Contains(e.Location)) { ViewModel.SelectedIndex = i; return; @@ -368,55 +171,40 @@ protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); - bool needsRedraw = false; - - // Update cursor based on location if (!ViewModel.ControlsVisible) { Cursor = Cursors.Default; return; } - bool wasOverPlay = isMouseOverPlay; - bool wasOverLeft = isMouseOverLeft; - bool wasOverRight = isMouseOverRight; - bool wasOverDownload = isMouseOverDownload; + var previousHoveredItem = hoveredItem; + hoveredItem = HoveredItem.None; - // Reset all hover states - isMouseOverPlay = false; - isMouseOverLeft = false; - isMouseOverRight = false; - isMouseOverDownload = false; - - // Check UI elements in priority order using cached rectangles - isMouseOverPlay = playButtonRect.Contains(e.Location); - - if (!isMouseOverPlay) - isMouseOverDownload = downloadMessageRect.Contains(e.Location); - - // Check if over any UI box (title, corner labels) - bool isOverUIBox = false; - if (!isMouseOverPlay && !isMouseOverDownload) + // Check UI elements in priority order + if (renderer.PlayButtonRect.Contains(e.Location)) { - isOverUIBox = titleBoxRect.Contains(e.Location) || - authorLabelRect.Contains(e.Location) || - downloadSizeLabelRect.Contains(e.Location); + hoveredItem = HoveredItem.PlayButton; } - - // Arrows only if not over other UI elements - if (!isMouseOverPlay && !isMouseOverDownload && !isOverUIBox) + else if (renderer.DownloadMessageRect.Contains(e.Location)) { - isMouseOverLeft = e.X < ARROW_AREA_WIDTH; - isMouseOverRight = e.X > Width - ARROW_AREA_WIDTH; + hoveredItem = HoveredItem.DownloadButton; + } + else if (renderer.LeftArrowRect.Contains(e.Location)) + { + hoveredItem = HoveredItem.LeftArrow; + } + else if (renderer.RightArrowRect.Contains(e.Location)) + { + hoveredItem = HoveredItem.RightArrow; } // Check carousel indicators for hand cursor bool isOverCarouselIndicator = false; - if (carouselIndicatorRects != null) + if (renderer.CarouselIndicatorRects != null) { - for (int i = 0; i < carouselIndicatorRects.Length; i++) + foreach (var rect in renderer.CarouselIndicatorRects) { - if (carouselIndicatorRects[i].Contains(e.Location)) + if (rect.Contains(e.Location)) { isOverCarouselIndicator = true; break; @@ -424,13 +212,10 @@ protected override void OnMouseMove(MouseEventArgs e) } } - needsRedraw = (isMouseOverPlay != wasOverPlay) || (isMouseOverLeft != wasOverLeft) || - (isMouseOverRight != wasOverRight) || (isMouseOverDownload != wasOverDownload); - - bool isOverClickable = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload || isOverCarouselIndicator; + bool isOverClickable = hoveredItem != HoveredItem.None || isOverCarouselIndicator; Cursor = isOverClickable ? Cursors.Hand : Cursors.Default; - if (needsRedraw) + if (hoveredItem != previousHoveredItem) { Invalidate(); } @@ -440,18 +225,10 @@ private void OnMouseLeave(object sender, EventArgs e) { ViewModel.IsMouseOver = false; - // Reset all hover states - bool needsRedraw = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload; - - isMouseOverPlay = false; - isMouseOverLeft = false; - isMouseOverRight = false; - isMouseOverDownload = false; - - Cursor = Cursors.Default; - - if (needsRedraw) + if (hoveredItem != HoveredItem.None) { + hoveredItem = HoveredItem.None; + Cursor = Cursors.Default; Invalidate(); } } @@ -496,14 +273,7 @@ protected override void Dispose(bool disposing) { fadeTimer?.Dispose(); ViewModel?.Stop(); - - // Dispose cached SkiaSharp objects - basePaint?.Dispose(); - titleFont?.Dispose(); - previewFont?.Dispose(); - textFont?.Dispose(); - iconFont16?.Dispose(); - iconFont20?.Dispose(); + renderer?.Dispose(); } base.Dispose(disposing); } From 1de9db9a0f616c9d7746539e8f408db12d3f7b7b Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Sat, 29 Nov 2025 10:07:04 -0500 Subject: [PATCH 13/15] Refactor drawing methods to be more clear --- src/Skia/ThemePreviewRenderer.cs | 40 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Skia/ThemePreviewRenderer.cs b/src/Skia/ThemePreviewRenderer.cs index cf2b49fb..460eebf3 100644 --- a/src/Skia/ThemePreviewRenderer.cs +++ b/src/Skia/ThemePreviewRenderer.cs @@ -30,13 +30,15 @@ internal class ThemePreviewRenderer // Hit test regions (updated during rendering) public Rectangle TitleBoxRect { get; private set; } public Rectangle PlayButtonRect { get; private set; } - public Rectangle DownloadMessageRect { get; private set; } - public Rectangle AuthorLabelRect { get; private set; } public Rectangle DownloadSizeLabelRect { get; private set; } + public Rectangle AuthorLabelRect { get; private set; } + public Rectangle DownloadMessageRect { get; private set; } public Rectangle LeftArrowRect { get; private set; } public Rectangle RightArrowRect { get; private set; } public Rectangle[] CarouselIndicatorRects { get; private set; } + private enum Side { Left, Right } + public ThemePreviewRenderer(SKTypeface fontAwesome) { basePaint = new SKPaint { IsAntialias = true }; @@ -81,8 +83,8 @@ public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewMod RightArrowRect = new Rectangle(info.Width - ARROW_AREA_WIDTH, 0, ARROW_AREA_WIDTH, info.Height); // Draw left and right arrow button areas - DrawArrowArea(canvas, info, true, hoveredItem == ThemePreviewer.HoveredItem.LeftArrow); - DrawArrowArea(canvas, info, false, hoveredItem == ThemePreviewer.HoveredItem.RightArrow); + DrawArrowArea(canvas, info, Side.Left, hoveredItem == ThemePreviewer.HoveredItem.LeftArrow); + DrawArrowArea(canvas, info, Side.Right, hoveredItem == ThemePreviewer.HoveredItem.RightArrow); // Title and preview text box (top left) var titleBounds = new SKRect(); @@ -118,10 +120,10 @@ public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewMod canvas.DrawText(playIcon, centerX - textBounds.MidX, centerY - textBounds.MidY, iconFont16, basePaint); // Corner labels - DrawCornerLabel(canvas, info, viewModel.Author, isBottomRight: true, out var authorRect); - AuthorLabelRect = authorRect; - DrawCornerLabel(canvas, info, viewModel.DownloadSize, isBottomRight: false, out var downloadSizeRect); + DrawCornerLabel(canvas, info, viewModel.DownloadSize, Side.Left, out var downloadSizeRect); DownloadSizeLabelRect = downloadSizeRect; + DrawCornerLabel(canvas, info, viewModel.Author, Side.Right, out var authorRect); + AuthorLabelRect = authorRect; // Download message (centered bottom) if (!string.IsNullOrEmpty(viewModel.Message)) @@ -155,22 +157,22 @@ public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewMod } } - private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft, bool isHovered) + private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, Side side, bool isHovered) { float opacity = isHovered ? OPACITY_HOVER : OPACITY_NORMAL; - float x = isLeft ? 40 : info.Width - 40; + float x = side == Side.Left ? 40 : info.Width - 40; float y = info.Height / 2; basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); - string icon = isLeft ? "\uf053" : "\uf054"; + string icon = side == Side.Left ? "\uf053" : "\uf054"; var textBounds = new SKRect(); iconFont20.MeasureText(icon, out textBounds); canvas.DrawText(icon, x - textBounds.MidX, y - textBounds.MidY, iconFont20, basePaint); } - private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, bool isBottomRight, out Rectangle labelRect) + private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, Side side, out Rectangle labelRect) { if (string.IsNullOrEmpty(text)) { @@ -181,21 +183,23 @@ private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, boo var textBounds = new SKRect(); textFont.MeasureText(text, out textBounds); - float leftMargin = isBottomRight ? 8 : 11; - float rightMargin = isBottomRight ? 11 : 8; + var padding = new System.Windows.Forms.Padding(8, 4, 10, 10); // Left, Top, Right, Bottom + float leftMargin = side == Side.Right ? padding.Left : padding.Right; + float rightMargin = side == Side.Right ? padding.Right : padding.Left; float borderWidth = textBounds.Width + leftMargin + rightMargin; - float borderHeight = 4 + 16 + 9; + float borderHeight = padding.Top + 16 + padding.Bottom; - float rectX = isBottomRight ? info.Width - borderWidth + 3 : -3; - float rectY = info.Height - borderHeight + 3; + int offset = 3; + float rectX = side == Side.Right ? info.Width - borderWidth + offset : -offset; + float rectY = info.Height - borderHeight + offset; labelRect = new Rectangle((int)rectX, (int)rectY, (int)borderWidth, (int)borderHeight); basePaint.Color = overlayColor; canvas.DrawRoundRect(SKRect.Create(rectX, rectY, borderWidth, borderHeight), BORDER_RADIUS, BORDER_RADIUS, basePaint); basePaint.Color = SKColors.White.WithAlpha(OVERLAY_ALPHA); - float textX = isBottomRight ? info.Width - textBounds.Width - rightMargin + 3 : leftMargin - 3; - float textY = rectY + 4 + 16; + float textX = side == Side.Right ? info.Width - textBounds.Width - rightMargin + offset : leftMargin - offset; + float textY = rectY + padding.Top + 16; canvas.DrawText(text, textX, textY, textFont, basePaint); } From d82463d986a38f06c8aa2b8da409078e285c7a9f Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Sat, 29 Nov 2025 19:48:04 -0500 Subject: [PATCH 14/15] Use linq methods in ThemePreviewer --- src/Skia/ThemePreviewer.cs | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/Skia/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs index 5ea511f7..f2e9892b 100644 --- a/src/Skia/ThemePreviewer.cs +++ b/src/Skia/ThemePreviewer.cs @@ -6,6 +6,7 @@ using SkiaSharp.Views.Desktop; using System; using System.IO; +using System.Linq; using System.Reflection; using System.Windows.Forms; @@ -154,16 +155,11 @@ protected override void OnMouseClick(MouseEventArgs e) } // Check if carousel indicator was clicked - if (renderer.CarouselIndicatorRects != null) + var clickedIndex = Array.FindIndex(renderer.CarouselIndicatorRects ?? [], r => r.Contains(e.Location)); + if (clickedIndex != -1) { - for (int i = 0; i < renderer.CarouselIndicatorRects.Length; i++) - { - if (renderer.CarouselIndicatorRects[i].Contains(e.Location)) - { - ViewModel.SelectedIndex = i; - return; - } - } + ViewModel.SelectedIndex = clickedIndex; + return; } } @@ -199,19 +195,7 @@ protected override void OnMouseMove(MouseEventArgs e) } // Check carousel indicators for hand cursor - bool isOverCarouselIndicator = false; - if (renderer.CarouselIndicatorRects != null) - { - foreach (var rect in renderer.CarouselIndicatorRects) - { - if (rect.Contains(e.Location)) - { - isOverCarouselIndicator = true; - break; - } - } - } - + bool isOverCarouselIndicator = renderer.CarouselIndicatorRects?.Any(r => r.Contains(e.Location)) ?? false; bool isOverClickable = hoveredItem != HoveredItem.None || isOverCarouselIndicator; Cursor = isOverClickable ? Cursors.Hand : Cursors.Default; From f90582ee52e207887434547e55be23b63a082bba Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Mon, 1 Dec 2025 23:16:30 -0500 Subject: [PATCH 15/15] Address Copilot feedback --- src/Skia/ImageCache.cs | 16 ++++----- src/Skia/ThemePreviewRenderer.cs | 56 ++++++++++++++++++-------------- src/Skia/ThemePreviewer.cs | 2 +- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/Skia/ImageCache.cs b/src/Skia/ImageCache.cs index 1e25b12d..ef45058f 100644 --- a/src/Skia/ImageCache.cs +++ b/src/Skia/ImageCache.cs @@ -25,19 +25,17 @@ public SKImage this[Uri uri] { lock (cacheLock) { - if (images.ContainsKey(uri)) + if (images.TryGetValue(uri, out var image)) { - return images[uri]; + return image; } - else + + var img = CreateImage(uri); + if (img != null) { - var img = CreateImage(uri); - if (img != null) - { - images.Add(uri, img); - } - return img; + images.Add(uri, img); } + return img; } } } diff --git a/src/Skia/ThemePreviewRenderer.cs b/src/Skia/ThemePreviewRenderer.cs index 460eebf3..f4aae7a5 100644 --- a/src/Skia/ThemePreviewRenderer.cs +++ b/src/Skia/ThemePreviewRenderer.cs @@ -16,15 +16,21 @@ internal class ThemePreviewRenderer private const byte OVERLAY_ALPHA = 127; private const float OPACITY_NORMAL = 0.5f; private const float OPACITY_HOVER = 1.0f; - private const float OPACITY_MESSAGE = 0.8f; + + // FontAwesome icons + private const string ICON_PLAY = "\uf04b"; + private const string ICON_PAUSE = "\uf04c"; + private const string ICON_CHEVRON_LEFT = "\uf053"; + private const string ICON_CHEVRON_RIGHT = "\uf054"; private readonly SKPaint basePaint; - private readonly SKColor overlayColor; + private readonly SKTypeface systemFont; + private readonly SKTypeface systemFontBold; private readonly SKFont titleFont; - private readonly SKFont previewFont; private readonly SKFont textFont; private readonly SKFont iconFont16; private readonly SKFont iconFont20; + private readonly SKColor overlayColor; private readonly SKSamplingOptions samplingOptions; // Hit test regions (updated during rendering) @@ -39,15 +45,16 @@ internal class ThemePreviewRenderer private enum Side { Left, Right } - public ThemePreviewRenderer(SKTypeface fontAwesome) + public ThemePreviewRenderer(SKTypeface fontAwesome, string systemFontName) { basePaint = new SKPaint { IsAntialias = true }; - overlayColor = new SKColor(0, 0, 0, OVERLAY_ALPHA); - titleFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 19); - previewFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), 16); - textFont = new SKFont(SKTypeface.FromFamilyName("Segoe UI"), 16); + systemFont = SKTypeface.FromFamilyName(systemFontName); + systemFontBold = SKTypeface.FromFamilyName(systemFontName, SKFontStyle.Bold); + titleFont = new SKFont(systemFontBold, 19); + textFont = new SKFont(systemFont, 16); iconFont16 = new SKFont(fontAwesome, 16); iconFont20 = new SKFont(fontAwesome, 20); + overlayColor = new SKColor(0, 0, 0, OVERLAY_ALPHA); samplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); } @@ -64,12 +71,12 @@ public void DrawImage(SKCanvas canvas, SKImage image, SKImageInfo info, float op { // Apply opacity with color filter using (var paint = new SKPaint()) + using (var colorFilter = SKColorFilter.CreateBlendMode( + SKColors.White.WithAlpha((byte)(255 * opacity)), + SKBlendMode.DstIn)) { paint.IsAntialias = true; - paint.ColorFilter = SKColorFilter.CreateBlendMode( - SKColors.White.WithAlpha((byte)(255 * opacity)), - SKBlendMode.DstIn); - + paint.ColorFilter = colorFilter; canvas.DrawImage(image, destRect, samplingOptions, paint); } } @@ -87,10 +94,10 @@ public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewMod DrawArrowArea(canvas, info, Side.Right, hoveredItem == ThemePreviewer.HoveredItem.RightArrow); // Title and preview text box (top left) - var titleBounds = new SKRect(); + SKRect titleBounds; titleFont.MeasureText(viewModel.Title ?? "", out titleBounds); - var previewBounds = new SKRect(); - previewFont.MeasureText(viewModel.PreviewText ?? "", out previewBounds); + SKRect previewBounds; + textFont.MeasureText(viewModel.PreviewText ?? "", out previewBounds); float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + MARGIN_STANDARD; float boxHeight = 19 + 4 + 16 + MARGIN_STANDARD; @@ -101,7 +108,7 @@ public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewMod basePaint.Color = SKColors.White; canvas.DrawText(viewModel.Title ?? "", TitleBoxRect.X + 10, TitleBoxRect.Y + 8 + 19, titleFont, basePaint); - canvas.DrawText(viewModel.PreviewText ?? "", TitleBoxRect.X + 10, TitleBoxRect.Y + 8 + 19 + 5 + 16, previewFont, basePaint); + canvas.DrawText(viewModel.PreviewText ?? "", TitleBoxRect.X + 10, TitleBoxRect.Y + 8 + 19 + 5 + 16, textFont, basePaint); // Play/Pause button (top right) int playButtonSize = 40; @@ -112,8 +119,8 @@ public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewMod float playOpacity = hoveredItem == ThemePreviewer.HoveredItem.PlayButton ? OPACITY_HOVER : OPACITY_NORMAL; basePaint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); - string playIcon = viewModel.IsPlaying ? "\uf04c" : "\uf04b"; - var textBounds = new SKRect(); + string playIcon = viewModel.IsPlaying ? ICON_PAUSE : ICON_PLAY; + SKRect textBounds; iconFont16.MeasureText(playIcon, out textBounds); float centerX = PlayButtonRect.X + PlayButtonRect.Width / 2; float centerY = PlayButtonRect.Y + PlayButtonRect.Height / 2; @@ -128,7 +135,7 @@ public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewMod // Download message (centered bottom) if (!string.IsNullOrEmpty(viewModel.Message)) { - var msgBounds = new SKRect(); + SKRect msgBounds; textFont.MeasureText(viewModel.Message, out msgBounds); float msgWidth = msgBounds.Width + 16; float msgHeight = 6 + 16 + 6; @@ -137,7 +144,7 @@ public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewMod basePaint.Color = overlayColor; canvas.DrawRoundRect(SKRect.Create(DownloadMessageRect.X, DownloadMessageRect.Y, DownloadMessageRect.Width, DownloadMessageRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); - float msgOpacity = hoveredItem == ThemePreviewer.HoveredItem.DownloadButton ? OPACITY_HOVER : OPACITY_MESSAGE; + float msgOpacity = hoveredItem == ThemePreviewer.HoveredItem.DownloadButton ? OPACITY_HOVER : OPACITY_NORMAL; basePaint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); canvas.DrawText(viewModel.Message, DownloadMessageRect.X + 8, DownloadMessageRect.Y + 5 + 16, textFont, basePaint); } @@ -166,8 +173,8 @@ private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, Side side, bool is basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); - string icon = side == Side.Left ? "\uf053" : "\uf054"; - var textBounds = new SKRect(); + string icon = side == Side.Left ? ICON_CHEVRON_LEFT : ICON_CHEVRON_RIGHT; + SKRect textBounds; iconFont20.MeasureText(icon, out textBounds); canvas.DrawText(icon, x - textBounds.MidX, y - textBounds.MidY, iconFont20, basePaint); } @@ -180,7 +187,7 @@ private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, Sid return; } - var textBounds = new SKRect(); + SKRect textBounds; textFont.MeasureText(text, out textBounds); var padding = new System.Windows.Forms.Padding(8, 4, 10, 10); // Left, Top, Right, Bottom @@ -230,8 +237,9 @@ private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info, int count public void Dispose() { basePaint?.Dispose(); + systemFont?.Dispose(); + systemFontBold?.Dispose(); titleFont?.Dispose(); - previewFont?.Dispose(); textFont?.Dispose(); iconFont16?.Dispose(); iconFont20?.Dispose(); diff --git a/src/Skia/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs index f2e9892b..14f33bb0 100644 --- a/src/Skia/ThemePreviewer.cs +++ b/src/Skia/ThemePreviewer.cs @@ -52,7 +52,7 @@ public ThemePreviewer() } } - renderer = new ThemePreviewRenderer(fontAwesome); + renderer = new ThemePreviewRenderer(fontAwesome, Control.DefaultFont.FontFamily.Name); // Timer for smooth fade animations fadeTimer = new Timer