using System.Runtime.InteropServices; namespace CursorLang.Core.Threading; /// /// A timer that ticks on the message loop — the agent's stand-in for /// DispatcherTimer. /// /// /// SetTimer with a null window binds the timer to the thread rather than to a /// window, and DispatchMessage 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. /// internal sealed class MessageTimer : IDisposable { /// Windows will not go below this, and pretending otherwise misleads. 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; /// Starts the timer, or restarts it from zero when it is already running. 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); }