Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/windows/DesktopHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public DesktopHost(Application app)
new WindowInteropHelper(window).EnsureHandle();
NativeWindow.UseHardwareTransparency(window);
NativeWindow.SetInteractive(window, false);
window.ContentRendered += (_, _) => NativeWindow.UseHardwareTransparency(window);
window.ContentRendered += (_, _) => { NativeWindow.UseHardwareTransparency(window); NativeWindow.SetInteractive(window, adjusting); };
SwitchVisualization(preferences.Visualization);
tray = new Forms.NotifyIcon { Text = "Miucs", ContextMenuStrip = menu };
trayAnimation = new AnimatedTrayIcon(tray, () => AdvanceCatHead(Now));
Expand Down
81 changes: 76 additions & 5 deletions apps/windows/NativeWindow.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
Expand All @@ -9,14 +10,18 @@ namespace Miucs.Windows;
internal static class NativeWindow
{
private const int ExtendedStyle = -20;
private const long ToolWindow = 0x80, Transparent = 0x20, NoActivate = 0x08000000;
private const int HitTestMessage = 0x0084;
private const int StyleChangingMessage = 0x007C;
private const long ToolWindow = 0x80, Transparent = 0x20, NoActivate = 0x08000000, Layered = 0x00080000;
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtrW")] private static extern IntPtr GetWindowLongPtr(IntPtr hwnd, int index);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW")] private static extern IntPtr SetWindowLongPtr(IntPtr hwnd, int index, IntPtr value);
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hwnd, IntPtr insertAfter, int x, int y, int cx, int cy, uint flags);
[DllImport("user32.dll")] private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags);
[DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool GetMonitorInfo(IntPtr monitor, ref MonitorInfo info);
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hwnd, int message, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)] private struct Rect { public int Left, Top, Right, Bottom; }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private struct MonitorInfo { public int Size; public Rect Monitor, Work; public uint Flags; }
[StructLayout(LayoutKind.Sequential)] private struct StyleChange { public int OldStyle, NewStyle; }

[StructLayout(LayoutKind.Sequential)] private struct CursorPoint { public int X, Y; }
[DllImport("user32.dll")] private static extern bool GetCursorPos(out CursorPoint point);
Expand All @@ -27,6 +32,13 @@ internal static class NativeWindow
[DllImport("dcomp.dll")] private static extern uint DCompositionWaitForCompositorClock(uint count, IntPtr[]? handles, uint timeout);
[StructLayout(LayoutKind.Sequential)] private struct Margins { public int Left, Right, Top, Bottom; }

private sealed class PassThrough
{
public bool Interactive;
}

private static readonly ConditionalWeakTable<Window, PassThrough> PassThroughWindows = new();

