105 lines
3.4 KiB
C#
105 lines
3.4 KiB
C#
using CursorLang.Agent.Interop;
|
|
|
|
namespace CursorLang.Agent.Windows;
|
|
|
|
/// <summary>
|
|
/// A window with no framework behind it: a registered class, a handle and a window
|
|
/// procedure that lands in <see cref="OnMessage"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Windows knows one procedure per class, so the procedure here is shared and static,
|
|
/// and finds the instance by handle. The very first message of a window arrives while
|
|
/// <c>CreateWindowExW</c> is still running and there is nothing to find yet — that is
|
|
/// what the field holding the instance under construction is for.
|
|
///
|
|
/// Everything is deliberately without locks: the agent has one message loop, every
|
|
/// window belongs to it, and a window procedure can only ever be called on the thread
|
|
/// that created the window.
|
|
/// </remarks>
|
|
internal abstract class NativeWindow : IDisposable
|
|
{
|
|
private static readonly Dictionary<IntPtr, NativeWindow> Live = [];
|
|
private static readonly HashSet<string> RegisteredClasses = new(StringComparer.Ordinal);
|
|
|
|
// The shared procedure is a static field for the same reason a hook procedure is:
|
|
// Windows holds the only reference to it and the collector does not see that
|
|
private static readonly WindowNative.WindowProc SharedProc = StaticWindowProc;
|
|
|
|
[ThreadStatic]
|
|
private static NativeWindow? _creating;
|
|
|
|
protected NativeWindow(string className, string title, int style, int exStyle)
|
|
{
|
|
if (RegisteredClasses.Add(className))
|
|
{
|
|
WindowNative.RegisterClass(className, SharedProc);
|
|
}
|
|
|
|
_creating = this;
|
|
try
|
|
{
|
|
Handle = WindowNative.CreateWindow(className, title, style, exStyle);
|
|
}
|
|
finally
|
|
{
|
|
_creating = null;
|
|
}
|
|
|
|
Live[Handle] = this;
|
|
}
|
|
|
|
/// <summary>The window handle. Zero once the window is gone.</summary>
|
|
internal IntPtr Handle { get; private set; }
|
|
|
|
public virtual void Dispose()
|
|
{
|
|
if (Handle == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IntPtr handle = Handle;
|
|
Handle = IntPtr.Zero;
|
|
Live.Remove(handle);
|
|
|
|
WindowNative.DestroyWindow(handle);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A message for this window. Returning <c>false</c> passes it to
|
|
/// <c>DefWindowProcW</c>, which is what the vast majority of messages want — and
|
|
/// what all of them want for a window whose content is set from the outside.
|
|
/// </summary>
|
|
protected virtual bool OnMessage(uint message, IntPtr wParam, IntPtr lParam, out IntPtr result)
|
|
{
|
|
result = IntPtr.Zero;
|
|
return false;
|
|
}
|
|
|
|
private static IntPtr StaticWindowProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam)
|
|
{
|
|
if (!Live.TryGetValue(hWnd, out NativeWindow? window))
|
|
{
|
|
if (_creating is null)
|
|
{
|
|
return WindowNative.DefWindowProc(hWnd, message, wParam, lParam);
|
|
}
|
|
|
|
// The window is being created right now: bind the handle to the instance
|
|
// so that the rest of its creation messages find their way home
|
|
window = _creating;
|
|
window.Handle = hWnd;
|
|
Live[hWnd] = window;
|
|
}
|
|
|
|
if (message == WindowNative.WM_DESTROY)
|
|
{
|
|
Live.Remove(hWnd);
|
|
}
|
|
|
|
return window.OnMessage(message, wParam, lParam, out IntPtr result)
|
|
? result
|
|
: WindowNative.DefWindowProc(hWnd, message, wParam, lParam);
|
|
}
|
|
}
|