SpinnerView.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. #nullable enable
  2. //------------------------------------------------------------------------------
  3. // Windows Terminal supports Unicode and Emoji characters, but by default
  4. // conhost shells (e.g., PowerShell and cmd.exe) do not. See
  5. // <https://spectreconsole.net/best-practices>.
  6. //------------------------------------------------------------------------------
  7. namespace Terminal.Gui;
  8. /// <summary>A <see cref="View"/> which displays (by default) a spinning line character.</summary>
  9. /// <remarks>
  10. /// By default, animation only occurs when you call <see cref="SpinnerView.AdvanceAnimation(bool)"/>. Use
  11. /// <see cref="AutoSpin"/> to make the automate calls to <see cref="SpinnerView.AdvanceAnimation(bool)"/>.
  12. /// </remarks>
  13. public class SpinnerView : View, IDesignable
  14. {
  15. private const int DEFAULT_DELAY = 130;
  16. private static readonly SpinnerStyle DEFAULT_STYLE = new SpinnerStyle.Line ();
  17. private bool _bounce = DEFAULT_STYLE.SpinBounce;
  18. private bool _bounceReverse;
  19. private int _currentIdx;
  20. private int _delay = DEFAULT_STYLE.SpinDelay;
  21. private DateTime _lastRender = DateTime.MinValue;
  22. private string [] _sequence = DEFAULT_STYLE.Sequence;
  23. private SpinnerStyle _style = DEFAULT_STYLE;
  24. private object? _timeout;
  25. /// <summary>Creates a new instance of the <see cref="SpinnerView"/> class.</summary>
  26. public SpinnerView ()
  27. {
  28. Width = Dim.Auto (minimumContentDim: 1);
  29. Height = Dim.Auto (minimumContentDim: 1);
  30. _delay = DEFAULT_DELAY;
  31. _bounce = false;
  32. SpinReverse = false;
  33. SetStyle (DEFAULT_STYLE);
  34. AdvanceAnimation ();
  35. }
  36. /// <summary>
  37. /// Gets or sets whether spinning should occur automatically or be manually triggered (e.g. from a background
  38. /// task).
  39. /// </summary>
  40. public bool AutoSpin
  41. {
  42. get => _timeout != null;
  43. set
  44. {
  45. if (value)
  46. {
  47. AddAutoSpinTimeout ();
  48. }
  49. else
  50. {
  51. RemoveAutoSpinTimeout ();
  52. }
  53. }
  54. }
  55. /// <summary>
  56. /// Gets whether the current spinner style contains emoji or other special characters. Does not check Custom
  57. /// sequences.
  58. /// </summary>
  59. public bool HasSpecialCharacters => _style.HasSpecialCharacters;
  60. /// <summary>Gets whether the current spinner style contains only ASCII characters. Also checks Custom sequences.</summary>
  61. public bool IsAsciiOnly => GetIsAsciiOnly ();
  62. /// <summary>Gets or sets the animation frames used to animate the spinner.</summary>
  63. public string [] Sequence
  64. {
  65. get => _sequence;
  66. set => SetSequence (value);
  67. }
  68. /// <summary>
  69. /// Gets or sets whether spinner should go back and forth through the frames rather than going to the end and
  70. /// starting again at the beginning.
  71. /// </summary>
  72. public bool SpinBounce
  73. {
  74. get => _bounce;
  75. set => SetBounce (value);
  76. }
  77. /// <summary>Gets or sets the number of milliseconds to wait between characters in the animation.</summary>
  78. /// <remarks>
  79. /// This is the maximum speed the spinner will rotate at. You still need to call
  80. /// <see cref="SpinnerView.AdvanceAnimation(bool)"/> or <see cref="SpinnerView.AutoSpin"/> to advance/start animation.
  81. /// </remarks>
  82. public int SpinDelay
  83. {
  84. get => _delay;
  85. set => SetDelay (value);
  86. }
  87. /// <summary>
  88. /// Gets or sets whether spinner should go through the frames in reverse order. If SpinBounce is true, this sets
  89. /// the starting order.
  90. /// </summary>
  91. public bool SpinReverse { get; set; }
  92. /// <summary>Gets or sets the Style used to animate the spinner.</summary>
  93. public SpinnerStyle Style
  94. {
  95. get => _style;
  96. set => SetStyle (value);
  97. }
  98. /// <summary>
  99. /// Advances the animation frame and notifies main loop that repainting needs to happen. Repeated calls are
  100. /// ignored based on <see cref="SpinDelay"/>.
  101. /// </summary>
  102. /// <remarks>Ensure this method is called on the main UI thread e.g. via <see cref="Application.Invoke"/></remarks>
  103. public void AdvanceAnimation (bool setNeedsDraw = true)
  104. {
  105. if (DateTime.Now - _lastRender > TimeSpan.FromMilliseconds (SpinDelay))
  106. {
  107. if (Sequence is { Length: > 1 })
  108. {
  109. var d = 1;
  110. if ((_bounceReverse && !SpinReverse) || (!_bounceReverse && SpinReverse))
  111. {
  112. d = -1;
  113. }
  114. _currentIdx += d;
  115. if (_currentIdx >= Sequence.Length)
  116. {
  117. if (SpinBounce)
  118. {
  119. _bounceReverse = !SpinReverse;
  120. _currentIdx = Sequence.Length - 1;
  121. }
  122. else
  123. {
  124. _currentIdx = 0;
  125. }
  126. }
  127. if (_currentIdx < 0)
  128. {
  129. if (SpinBounce)
  130. {
  131. _bounceReverse = SpinReverse;
  132. _currentIdx = 1;
  133. }
  134. else
  135. {
  136. _currentIdx = Sequence.Length - 1;
  137. }
  138. }
  139. }
  140. _lastRender = DateTime.Now;
  141. }
  142. if (setNeedsDraw)
  143. {
  144. SetNeedsDraw ();
  145. }
  146. }
  147. /// <inheritdoc/>
  148. protected override bool OnClearingViewport () { return true; }
  149. /// <inheritdoc/>
  150. protected override bool OnDrawingContent ()
  151. {
  152. Render ();
  153. return true;
  154. }
  155. /// <summary>
  156. /// Renders the current frame of the spinner.
  157. /// </summary>
  158. public void Render ()
  159. {
  160. if (Sequence is { Length: > 0 } && _currentIdx < Sequence.Length)
  161. {
  162. Move (Viewport.X, Viewport.Y);
  163. Driver?.AddStr (Sequence [_currentIdx]);
  164. }
  165. }
  166. /// <inheritdoc/>
  167. protected override void Dispose (bool disposing)
  168. {
  169. RemoveAutoSpinTimeout ();
  170. base.Dispose (disposing);
  171. }
  172. private void AddAutoSpinTimeout ()
  173. {
  174. // Only add timeout if we are initialized and not already spinning
  175. if (_timeout is { } || !Application.Initialized)
  176. {
  177. return;
  178. }
  179. _timeout = Application.AddTimeout (
  180. TimeSpan.FromMilliseconds (SpinDelay),
  181. () =>
  182. {
  183. Application.Invoke (() => AdvanceAnimation ());
  184. return true;
  185. }
  186. );
  187. }
  188. private bool GetIsAsciiOnly ()
  189. {
  190. if (HasSpecialCharacters)
  191. {
  192. return false;
  193. }
  194. if (_sequence is { Length: > 0 })
  195. {
  196. foreach (string frame in _sequence)
  197. {
  198. foreach (char c in frame)
  199. {
  200. if (!char.IsAscii (c))
  201. {
  202. return false;
  203. }
  204. }
  205. }
  206. return true;
  207. }
  208. return true;
  209. }
  210. private int GetSpinnerWidth ()
  211. {
  212. var max = 0;
  213. if (_sequence is { Length: > 0 })
  214. {
  215. foreach (string frame in _sequence)
  216. {
  217. if (frame.Length > max)
  218. {
  219. max = frame.Length;
  220. }
  221. }
  222. }
  223. return max;
  224. }
  225. private void RemoveAutoSpinTimeout ()
  226. {
  227. if (_timeout is { })
  228. {
  229. Application.RemoveTimeout (_timeout);
  230. _timeout = null;
  231. }
  232. }
  233. private void SetBounce (bool bounce) { _bounce = bounce; }
  234. private void SetDelay (int delay)
  235. {
  236. if (delay > -1)
  237. {
  238. _delay = delay;
  239. }
  240. }
  241. private void SetSequence (string [] frames)
  242. {
  243. if (frames is { Length: > 0 })
  244. {
  245. _style = new SpinnerStyle.Custom ();
  246. _sequence = frames;
  247. Width = GetSpinnerWidth ();
  248. }
  249. }
  250. private void SetStyle (SpinnerStyle? style)
  251. {
  252. if (style is { })
  253. {
  254. _style = style;
  255. _sequence = style.Sequence;
  256. _delay = style.SpinDelay;
  257. _bounce = style.SpinBounce;
  258. Width = GetSpinnerWidth ();
  259. }
  260. }
  261. bool IDesignable.EnableForDesign ()
  262. {
  263. Style = new SpinnerStyle.Points ();
  264. SpinReverse = true;
  265. AutoSpin = true;
  266. return true;
  267. }
  268. }