WindowsMainLoop.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #nullable enable
  2. #define HACK_CHECK_WINCHANGED
  3. using System.Collections.Concurrent;
  4. namespace Terminal.Gui;
  5. /// <summary>
  6. /// Mainloop intended to be used with the <see cref="WindowsDriver"/>, and can
  7. /// only be used on Windows.
  8. /// </summary>
  9. /// <remarks>
  10. /// This implementation is used for WindowsDriver.
  11. /// </remarks>
  12. internal class WindowsMainLoop : IMainLoopDriver
  13. {
  14. /// <summary>
  15. /// Invoked when the window is changed.
  16. /// </summary>
  17. public EventHandler<SizeChangedEventArgs>? WinChanged;
  18. private readonly IConsoleDriver _consoleDriver;
  19. private readonly ManualResetEventSlim _eventReady = new (false);
  20. // The records that we keep fetching
  21. private readonly ConcurrentQueue<WindowsConsole.InputRecord> _resultQueue = new ();
  22. private readonly ManualResetEventSlim _waitForProbe = new (false);
  23. private readonly WindowsConsole? _winConsole;
  24. private CancellationTokenSource _eventReadyTokenSource = new ();
  25. private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
  26. private MainLoop? _mainLoop;
  27. public WindowsMainLoop (IConsoleDriver consoleDriver)
  28. {
  29. _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
  30. if (!ConsoleDriver.RunningUnitTests)
  31. {
  32. _winConsole = ((WindowsDriver)consoleDriver).WinConsole;
  33. _winConsole!._mainLoop = this;
  34. }
  35. }
  36. void IMainLoopDriver.Setup (MainLoop mainLoop)
  37. {
  38. _mainLoop = mainLoop;
  39. if (ConsoleDriver.RunningUnitTests)
  40. {
  41. return;
  42. }
  43. Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token);
  44. #if HACK_CHECK_WINCHANGED
  45. Task.Run (CheckWinChange);
  46. #endif
  47. }
  48. void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
  49. bool IMainLoopDriver.EventsPending ()
  50. {
  51. if (ConsoleDriver.RunningUnitTests)
  52. {
  53. return true;
  54. }
  55. _waitForProbe.Set ();
  56. #if HACK_CHECK_WINCHANGED
  57. _winChange.Set ();
  58. #endif
  59. if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout))
  60. {
  61. return true;
  62. }
  63. try
  64. {
  65. if (!_eventReadyTokenSource.IsCancellationRequested)
  66. {
  67. // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there
  68. // are no timers, but there IS an idle handler waiting.
  69. _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
  70. }
  71. }
  72. catch (OperationCanceledException)
  73. {
  74. return true;
  75. }
  76. finally
  77. {
  78. if (!_eventReadyTokenSource.IsCancellationRequested)
  79. {
  80. _eventReady.Reset ();
  81. }
  82. }
  83. if (!_eventReadyTokenSource.IsCancellationRequested)
  84. {
  85. #if HACK_CHECK_WINCHANGED
  86. return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _) || _winChanged;
  87. #else
  88. return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _);
  89. #endif
  90. }
  91. _eventReadyTokenSource.Dispose ();
  92. _eventReadyTokenSource = new CancellationTokenSource ();
  93. // If cancellation was requested then always return true
  94. return true;
  95. }
  96. void IMainLoopDriver.Iteration ()
  97. {
  98. foreach (var i in ((WindowsDriver)_consoleDriver).ShouldReleaseParserHeldKeys ())
  99. {
  100. ((WindowsDriver)_consoleDriver).ProcessInputAfterParsing (i);
  101. }
  102. while (!ConsoleDriver.RunningUnitTests && _resultQueue.TryDequeue (out WindowsConsole.InputRecord inputRecords))
  103. {
  104. ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords);
  105. }
  106. #if HACK_CHECK_WINCHANGED
  107. if (_winChanged)
  108. {
  109. _winChanged = false;
  110. WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize));
  111. }
  112. #endif
  113. }
  114. void IMainLoopDriver.TearDown ()
  115. {
  116. _inputHandlerTokenSource.Cancel ();
  117. _inputHandlerTokenSource.Dispose ();
  118. if (_winConsole is { })
  119. {
  120. var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents ();
  121. if (numOfEvents > 0)
  122. {
  123. _winConsole.FlushConsoleInputBuffer ();
  124. //Debug.WriteLine ($"Flushed {numOfEvents} events.");
  125. }
  126. }
  127. _waitForProbe.Dispose ();
  128. _resultQueue.Clear ();
  129. _eventReadyTokenSource.Cancel ();
  130. _eventReadyTokenSource.Dispose ();
  131. _eventReady.Dispose ();
  132. #if HACK_CHECK_WINCHANGED
  133. _winChange?.Dispose ();
  134. #endif
  135. _mainLoop = null;
  136. }
  137. private void WindowsInputHandler ()
  138. {
  139. while (_mainLoop is { })
  140. {
  141. try
  142. {
  143. if (_inputHandlerTokenSource.IsCancellationRequested)
  144. {
  145. try
  146. {
  147. _waitForProbe.Wait (_inputHandlerTokenSource.Token);
  148. }
  149. catch (Exception ex)
  150. {
  151. if (ex is OperationCanceledException or ObjectDisposedException)
  152. {
  153. return;
  154. }
  155. throw;
  156. }
  157. _waitForProbe.Reset ();
  158. }
  159. ProcessInputQueue ();
  160. }
  161. catch (OperationCanceledException)
  162. {
  163. return;
  164. }
  165. }
  166. }
  167. private void ProcessInputQueue ()
  168. {
  169. if (_resultQueue?.Count == 0)
  170. {
  171. WindowsConsole.InputRecord? result = _winConsole!.DequeueInput ();
  172. if (result.HasValue)
  173. {
  174. _resultQueue!.Enqueue (result.Value);
  175. _eventReady.Set ();
  176. }
  177. }
  178. }
  179. #if HACK_CHECK_WINCHANGED
  180. private readonly ManualResetEventSlim _winChange = new (false);
  181. private bool _winChanged;
  182. private Size _windowSize;
  183. private void CheckWinChange ()
  184. {
  185. while (_mainLoop is { })
  186. {
  187. _winChange.Wait ();
  188. _winChange.Reset ();
  189. // Check if the window size changed every half second.
  190. // We do this to minimize the weird tearing seen on Windows when resizing the console
  191. while (_mainLoop is { })
  192. {
  193. Task.Delay (500).Wait ();
  194. _windowSize = _winConsole.GetConsoleBufferWindow (out _);
  195. if (_windowSize != Size.Empty
  196. && (_windowSize.Width != _consoleDriver.Cols
  197. || _windowSize.Height != _consoleDriver.Rows))
  198. {
  199. break;
  200. }
  201. }
  202. _winChanged = true;
  203. _eventReady.Set ();
  204. }
  205. }
  206. #endif
  207. }