public static bool TryGetCursorPosition(out Point point)
{
bool found = GetCursorPos(out var native);
Expand All @@ -43,6 +55,20 @@ public static void PositionControl(Window window, Point origin, double width, do
internal static bool IsClickThrough(Window window) =>
(GetWindowLongPtr(new WindowInteropHelper(window).Handle, ExtendedStyle).ToInt64() & Transparent) != 0;

internal static bool IsLayered(Window window) =>
(GetWindowLongPtr(new WindowInteropHelper(window).Handle, ExtendedStyle).ToInt64() & Layered) != 0;

internal static long ExStyle(IntPtr hwnd) => GetWindowLongPtr(hwnd, ExtendedStyle).ToInt64();

internal static int HitTest(Window window, int screenX, int screenY) =>
HitTestHandle(new WindowInteropHelper(window).Handle, screenX, screenY);

internal static int HitTestHandle(IntPtr hwnd, int screenX, int screenY)
{
var packed = new IntPtr((screenY << 16) | (screenX & 0xFFFF));
return (int)SendMessage(hwnd, HitTestMessage, IntPtr.Zero, packed).ToInt64();
}

public static void UseHardwareTransparency(Window window)
{
var hwnd = new WindowInteropHelper(window).Handle;
Expand All @@ -69,13 +95,58 @@ public static void WaitForFrame()
DCompositionWaitForCompositorClock(0, null, 34);
}

internal static void PassThroughSurface(HwndSource source)
{
var pass = new PassThrough { Interactive = false };
source.AddHook((IntPtr hwnd, int message, IntPtr wParam, IntPtr lParam, ref bool handled) =>
FilterPassThrough(message, wParam, lParam, ref handled, pass));
ApplyPassThroughStyle(source.Handle, false, false);
}

public static void SetInteractive(Window window, bool interactive)
{
var hwnd = new WindowInteropHelper(window).Handle;
long style = GetWindowLongPtr(hwnd, ExtendedStyle).ToInt64() | ToolWindow | NoActivate;
style = interactive ? style & ~Transparent : style | Transparent;
var hwnd = new WindowInteropHelper(window).EnsureHandle();
var pass = PassThroughWindows.GetValue(window, current =>
{
var created = new PassThrough();
if (HwndSource.FromHwnd(new WindowInteropHelper(current).Handle) is HwndSource source)
source.AddHook((IntPtr hookHwnd, int message, IntPtr wParam, IntPtr lParam, ref bool handled) =>
FilterPassThrough(message, wParam, lParam, ref handled, created));
return created;
});
pass.Interactive = interactive;
ApplyPassThroughStyle(hwnd, interactive, true);
}

private static IntPtr FilterPassThrough(int message, IntPtr wParam, IntPtr lParam, ref bool handled, PassThrough pass)
{
if (message == StyleChangingMessage && wParam.ToInt64() == ExtendedStyle)
{
var change = Marshal.PtrToStructure<StyleChange>(lParam);
change.NewStyle = WithPassThrough(change.NewStyle, pass.Interactive);
Marshal.StructureToPtr(change, lParam, false);
handled = true;
return IntPtr.Zero;
}
if (message != HitTestMessage || pass.Interactive) return IntPtr.Zero;
handled = true;
return new IntPtr(-1);
}

private static int WithPassThrough(int style, bool interactive)
{
style |= (int)(ToolWindow | NoActivate | Layered);
return interactive ? style & ~(int)Transparent : style | (int)Transparent;
}

private static void ApplyPassThroughStyle(IntPtr hwnd, bool interactive, bool topmost)
{
int style = WithPassThrough(unchecked((int)GetWindowLongPtr(hwnd, ExtendedStyle).ToInt64()), interactive);
SetWindowLongPtr(hwnd, ExtendedStyle, new IntPtr(style));
SetWindowPos(hwnd, new IntPtr(-1), 0, 0, 0, 0, 0x0001 | 0x0002 | 0x0010 | 0x0020);
uint flags = 0x0001 | 0x0002 | 0x0010 | 0x0020;
var after = new IntPtr(-1);
if (!topmost) { flags |= 0x0004; after = IntPtr.Zero; }
SetWindowPos(hwnd, after, 0, 0, 0, 0, flags);
}

public static WindowFrame WorkArea(Window window)
Expand Down
43 changes: 43 additions & 0 deletions apps/windows/WindowsSmokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Miucs.Audio;
using Miucs.Keyboard;
Expand All @@ -17,6 +18,7 @@ public static void Run(string output)
Directory.CreateDirectory(output);
VerifyLockGlyph(output);
VerifyHoverLockWindow();
VerifyHardwareClickThrough();
VerifyTrayCat(output);
VerifyPluginRendering();
File.WriteAllText(Path.Combine(output, "result.json"), "{\"pluginSurface\":\"passed\",\"realLoopback\":\"not-run\",\"realKeyboard\":\"not-run\",\"syntheticHoverWindow\":\"passed\"}\n");
Expand Down Expand Up @@ -113,6 +115,13 @@ private static void VerifyLockGlyph(string output)
}
}


private static int HitAtCenter(Window window)
{
var point = window.PointToScreen(new Point(window.ActualWidth / 2, window.ActualHeight / 2));
return NativeWindow.HitTest(window, (int)Math.Round(point.X), (int)Math.Round(point.Y));
}

