mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-11-01 23:04:16 +01:00
Pause plugins when covered by other window
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "lib/modio.net"]
|
||||
path = lib/modio.net
|
||||
url = https://github.com/nickelc/modio.net.git
|
||||
Submodule
+1
Submodule lib/modio.net added at cc684b198f
@@ -9,9 +9,6 @@ namespace DesktopMagic.BuiltInPlugins;
|
||||
|
||||
internal class BenchmarkPlugin : Plugin
|
||||
{
|
||||
private const int BitmapWidth = 400;
|
||||
private const int BitmapHeight = 300;
|
||||
|
||||
private readonly Random rng = new(42);
|
||||
private readonly object rngLock = new();
|
||||
|
||||
@@ -28,7 +25,7 @@ internal class BenchmarkPlugin : Plugin
|
||||
[Setting("fps-counter", "Fps")]
|
||||
private readonly Label fpsCounter = new("");
|
||||
|
||||
public override int UpdateInterval => 16;
|
||||
public override int UpdateInterval => 1;
|
||||
|
||||
public override Bitmap? Main()
|
||||
{
|
||||
@@ -40,8 +37,10 @@ internal class BenchmarkPlugin : Plugin
|
||||
}
|
||||
lastFrameMs = now;
|
||||
|
||||
int w = BitmapWidth;
|
||||
int h = BitmapHeight;
|
||||
// Render at the full device-pixel window size so both benchmarks cover the
|
||||
// same on-screen area at the same resolution for a fair comparison.
|
||||
int w = Application.WindowPixelSize.Width;
|
||||
int h = Application.WindowPixelSize.Height;
|
||||
|
||||
Bitmap bmp = new Bitmap(w, h);
|
||||
using Graphics g = Graphics.FromImage(bmp);
|
||||
|
||||
@@ -4,6 +4,7 @@ using DesktopMagic.Api.Settings;
|
||||
using SkiaSharp;
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace DesktopMagic.BuiltInPlugins;
|
||||
|
||||
@@ -22,9 +23,23 @@ internal class SkiaBenchmarkPlugin : SkiaPlugin
|
||||
[Setting("skia-shape-type", "Shape Type")]
|
||||
private readonly ComboBox shapeType = new("Rectangles", "Ellipses", "Lines", "Text", "Mixed");
|
||||
|
||||
public override int UpdateInterval => 16;
|
||||
[Setting("skia-fps-counter", "Fps")]
|
||||
private readonly Label fpsCounter = new("");
|
||||
|
||||
public override void Main(SKCanvas canvas)
|
||||
public override int UpdateInterval => 1;
|
||||
|
||||
SKSurface surface;
|
||||
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
Size pixelSize = Application.WindowPixelSize;
|
||||
int w = Math.Max(1, pixelSize.Width);
|
||||
int h = Math.Max(1, pixelSize.Height);
|
||||
surface = SKSurface.Create(new SKImageInfo(w, h, SKColorType.Bgra8888, SKAlphaType.Premul));
|
||||
}
|
||||
|
||||
public override SKImage? Render()
|
||||
{
|
||||
long now = frameTimer.ElapsedMilliseconds;
|
||||
if (lastFrameMs > 0)
|
||||
@@ -34,8 +49,14 @@ internal class SkiaBenchmarkPlugin : SkiaPlugin
|
||||
}
|
||||
lastFrameMs = now;
|
||||
|
||||
int w = (int)canvas.DeviceClipBounds.Width;
|
||||
int h = (int)canvas.DeviceClipBounds.Height;
|
||||
// Render at the full device-pixel window size so the benchmark measures
|
||||
// real on-screen resolution instead of being scaled down.
|
||||
Size pixelSize = Application.WindowPixelSize;
|
||||
int w = Math.Max(1, pixelSize.Width);
|
||||
int h = Math.Max(1, pixelSize.Height);
|
||||
|
||||
|
||||
SKCanvas canvas = surface.Canvas;
|
||||
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
@@ -112,12 +133,8 @@ internal class SkiaBenchmarkPlugin : SkiaPlugin
|
||||
}
|
||||
}
|
||||
|
||||
using var fpsFont = new SKFont(SKTypeface.Default, 14);
|
||||
using var fpsPaint = new SKPaint
|
||||
{
|
||||
Color = color,
|
||||
IsAntialias = false
|
||||
};
|
||||
canvas.DrawText($"FPS: {fps:F1} | Shapes: {count} | Type: {type}", 8, 18, SKTextAlign.Left, fpsFont, fpsPaint);
|
||||
fpsCounter.Value = fps.ToString();
|
||||
|
||||
return surface.Snapshot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,56 @@
|
||||
using DesktopMagic.Api;
|
||||
using DesktopMagic.Api.Settings;
|
||||
|
||||
using SkiaSharp;
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Text;
|
||||
|
||||
namespace DesktopMagic.BuiltInPlugins;
|
||||
|
||||
internal class TimePlugin : Plugin
|
||||
internal class TimePlugin : SkiaPlugin
|
||||
{
|
||||
private const float FontSize = 200;
|
||||
private const float ContentPadding = 8;
|
||||
|
||||
[Setting("display-seconds", "Show Seconds")]
|
||||
private readonly CheckBox displaySecondsCheckBox = new CheckBox(true);
|
||||
|
||||
private string oldTime = string.Empty;
|
||||
private bool themeChanged;
|
||||
|
||||
public override int UpdateInterval => 1000;
|
||||
|
||||
public override Bitmap? Main()
|
||||
public override SKImage? Render()
|
||||
{
|
||||
string time = displaySecondsCheckBox.Value ? DateTime.Now.ToLongTimeString() : DateTime.Now.ToShortTimeString();
|
||||
string time = GetTime();
|
||||
|
||||
if (oldTime == time && !themeChanged)
|
||||
using SKTypeface typeface = SKTypeface.FromFamilyName(Application.Theme.Font);
|
||||
using SKFont font = new(typeface, FontSize);
|
||||
using SKPaint paint = new()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Color = new SKColor(
|
||||
Application.Theme.PrimaryColor.R,
|
||||
Application.Theme.PrimaryColor.G,
|
||||
Application.Theme.PrimaryColor.B,
|
||||
Application.Theme.PrimaryColor.A),
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
oldTime = time;
|
||||
themeChanged = false;
|
||||
float textWidth = font.MeasureText(time, paint);
|
||||
SKFontMetrics metrics = font.Metrics;
|
||||
float textHeight = metrics.Descent - metrics.Ascent;
|
||||
|
||||
using Font font = new Font(Application.Theme.Font, 200);
|
||||
int width = (int)Math.Ceiling(textWidth) + (int)(ContentPadding * 2);
|
||||
int height = (int)Math.Ceiling(textHeight) + (int)(ContentPadding * 2);
|
||||
|
||||
Bitmap bmp = new Bitmap(1, 1);
|
||||
bmp.SetResolution(100, 100);
|
||||
using Graphics tmpGr = Graphics.FromImage(bmp);
|
||||
tmpGr.TextRenderingHint = TextRenderingHint.AntiAlias;
|
||||
using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul));
|
||||
surface.Canvas.Clear(SKColors.Transparent);
|
||||
|
||||
SizeF size = tmpGr.MeasureString(time, font);
|
||||
// Draw the text at a fixed size into a content-sized image; the window scales it to fit.
|
||||
surface.Canvas.DrawText(time, ContentPadding, -metrics.Ascent + ContentPadding, SKTextAlign.Left, font, paint);
|
||||
|
||||
bmp = new Bitmap((int)size.Width, (int)size.Height);
|
||||
bmp.SetResolution(100, 100);
|
||||
|
||||
using Graphics gr = Graphics.FromImage(bmp);
|
||||
using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
|
||||
|
||||
gr.TextRenderingHint = TextRenderingHint.AntiAlias;
|
||||
gr.DrawString(time, font, brush, 0, 0);
|
||||
|
||||
return bmp;
|
||||
return surface.Snapshot();
|
||||
}
|
||||
|
||||
public override void OnThemeChanged()
|
||||
private string GetTime()
|
||||
{
|
||||
themeChanged = true;
|
||||
return displaySecondsCheckBox.Value ? DateTime.Now.ToLongTimeString() : DateTime.Now.ToShortTimeString();
|
||||
}
|
||||
|
||||
public override void OnSettingsChanged()
|
||||
{
|
||||
themeChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace DesktopMagic.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Periodically checks whether plugin windows are occluded by other top-level windows and pauses
|
||||
/// their rendering while they are (a coverage-based pause algorithm, same idea as Lively's).
|
||||
/// Pausing stops the plugin update timer, which stops re-rendering and re-uploading frames to the
|
||||
/// GPU, so fully covered widgets consume almost no resources.
|
||||
/// </summary>
|
||||
internal sealed class OcclusionMonitor : IDisposable
|
||||
{
|
||||
private const int CheckIntervalMs = 100;
|
||||
|
||||
// Pause only when essentially the whole window is covered so widgets stay responsive even
|
||||
// when a corner is overlapped by another window.
|
||||
private const double CoverageThreshold = 0.95;
|
||||
|
||||
private readonly System.Timers.Timer timer = new(CheckIntervalMs);
|
||||
private readonly Dictionary<PluginWindow, bool> lastPaused = [];
|
||||
private readonly object gate = new();
|
||||
private bool disposed;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timer.Elapsed += Timer_Elapsed;
|
||||
timer.Start();
|
||||
IsRunning = true;
|
||||
}
|
||||
|
||||
private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// System.Timers.Timer can re-enter this handler on another ThreadPool thread when the
|
||||
// previous invocation is still blocked in Dispatcher.Invoke, so serialize the whole pass.
|
||||
if (!Monitor.TryEnter(gate))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RunOcclusionPass();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Exit(gate);
|
||||
}
|
||||
}
|
||||
|
||||
private void RunOcclusionPass()
|
||||
{
|
||||
uint currentProcessId = (uint)Environment.ProcessId;
|
||||
|
||||
// The manager mutates PluginWindows on the UI thread, and the window handle can only be
|
||||
// obtained there, so snapshot (window, handle) pairs over the dispatcher.
|
||||
List<(PluginWindow Window, nint Hwnd)> windows;
|
||||
try
|
||||
{
|
||||
windows = Application.Current.Dispatcher.Invoke(() => Manager.Instance.PluginWindows
|
||||
.OfType<PluginWindow>()
|
||||
.Where(window => window.IsRunning)
|
||||
.Select(window => (Window: window, Hwnd: new WindowInteropHelper(window).Handle))
|
||||
.Where(item => item.Hwnd != nint.Zero)
|
||||
.ToList());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<PluginWindow> live = [.. windows.Select(item => item.Window)];
|
||||
|
||||
foreach ((PluginWindow window, nint hwnd) in windows)
|
||||
{
|
||||
bool paused;
|
||||
|
||||
try
|
||||
{
|
||||
if (!W32.GetWindowRect(hwnd, out W32.RECT windowRect))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Rectangle windowArea = windowRect;
|
||||
double coverage = ComputeCoverage(windowArea, hwnd, currentProcessId);
|
||||
bool foregroundCovers = IsForegroundCovering(windowArea, hwnd);
|
||||
|
||||
paused = foregroundCovers || coverage >= CoverageThreshold;
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastPaused.TryGetValue(window, out bool previous) && previous == paused)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lastPaused[window] = paused;
|
||||
|
||||
try
|
||||
{
|
||||
_ = window.Dispatcher.InvokeAsync(() => window.SetRenderPaused(paused));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Window is closing; ignore.
|
||||
}
|
||||
}
|
||||
|
||||
// Drop tracking for windows that no longer exist.
|
||||
foreach (PluginWindow stale in lastPaused.Keys.Where(key => !live.Contains(key)).ToArray())
|
||||
{
|
||||
_ = lastPaused.Remove(stale);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes how much of <paramref name="target"/> (in virtual screen coordinates) is covered by
|
||||
/// visible top-level windows from other processes that are above it in z-order.
|
||||
/// </summary>
|
||||
private static double ComputeCoverage(Rectangle target, nint selfHwnd, uint currentProcessId)
|
||||
{
|
||||
List<Rectangle> coveringRects = [];
|
||||
|
||||
_ = W32.EnumWindows((hwnd, _) =>
|
||||
{
|
||||
if (hwnd == selfHwnd || !W32.IsWindowVisible(hwnd))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only windows from other processes occlude our widgets; only non-cloaked windows are
|
||||
// actually visible (exclusive-fullscreen games cloak the desktop).
|
||||
if (W32.GetWindowThreadProcessId(hwnd, out uint processId) == 0
|
||||
|| processId == currentProcessId
|
||||
|| IsCloaked(hwnd)
|
||||
|| !W32.GetWindowRect(hwnd, out W32.RECT rect))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Explorer's desktop/wallpaper host windows (WorkerW, Progman, ...) report as visible
|
||||
// and span the whole screen but never occlude anything, so ignore them.
|
||||
if (IsDesktopShellWindow(hwnd))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fully transparent layered overlays (game overlays etc.) span the whole screen but
|
||||
// cannot be seen, so they never occlude anything.
|
||||
if (IsFullyTransparent(hwnd))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ignore windows that are behind ours in z-order.
|
||||
if (!IsWindowAbove(hwnd, selfHwnd))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Rectangle intersection = Rectangle.Intersect(target, rect);
|
||||
if (intersection.Width > 0 && intersection.Height > 0)
|
||||
{
|
||||
coveringRects.Add(intersection);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, nint.Zero);
|
||||
|
||||
if (coveringRects.Count == 0 || target.Width <= 0 || target.Height <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return UnionArea(coveringRects) / (double)(target.Width * target.Height);
|
||||
}
|
||||
|
||||
private static bool IsForegroundCovering(Rectangle target, nint selfHwnd)
|
||||
{
|
||||
nint foreground = W32.GetForegroundWindow();
|
||||
if (foreground == nint.Zero || foreground == selfHwnd)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (W32.GetWindowThreadProcessId(foreground, out _) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// A maximized/fullscreen app from another process covering the widget pauses it.
|
||||
_ = W32.GetWindowRect(foreground, out W32.RECT rect);
|
||||
Rectangle intersection = Rectangle.Intersect(target, rect);
|
||||
|
||||
return target.Width > 0 && target.Height > 0
|
||||
&& intersection.Width >= target.Width * CoverageThreshold
|
||||
&& intersection.Height >= target.Height * CoverageThreshold;
|
||||
}
|
||||
|
||||
private static bool IsCloaked(nint hwnd)
|
||||
{
|
||||
return W32.DwmGetWindowAttribute(hwnd, W32.DWMWA_CLOAKED, out int cloaked, sizeof(int)) == 0 && cloaked != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true for layered windows that are fully transparent (alpha 0), i.e. invisible
|
||||
/// overlay hosts that span the screen but never visibly occlude anything.
|
||||
/// </summary>
|
||||
private static bool IsFullyTransparent(nint hwnd)
|
||||
{
|
||||
if ((W32.GetWindowLong(hwnd, W32.GWL_EXSTYLE) & W32.WS_EX_LAYERED) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return W32.GetLayeredWindowAttributes(hwnd, out _, out byte alpha, out int flags)
|
||||
&& (flags & W32.LWA_ALPHA) != 0
|
||||
&& alpha == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true for the Explorer desktop/wallpaper host windows that span the whole screen but
|
||||
/// never visually occlude anything (wallpaper, hidden desktop icon layer, ...).
|
||||
/// </summary>
|
||||
private static bool IsDesktopShellWindow(nint hwnd)
|
||||
{
|
||||
StringBuilder className = new(256);
|
||||
_ = W32.GetClassName(hwnd, className, className.Capacity);
|
||||
|
||||
return className.ToString() is "WorkerW" or "Progman" or "SHELLDLL_DefView" or "SysListView32";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when <paramref name="other"/> is above <paramref name="self"/> in z-order, i.e.
|
||||
/// when walking toward the front (GW_HWNDPREV) from <paramref name="self"/> we reach
|
||||
/// <paramref name="other"/>.
|
||||
/// </summary>
|
||||
private static bool IsWindowAbove(nint other, nint self)
|
||||
{
|
||||
nint current = self;
|
||||
while ((current = W32.GetWindow(current, W32.GW_HWNDPREV)) != nint.Zero)
|
||||
{
|
||||
if (current == other)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total (union) area of <paramref name="rects"/> using a y-scanline sweep so
|
||||
/// overlapping windows are not double counted.
|
||||
/// </summary>
|
||||
private static long UnionArea(List<Rectangle> rects)
|
||||
{
|
||||
if (rects.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<int> yEdges = [];
|
||||
foreach (Rectangle rect in rects)
|
||||
{
|
||||
yEdges.Add(rect.Top);
|
||||
yEdges.Add(rect.Bottom);
|
||||
}
|
||||
yEdges.Sort();
|
||||
|
||||
long area = 0;
|
||||
for (int i = 0; i < yEdges.Count - 1; i++)
|
||||
{
|
||||
int y0 = yEdges[i];
|
||||
int y1 = yEdges[i + 1];
|
||||
if (y0 == y1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect the x extents of all rects that span this band, then merge them.
|
||||
List<(int Left, int Right)> xRanges = [];
|
||||
foreach (Rectangle rect in rects)
|
||||
{
|
||||
if (rect.Top <= y0 && rect.Bottom >= y1)
|
||||
{
|
||||
xRanges.Add((rect.Left, rect.Right));
|
||||
}
|
||||
}
|
||||
|
||||
if (xRanges.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
xRanges.Sort((a, b) => a.Left.CompareTo(b.Left));
|
||||
int mergedLeft = xRanges[0].Left;
|
||||
int mergedRight = xRanges[0].Right;
|
||||
long bandWidth = 0;
|
||||
|
||||
for (int j = 1; j < xRanges.Count; j++)
|
||||
{
|
||||
if (xRanges[j].Left <= mergedRight)
|
||||
{
|
||||
mergedRight = Math.Max(mergedRight, xRanges[j].Right);
|
||||
}
|
||||
else
|
||||
{
|
||||
bandWidth += mergedRight - mergedLeft;
|
||||
mergedLeft = xRanges[j].Left;
|
||||
mergedRight = xRanges[j].Right;
|
||||
}
|
||||
}
|
||||
bandWidth += mergedRight - mergedLeft;
|
||||
|
||||
area += bandWidth * (y1 - y0);
|
||||
}
|
||||
|
||||
return area;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
timer.Elapsed -= Timer_Elapsed;
|
||||
timer.Stop();
|
||||
timer.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,18 @@ internal class W32
|
||||
|
||||
public const int SM_CYSCREEN = 1;
|
||||
|
||||
public const int DWMWA_CLOAKED = 14;
|
||||
|
||||
public const int GWL_EXSTYLE = -20;
|
||||
|
||||
public const uint GW_HWNDNEXT = 2;
|
||||
|
||||
public const uint GW_HWNDPREV = 3;
|
||||
|
||||
public const int LWA_ALPHA = 0x2;
|
||||
|
||||
public const long WS_EX_LAYERED = 0x00080000L;
|
||||
|
||||
public const int SPI_SETDESKWALLPAPER = 20;
|
||||
public const int SPIF_SENDWININICHANGE = 0x02;
|
||||
public const int SPIF_UPDATEINIFILE = 0x01;
|
||||
@@ -55,6 +67,26 @@ internal class W32
|
||||
[DllImport("dwmapi.dll")]
|
||||
public static extern int DwmSetWindowAttribute(nint hwnd, int attr, ref int attrValue, int attrSize);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
public static extern int DwmGetWindowAttribute(nint hwnd, int dwAttribute, out int pvAttribute, int cbAttribute);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetWindowRect(nint hWnd, out RECT lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int GetWindowLong(nint hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetLayeredWindowAttributes(nint hWnd, out int colorKey, out byte alpha, out int dwFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern nint GetWindow(nint hWnd, uint uCmd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam);
|
||||
|
||||
@@ -19,6 +19,7 @@ public partial class MainWindow : FluentWindow
|
||||
{
|
||||
private readonly Manager _manager = Manager.Instance;
|
||||
private readonly MainWindowDataContext _mainWindowDataContext = new();
|
||||
private OcclusionMonitor? occlusionMonitor;
|
||||
|
||||
[Obsolete]
|
||||
public MainWindow()
|
||||
@@ -49,6 +50,10 @@ public partial class MainWindow : FluentWindow
|
||||
_manager.LoadPlugins();
|
||||
_manager.LoadLayout();
|
||||
|
||||
// Pause plugin rendering while widgets are fully occluded by other windows.
|
||||
occlusionMonitor = new OcclusionMonitor();
|
||||
occlusionMonitor.Start();
|
||||
|
||||
if (_manager.Settings.IsFirstRun)
|
||||
{
|
||||
_manager.Settings.IsFirstRun = false;
|
||||
@@ -103,6 +108,9 @@ public partial class MainWindow : FluentWindow
|
||||
{
|
||||
Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged;
|
||||
|
||||
occlusionMonitor?.Dispose();
|
||||
occlusionMonitor = null;
|
||||
|
||||
Visibility = Visibility.Collapsed;
|
||||
UpdateLayout();
|
||||
_manager.CloseAllPluginWindows();
|
||||
|
||||
@@ -24,5 +24,10 @@ public interface IPluginWindow
|
||||
void ApplySettingValue(string id, string value);
|
||||
void ApplyButtonClick(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Pauses or resumes rendering of the plugin window (e.g. when it is fully occluded by other windows).
|
||||
/// </summary>
|
||||
void SetRenderPaused(bool paused);
|
||||
|
||||
event System.ComponentModel.CancelEventHandler Closing;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using DesktopMagic.Api;
|
||||
using DesktopMagic.Settings;
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
using Wpf.Ui.Controls;
|
||||
@@ -12,11 +13,23 @@ namespace DesktopMagic.Plugins;
|
||||
internal class PluginData(PluginWindow window, PluginSettings pluginSettings) : IPluginData
|
||||
{
|
||||
private readonly PluginWindow window = window;
|
||||
private Size windowPixelSize = new(1, 1);
|
||||
|
||||
public ITheme Theme => pluginSettings.Theme;
|
||||
|
||||
public Size WindowSize => new Size((int)window.ActualWidth, (int)window.ActualHeight);
|
||||
|
||||
public Size WindowPixelSize => windowPixelSize;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the cached device-pixel window size. Must be called from the UI thread
|
||||
/// so the value can be read safely by plugins on their render thread.
|
||||
/// </summary>
|
||||
internal void UpdateWindowPixelSize(int width, int height)
|
||||
{
|
||||
windowPixelSize = new Size(Math.Max(1, width), Math.Max(1, height));
|
||||
}
|
||||
|
||||
public Point WindowPosition => new Point((int)window.Left, (int)window.Top);
|
||||
|
||||
public string PluginName => window.PluginMetadata.Name;
|
||||
|
||||
@@ -40,12 +40,14 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
private Thread? pluginThread;
|
||||
private System.Timers.Timer? updateTimer;
|
||||
private Plugin? pluginClassInstance;
|
||||
private PluginData? pluginData;
|
||||
private AssemblyLoadContext assemblyLoadContext;
|
||||
|
||||
private readonly Rectangle screenBounds;
|
||||
private readonly string screenDeviceName;
|
||||
private bool isUpdatingPosition = false;
|
||||
private bool _editMode = false;
|
||||
private volatile bool renderPaused = false;
|
||||
|
||||
private CancellationTokenSource? pluginCancellationTokenSource;
|
||||
private FileSystemWatcher? pluginFileWatcher;
|
||||
@@ -55,6 +57,18 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
private WriteableBitmap? writeableBitmap;
|
||||
private BitmapScalingMode lastBitmapScalingMode = BitmapScalingMode.Unspecified;
|
||||
|
||||
// Last presented frame bytes; used to skip re-uploading identical frames so WPF does not
|
||||
// re-upload the texture or re-composite the window (GPU stays idle for static widgets).
|
||||
private byte[]? lastFramePixels;
|
||||
private int lastFrameWidth;
|
||||
private int lastFrameHeight;
|
||||
private int lastFrameStride;
|
||||
|
||||
// Guards against overlapping render ticks. The timer can re-enter while an async
|
||||
// render awaits the UI thread, which would run the plugin concurrently and present
|
||||
// frames out of order. Only one render may be in flight at a time.
|
||||
private long renderInProgress;
|
||||
|
||||
// Event handlers on long-lived settings objects, tracked so they can be unsubscribed on close.
|
||||
private readonly PropertyChangedEventHandler settingsPropertyChangedHandler;
|
||||
private readonly PropertyChangedEventHandler themePropertyChangedHandler;
|
||||
@@ -334,6 +348,21 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
}
|
||||
}
|
||||
|
||||
public void SetRenderPaused(bool paused)
|
||||
{
|
||||
renderPaused = paused;
|
||||
|
||||
if (paused)
|
||||
{
|
||||
updateTimer?.Stop();
|
||||
}
|
||||
else if (IsRunning && pluginClassInstance is { UpdateInterval: > 0 } && updateTimer is not null)
|
||||
{
|
||||
updateTimer.Interval = pluginClassInstance.UpdateInterval;
|
||||
updateTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePluginWindow()
|
||||
{
|
||||
UpdateTimer_Elapsed(updateTimer, null);
|
||||
@@ -400,8 +429,28 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
|
||||
try
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
int stride = bitmapData.Stride;
|
||||
int height = bitmapData.Height;
|
||||
int byteCount = stride * height;
|
||||
|
||||
unsafe
|
||||
{
|
||||
byte* scan0 = (byte*)bitmapData.Scan0.ToPointer();
|
||||
|
||||
// Skip the present when the frame is byte-identical to the last presented one so WPF
|
||||
// does not re-upload the texture or re-composite the window (GPU stays idle for
|
||||
// static widgets that keep ticking).
|
||||
if (lastFramePixels is { Length: > 0 }
|
||||
&& lastFrameWidth == bitmapData.Width
|
||||
&& lastFrameHeight == height
|
||||
&& lastFrameStride == stride
|
||||
&& lastFramePixels.AsSpan().SequenceEqual(new ReadOnlySpan<byte>(scan0, byteCount)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (lastBitmapScalingMode != scalingMode)
|
||||
{
|
||||
RenderOptions.SetBitmapScalingMode(image, scalingMode);
|
||||
@@ -410,9 +459,19 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
|
||||
if (writeableBitmap == null || writeableBitmap.PixelWidth != bitmapData.Width || writeableBitmap.PixelHeight != bitmapData.Height)
|
||||
{
|
||||
// A frame sized to the window maps 1:1 to the display when the bitmap uses the
|
||||
// actual monitor DPI, so WPF skips the (expensive) per-frame scaling filter.
|
||||
// Smaller content-sized frames keep 96 DPI to preserve their on-screen semantics.
|
||||
DpiScale dpi = VisualTreeHelper.GetDpi(this);
|
||||
bool fullWindowFrame = pluginData != null
|
||||
&& bitmapData.Width == pluginData.WindowPixelSize.Width
|
||||
&& bitmapData.Height == pluginData.WindowPixelSize.Height;
|
||||
double dpiX = fullWindowFrame ? dpi.DpiScaleX * 96 : bitmap.HorizontalResolution;
|
||||
double dpiY = fullWindowFrame ? dpi.DpiScaleY * 96 : bitmap.VerticalResolution;
|
||||
|
||||
writeableBitmap = new WriteableBitmap(
|
||||
bitmapData.Width, bitmapData.Height,
|
||||
bitmap.HorizontalResolution, bitmap.VerticalResolution,
|
||||
dpiX, dpiY,
|
||||
PixelFormats.Bgra32, null);
|
||||
image.Source = writeableBitmap;
|
||||
}
|
||||
@@ -434,7 +493,17 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
{
|
||||
writeableBitmap.Unlock();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (lastFramePixels is null || lastFramePixels.Length != byteCount)
|
||||
{
|
||||
lastFramePixels = new byte[byteCount];
|
||||
}
|
||||
lastFrameWidth = bitmapData.Width;
|
||||
lastFrameHeight = height;
|
||||
lastFrameStride = stride;
|
||||
new ReadOnlySpan<byte>(scan0, byteCount).CopyTo(lastFramePixels);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -560,7 +629,8 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
if (instance is Plugin plugin)
|
||||
{
|
||||
pluginClassInstance = plugin;
|
||||
pluginClassInstance.Application = new PluginData(this, settings);
|
||||
pluginData = new PluginData(this, settings);
|
||||
pluginClassInstance.Application = pluginData;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -589,6 +659,8 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
};
|
||||
updateTimer.Elapsed += UpdateTimer_Elapsed;
|
||||
|
||||
await Dispatcher.InvokeAsync(RefreshWindowPixelSize);
|
||||
|
||||
pluginClassInstance.Start();
|
||||
if (pluginClassInstance is AsyncPlugin asyncPluginStart)
|
||||
{
|
||||
@@ -599,7 +671,7 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
UpdatePluginWindow();
|
||||
await Dispatcher.InvokeAsync(ThemeChanged);
|
||||
|
||||
if (pluginClassInstance.UpdateInterval > 0)
|
||||
if (pluginClassInstance.UpdateInterval > 0 && !renderPaused)
|
||||
{
|
||||
updateTimer.Interval = pluginClassInstance.UpdateInterval;
|
||||
updateTimer.Start();
|
||||
@@ -963,6 +1035,13 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop the tick when a render is still in flight instead of running the plugin
|
||||
// concurrently (the timer can re-enter while the async render awaits the UI thread).
|
||||
if (Interlocked.CompareExchange(ref renderInProgress, 1, 0) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (plugin is SkiaPlugin or SkiaAsyncPlugin)
|
||||
@@ -1034,6 +1113,10 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
});
|
||||
Exit();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref renderInProgress, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RenderSkiaFrame()
|
||||
@@ -1045,62 +1128,128 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
return;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
width = Math.Max(1, (int)ActualWidth);
|
||||
height = Math.Max(1, (int)ActualHeight);
|
||||
});
|
||||
|
||||
using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul));
|
||||
surface.Canvas.Clear(SKColors.Transparent);
|
||||
SKImage? skImage;
|
||||
|
||||
if (plugin is SkiaAsyncPlugin skiaAsyncPlugin)
|
||||
{
|
||||
CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
await skiaAsyncPlugin.MainAsync(surface.Canvas, token);
|
||||
skImage = await skiaAsyncPlugin.RenderAsync(token);
|
||||
}
|
||||
else if (plugin is SkiaPlugin skiaPlugin)
|
||||
{
|
||||
skiaPlugin.Main(surface.Canvas);
|
||||
skImage = skiaPlugin.Render();
|
||||
}
|
||||
|
||||
SKPixmap? pixmap = surface.PeekPixels();
|
||||
|
||||
if (pixmap is null)
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
// Null keeps the previous frame, matching how GDI plugins return null from Main().
|
||||
if (skImage is null)
|
||||
{
|
||||
if (writeableBitmap is null || writeableBitmap.PixelWidth != width || writeableBitmap.PixelHeight != height)
|
||||
return;
|
||||
}
|
||||
|
||||
using (skImage)
|
||||
{
|
||||
using SKImage raster = skImage.ToRasterImage();
|
||||
using SKPixmap? pixmap = raster.PeekPixels();
|
||||
|
||||
if (pixmap is null)
|
||||
{
|
||||
writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
|
||||
image.Source = writeableBitmap;
|
||||
return;
|
||||
}
|
||||
|
||||
writeableBitmap.Lock();
|
||||
try
|
||||
int width = pixmap.Width;
|
||||
int height = pixmap.Height;
|
||||
int byteCount = pixmap.RowBytes * height;
|
||||
|
||||
// Skip the present when the frame is byte-identical to the last presented one so WPF
|
||||
// does not re-upload the texture or re-composite the window (GPU stays idle for
|
||||
// static widgets that keep ticking).
|
||||
if (lastFramePixels is { Length: > 0 }
|
||||
&& lastFrameWidth == width
|
||||
&& lastFrameHeight == height
|
||||
&& lastFrameStride == pixmap.RowBytes
|
||||
&& lastFramePixels.AsSpan().SequenceEqual(pixmap.GetPixelSpan()))
|
||||
{
|
||||
unsafe
|
||||
return;
|
||||
}
|
||||
|
||||
BitmapScalingMode scalingMode = plugin.RenderQuality switch
|
||||
{
|
||||
RenderQuality.High => BitmapScalingMode.HighQuality,
|
||||
RenderQuality.Low => BitmapScalingMode.LowQuality,
|
||||
RenderQuality.Performance => BitmapScalingMode.NearestNeighbor,
|
||||
_ => BitmapScalingMode.Unspecified
|
||||
};
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (lastBitmapScalingMode != scalingMode)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
pixmap.GetPixels().ToPointer(),
|
||||
writeableBitmap.BackBuffer.ToPointer(),
|
||||
writeableBitmap.BackBufferStride * height,
|
||||
pixmap.RowBytes * pixmap.Height);
|
||||
RenderOptions.SetBitmapScalingMode(image, scalingMode);
|
||||
lastBitmapScalingMode = scalingMode;
|
||||
}
|
||||
|
||||
writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
|
||||
}
|
||||
finally
|
||||
if (writeableBitmap is null || writeableBitmap.PixelWidth != width || writeableBitmap.PixelHeight != height)
|
||||
{
|
||||
// A frame sized to the window maps 1:1 to the display when the bitmap uses the
|
||||
// actual monitor DPI, so WPF skips the (expensive) per-frame scaling filter.
|
||||
// Smaller content-sized frames keep 96 DPI to preserve their on-screen semantics.
|
||||
DpiScale dpi = VisualTreeHelper.GetDpi(this);
|
||||
bool fullWindowFrame = pluginData != null
|
||||
&& width == pluginData.WindowPixelSize.Width
|
||||
&& height == pluginData.WindowPixelSize.Height;
|
||||
double dpiX = fullWindowFrame ? dpi.DpiScaleX * 96 : 96;
|
||||
double dpiY = fullWindowFrame ? dpi.DpiScaleY * 96 : 96;
|
||||
|
||||
// Skia outputs premultiplied alpha, so use Pbgra32 (premultiplied BGRA).
|
||||
// Bgra32 is straight alpha and would render semi-transparent pixels too dark.
|
||||
writeableBitmap = new WriteableBitmap(width, height, dpiX, dpiY, PixelFormats.Pbgra32, null);
|
||||
image.Source = writeableBitmap;
|
||||
}
|
||||
|
||||
writeableBitmap.Lock();
|
||||
try
|
||||
{
|
||||
int destStride = writeableBitmap.BackBufferStride;
|
||||
int srcStride = pixmap.RowBytes;
|
||||
int copyWidth = Math.Min(srcStride, destStride);
|
||||
int copyRows = Math.Min(pixmap.Height, height);
|
||||
|
||||
unsafe
|
||||
{
|
||||
byte* src = (byte*)pixmap.GetPixels().ToPointer();
|
||||
byte* dst = (byte*)writeableBitmap.BackBuffer.ToPointer();
|
||||
|
||||
for (int row = 0; row < copyRows; row++)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
src + (row * srcStride),
|
||||
dst + (row * destStride),
|
||||
destStride,
|
||||
copyWidth);
|
||||
}
|
||||
}
|
||||
|
||||
writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
|
||||
}
|
||||
finally
|
||||
{
|
||||
writeableBitmap.Unlock();
|
||||
}
|
||||
});
|
||||
|
||||
if (lastFramePixels is null || lastFramePixels.Length != byteCount)
|
||||
{
|
||||
writeableBitmap.Unlock();
|
||||
lastFramePixels = new byte[byteCount];
|
||||
}
|
||||
});
|
||||
lastFrameWidth = width;
|
||||
lastFrameHeight = height;
|
||||
lastFrameStride = pixmap.RowBytes;
|
||||
pixmap.GetPixelSpan().CopyTo(lastFramePixels);
|
||||
}
|
||||
}
|
||||
|
||||
private void Border_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
@@ -1257,6 +1406,7 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
}
|
||||
|
||||
settings.Position = ScreenUtilities.PositionToPercent(new System.Windows.Point(Left, Top), screenBounds);
|
||||
RefreshWindowPixelSize();
|
||||
}
|
||||
|
||||
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
@@ -1267,6 +1417,13 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
}
|
||||
|
||||
tileBar.CaptionHeight = ActualHeight - 10;
|
||||
RefreshWindowPixelSize();
|
||||
}
|
||||
|
||||
private void RefreshWindowPixelSize()
|
||||
{
|
||||
DpiScale dpi = VisualTreeHelper.GetDpi(this);
|
||||
pluginData?.UpdateWindowPixelSize((int)Math.Ceiling(ActualWidth * dpi.DpiScaleX), (int)Math.Ceiling(ActualHeight * dpi.DpiScaleY));
|
||||
}
|
||||
|
||||
private void Image_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
@@ -1275,8 +1432,15 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
|
||||
ImageSource imageSource = image.Source;
|
||||
BitmapSource bitmapImage = (BitmapSource)imageSource;
|
||||
double pixelMousePositionX = e.GetPosition(image).X * bitmapImage.PixelWidth / image.ActualHeight;
|
||||
double pixelMousePositionY = e.GetPosition(image).Y * bitmapImage.PixelHeight / image.ActualHeight;
|
||||
double actualWidth = image.ActualWidth;
|
||||
double actualHeight = image.ActualHeight;
|
||||
if (actualWidth <= 0 || actualHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double pixelMousePositionX = e.GetPosition(image).X * bitmapImage.PixelWidth / actualWidth;
|
||||
double pixelMousePositionY = e.GetPosition(image).Y * bitmapImage.PixelHeight / actualHeight;
|
||||
|
||||
MouseButton mouseButton;
|
||||
|
||||
@@ -1306,8 +1470,15 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
{
|
||||
ImageSource imageSource = image.Source;
|
||||
BitmapSource bitmapImage = (BitmapSource)imageSource;
|
||||
double pixelMousePositionX = e.GetPosition(image).X * bitmapImage.PixelWidth / image.ActualHeight;
|
||||
double pixelMousePositionY = e.GetPosition(image).Y * bitmapImage.PixelHeight / image.ActualHeight;
|
||||
double actualWidth = image.ActualWidth;
|
||||
double actualHeight = image.ActualHeight;
|
||||
if (actualWidth <= 0 || actualHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double pixelMousePositionX = e.GetPosition(image).X * bitmapImage.PixelWidth / actualWidth;
|
||||
double pixelMousePositionY = e.GetPosition(image).Y * bitmapImage.PixelHeight / actualHeight;
|
||||
|
||||
System.Drawing.Point point = new System.Drawing.Point((int)pixelMousePositionX, (int)pixelMousePositionY);
|
||||
pluginClassInstance?.OnMouseMove(point);
|
||||
@@ -1317,8 +1488,15 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
{
|
||||
ImageSource imageSource = image.Source;
|
||||
BitmapSource bitmapImage = (BitmapSource)imageSource;
|
||||
double pixelMousePositionX = e.GetPosition(image).X * bitmapImage.PixelWidth / image.ActualHeight;
|
||||
double pixelMousePositionY = e.GetPosition(image).Y * bitmapImage.PixelHeight / image.ActualHeight;
|
||||
double actualWidth = image.ActualWidth;
|
||||
double actualHeight = image.ActualHeight;
|
||||
if (actualWidth <= 0 || actualHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double pixelMousePositionX = e.GetPosition(image).X * bitmapImage.PixelWidth / actualWidth;
|
||||
double pixelMousePositionY = e.GetPosition(image).Y * bitmapImage.PixelHeight / actualHeight;
|
||||
|
||||
System.Drawing.Point point = new System.Drawing.Point((int)pixelMousePositionX, (int)pixelMousePositionY);
|
||||
pluginClassInstance?.OnMouseWheel(point, e.Delta);
|
||||
|
||||
@@ -194,6 +194,23 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
}
|
||||
}
|
||||
|
||||
public void SetRenderPaused(bool paused)
|
||||
{
|
||||
if (webView.CoreWebView2 is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (paused)
|
||||
{
|
||||
_ = webView.CoreWebView2.TrySuspendAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
webView.CoreWebView2.Resume();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
|
||||
@@ -13,10 +13,16 @@ public interface IPluginData
|
||||
ITheme Theme { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the window size of the plugin window.
|
||||
/// Gets the window size of the plugin window in device independent pixels.
|
||||
/// </summary>
|
||||
Size WindowSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the window size of the plugin window in physical (device) pixels.
|
||||
/// Use this to create a rendering surface that maps 1:1 to the display.
|
||||
/// </summary>
|
||||
Size WindowPixelSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the window position of the plugin window.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,14 +8,17 @@ namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for creating asynchronous plugins using SkiaSharp rendering.
|
||||
/// Plugins override <see cref="MainAsync(SKCanvas, CancellationToken)"/> instead of <see cref="Plugin.Main()"/>.
|
||||
/// Plugins override <see cref="RenderAsync(CancellationToken)"/> and return an <see cref="SKImage"/>
|
||||
/// of their own size, just like <see cref="AsyncPlugin.MainAsync(CancellationToken)"/> returns a
|
||||
/// <see cref="Bitmap"/>. The returned image is scaled to fit the plugin window while preserving
|
||||
/// its aspect ratio.
|
||||
/// All existing <see cref="Plugin"/>, <see cref="AsyncPlugin"/>, and <see cref="SkiaPlugin"/> code is unaffected.
|
||||
/// </summary>
|
||||
public abstract class SkiaAsyncPlugin : AsyncPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// This method is sealed and returns null for async Skia plugins.
|
||||
/// Override <see cref="MainAsync(SKCanvas, CancellationToken)"/> instead.
|
||||
/// Override <see cref="RenderAsync(CancellationToken)"/> instead.
|
||||
/// </summary>
|
||||
public sealed override Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -24,8 +27,9 @@ public abstract class SkiaAsyncPlugin : AsyncPlugin
|
||||
|
||||
/// <summary>
|
||||
/// Called every <see cref="Plugin.UpdateInterval"/> milliseconds to render the plugin asynchronously.
|
||||
/// Return <see langword="null"/> to keep the previously rendered image.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The SkiaSharp canvas to draw on.</param>
|
||||
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
|
||||
public abstract Task MainAsync(SKCanvas canvas, CancellationToken cancellationToken);
|
||||
/// <returns>The rendered image, or <see langword="null"/> to keep the previous frame.</returns>
|
||||
public abstract Task<SKImage?> RenderAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for creating plugins using SkiaSharp rendering.
|
||||
/// Plugins override <see cref="Main(SKCanvas)"/> instead of <see cref="Plugin.Main()"/>.
|
||||
/// Plugins override <see cref="Render()"/> and return an <see cref="SKImage"/> of their own size,
|
||||
/// just like <see cref="Plugin.Main()"/> returns a <see cref="Bitmap"/>. The returned image is
|
||||
/// scaled to fit the plugin window while preserving its aspect ratio.
|
||||
/// All existing <see cref="Plugin"/> and <see cref="AsyncPlugin"/> code is unaffected.
|
||||
/// </summary>
|
||||
public abstract class SkiaPlugin : Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// This method is sealed and returns null for Skia plugins.
|
||||
/// Override <see cref="Main(SKCanvas)"/> instead.
|
||||
/// Override <see cref="Render()"/> instead.
|
||||
/// </summary>
|
||||
/// <returns>Always null.</returns>
|
||||
public sealed override Bitmap? Main()
|
||||
@@ -23,7 +25,8 @@ public abstract class SkiaPlugin : Plugin
|
||||
|
||||
/// <summary>
|
||||
/// Called every <see cref="Plugin.UpdateInterval"/> milliseconds to render the plugin.
|
||||
/// Return <see langword="null"/> to keep the previously rendered image.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The SkiaSharp canvas to draw on.</param>
|
||||
public abstract void Main(SKCanvas canvas);
|
||||
/// <returns>The rendered image, or <see langword="null"/> to keep the previous frame.</returns>
|
||||
public abstract SKImage? Render();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user