lightweight variant (#1)
Reviewed-on: #1 Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// The Win32 message loop — what the application has instead of a dispatcher.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It lives in Core rather than in the agent because the things that need a pumping
|
||||
/// thread do: <see cref="MessageTimer"/> is used by the layout polling and by saving
|
||||
/// the settings, and both are Core's. The settings window has a loop of its own, run
|
||||
/// by WPF, and everything here works inside it just the same.
|
||||
/// </remarks>
|
||||
public static class MessageLoop
|
||||
{
|
||||
/// <summary>
|
||||
/// Pumps messages until <c>WM_QUIT</c> and returns its exit code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A -1 from GetMessage means the window handle has already gone; going round
|
||||
/// again would spin forever, so the loop gives up instead.
|
||||
/// </remarks>
|
||||
public static int Run()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
int result = GetMessage(out Message message, IntPtr.Zero, 0, 0);
|
||||
if (result is 0 or -1)
|
||||
{
|
||||
return result == 0 ? (int)message.wParam : 1;
|
||||
}
|
||||
|
||||
TranslateMessage(ref message);
|
||||
DispatchMessage(ref message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Asks the loop on this thread to finish.</summary>
|
||||
public static void Quit(int exitCode = 0) => PostQuitMessage(exitCode);
|
||||
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetMessageW")]
|
||||
private static extern int GetMessage(out Message lpMsg, IntPtr hWnd, uint filterMin, uint filterMax);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool TranslateMessage(ref Message lpMsg);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "DispatchMessageW")]
|
||||
private static extern IntPtr DispatchMessage(ref Message lpMsg);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern void PostQuitMessage(int exitCode);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct Message
|
||||
{
|
||||
public IntPtr hwnd;
|
||||
public uint message;
|
||||
public IntPtr wParam;
|
||||
public IntPtr lParam;
|
||||
public uint time;
|
||||
public int x;
|
||||
public int y;
|
||||
public uint lPrivate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// A timer that ticks on the message loop — the agent's stand-in for
|
||||
/// <c>DispatcherTimer</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>SetTimer</c> with a null window binds the timer to the thread rather than to a
|
||||
/// window, and <c>DispatchMessage</c> calls the callback straight from the loop. The
|
||||
/// upshot is the same as with a dispatcher timer: the tick arrives on the thread that
|
||||
/// owns the hook and the popup, so nothing needs marshalling and nothing races.
|
||||
///
|
||||
/// The callback lives in a field for the reason a hook procedure does: the only
|
||||
/// reference to it is held by Win32, and a collected delegate takes the process down
|
||||
/// with it at the first tick.
|
||||
/// </remarks>
|
||||
internal sealed class MessageTimer : IDisposable
|
||||
{
|
||||
/// <summary>Windows will not go below this, and pretending otherwise misleads.</summary>
|
||||
private const uint MinimumIntervalMilliseconds = 10;
|
||||
|
||||
private readonly TimerProc _callback;
|
||||
|
||||
private nuint _id;
|
||||
|
||||
internal MessageTimer() => _callback = OnTimer;
|
||||
|
||||
internal event EventHandler? Tick;
|
||||
|
||||
internal TimeSpan Interval { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
internal bool IsRunning => _id != 0;
|
||||
|
||||
/// <summary>Starts the timer, or restarts it from zero when it is already running.</summary>
|
||||
internal void Start()
|
||||
{
|
||||
Stop();
|
||||
|
||||
var milliseconds = (uint)Math.Clamp(
|
||||
Math.Round(Interval.TotalMilliseconds), MinimumIntervalMilliseconds, int.MaxValue);
|
||||
|
||||
_id = SetTimer(IntPtr.Zero, 0, milliseconds, _callback);
|
||||
}
|
||||
|
||||
internal void Stop()
|
||||
{
|
||||
if (_id == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KillTimer(IntPtr.Zero, _id);
|
||||
_id = 0;
|
||||
}
|
||||
|
||||
public void Dispose() => Stop();
|
||||
|
||||
private void OnTimer(IntPtr window, uint message, nuint id, uint time) =>
|
||||
Tick?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
private delegate void TimerProc(IntPtr hWnd, uint message, nuint idEvent, uint time);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern nuint SetTimer(IntPtr hWnd, nuint nIDEvent, uint uElapse, TimerProc lpTimerFunc);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool KillTimer(IntPtr hWnd, nuint uIDEvent);
|
||||
}
|
||||
Reference in New Issue
Block a user