private static void VerifyHoverLockWindow()
{
var owner = new Window
Expand All @@ -139,6 +148,7 @@ private static void VerifyHoverLockWindow()
hover.Sample(3, true);
Assert(hover.TargetWindow.IsVisible, "visible at 3 seconds");
Assert(NativeWindow.IsClickThrough(owner), "revealing does not intercept canvas");
Assert(HitAtCenter(owner) == -1, "canvas hit test passes through");
Assert(!NativeWindow.IsClickThrough(hover.TargetWindow), "only lock target interactive");
Assert(hover.TargetWindow.ActualWidth >= 32 && hover.TargetWindow.ActualWidth <= 36 &&
hover.TargetWindow.ActualHeight >= 32 && hover.TargetWindow.ActualHeight <= 36,
Expand All @@ -157,6 +167,7 @@ private static void VerifyHoverLockWindow()
hover.Sample(3.1, false);
Assert(!hover.TargetWindow.IsVisible, "leave hides target");
adjusting = true; NativeWindow.SetInteractive(owner, true); hover.SetLocked(false);
Assert(HitAtCenter(owner) == 1, "adjusting canvas accepts the pointer");
Assert(hover.TargetWindow.IsVisible, "menu unlock immediately reveals target");
hover.ClickForVerification(); hover.Sample(3.2, false);
for (int i = 0; i < 30; i++) hover.Sample(4 + i * .1, true);
Expand All @@ -183,6 +194,38 @@ private static void VerifyHoverLockWindow()
finally { hover?.Dispose(); owner.Close(); }
}

private static void VerifyHardwareClickThrough()
{
var window = new Window
{
Width = 180, Height = 120, Left = 40, Top = 40, WindowStyle = WindowStyle.None,
AllowsTransparency = false, Background = Brushes.Transparent, ShowActivated = false,
ShowInTaskbar = false
};
window.Show();
try
{
NativeWindow.UseHardwareTransparency(window);
NativeWindow.SetInteractive(window, false);
if (!NativeWindow.IsClickThrough(window) || !NativeWindow.IsLayered(window) || HitAtCenter(window) != -1)
throw new InvalidOperationException("Hardware canvas is not click-through: style=0x" + NativeWindow.ExStyle(new System.Windows.Interop.WindowInteropHelper(window).Handle).ToString("X"));
NativeWindow.SetInteractive(window, true);
if (NativeWindow.IsClickThrough(window) || !NativeWindow.IsLayered(window) || HitAtCenter(window) != 1)
throw new InvalidOperationException("Adjusting canvas lost layered input: style=0x" + NativeWindow.ExStyle(new System.Windows.Interop.WindowInteropHelper(window).Handle).ToString("X"));
var parameters = new HwndSourceParameters("Miucs click-through probe")
{
Width = 80, Height = 60, WindowStyle = unchecked((int)0x80000000),
ExtendedWindowStyle = 0x00200000 | 0x08000000 | 0x00000080 | 0x00000020 | 0x00000008
};
using var source = new HwndSource(parameters);
NativeWindow.PassThroughSurface(source);
long style = NativeWindow.ExStyle(source.Handle);
if ((style & 0x80000) == 0 || (style & 0x20) == 0 || (style & 0x200000) == 0 || NativeWindow.HitTestHandle(source.Handle, 1, 1) != -1)
throw new InvalidOperationException("Plugin surface click-through style lost: 0x" + style.ToString("X"));
}
finally { window.Close(); }
}

private static void Assert(bool result, string message)
{ if (!result) throw new InvalidOperationException("Hover lock: " + message); }

Expand Down
6 changes: 2 additions & 4 deletions modules/plugins/platforms/windows/PluginView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ private void EnsureWindow()
{
if(native!=null||!IsLoaded)return;var parent=Window.GetWindow(this);if(parent==null)return;
// Keep this popup unowned: owned popups are always above their owner and would cover WPF controls.
var settings=new HwndSourceParameters("Miucs plugin surface") {Width=1,Height=1,WindowStyle=unchecked((int)0x80000000),ExtendedWindowStyle=0x00200000|0x08000000|0x00000080|0x00000020|0x00000008};
native=new HwndSource(settings);native.AddHook(HitTest);surface=PluginNative.miucs_surface_create(package.Handle,native.Handle,Id);
var settings=new HwndSourceParameters("Miucs plugin surface") {Width=1,Height=1,WindowStyle=unchecked((int)0x80000000),ExtendedWindowStyle=0x00200000|0x08000000|0x00080000|0x00000080|0x00000020|0x00000008};
native=new HwndSource(settings);Miucs.Windows.NativeWindow.PassThroughSurface(native);surface=PluginNative.miucs_surface_create(package.Handle,native.Handle,Id);
var parentWindow=Window.GetWindow(this);if(parentWindow!=null)gpuFrameInterval=1d/Miucs.Windows.NativeWindow.RefreshHz(parentWindow);
if(surface!=IntPtr.Zero&&PluginNative.miucs_surface_backend(surface)==0)System.Diagnostics.Trace.WriteLine($"Plugin {Id}: CPU fallback, 30 FPS limit");
}
Expand All @@ -54,8 +54,6 @@ public override void Update(AudioInput audio,KeyboardInput keyboard,double delta
PluginNative.miucs_surface_draw(surface,(uint)w,(uint)h,dpi.DpiScaleX);
}
public override void Dispose(){if(Disposed)return;base.Dispose();Release();package.Replacing-=Release;host.ReleaseUnused(package);}
private static IntPtr HitTest(IntPtr window,int message,IntPtr wParam,IntPtr lParam,ref bool handled)
{if(message==0x0084){handled=true;return new IntPtr(-1);}return IntPtr.Zero;}
protected override void RenderScene(DrawingContext context){}
[DllImport("user32.dll")]private static extern bool SetWindowPos(IntPtr window,IntPtr after,int x,int y,int cx,int cy,uint flags);
[DllImport("user32.dll")]private static extern bool ShowWindow(IntPtr window,int command);
Expand Down
Loading