SpinnerView.cs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. //------------------------------------------------------------------------------
  2. // Windows Terminal supports Unicode and Emoji characters, but by default
  3. // conhost shells (e.g., PowerShell and cmd.exe) do not. See
  4. // <https://spectreconsole.net/best-practices>.
  5. //------------------------------------------------------------------------------
  6. using System.Diagnostics;
  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()"/> 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. if (SpinReverse)
  120. {
  121. _bounceReverse = false;
  122. }
  123. else
  124. {
  125. _bounceReverse = true;
  126. }
  127. _currentIdx = Sequence.Length - 1;
  128. }
  129. else
  130. {
  131. _currentIdx = 0;
  132. }
  133. }
  134. if (_currentIdx < 0)
  135. {
  136. if (SpinBounce)
  137. {
  138. if (SpinReverse)
  139. {
  140. _bounceReverse = true;
  141. }
  142. else
  143. {
  144. _bounceReverse = false;
  145. }
  146. _currentIdx = 1;
  147. }
  148. else
  149. {
  150. _currentIdx = Sequence.Length - 1;
  151. }
  152. }
  153. }
  154. _lastRender = DateTime.Now;
  155. }
  156. if (setNeedsDraw)
  157. {
  158. SetNeedsDraw ();
  159. }
  160. }
  161. /// <inheritdoc />
  162. protected override bool OnClearingViewport (Rectangle viewport) { return true; }
  163. /// <inheritdoc />
  164. protected override bool OnDrawingText (Rectangle viewport)
  165. {
  166. if (Sequence is { Length: > 0 } && _currentIdx < Sequence.Length)
  167. {
  168. Move (Viewport.X, Viewport.Y);
  169. View.Driver?.AddStr (Sequence [_currentIdx]);
  170. }
  171. return true;
  172. }
  173. /// <inheritdoc/>
  174. protected override void Dispose (bool disposing)
  175. {
  176. RemoveAutoSpinTimeout ();
  177. base.Dispose (disposing);
  178. }
  179. private void AddAutoSpinTimeout ()
  180. {
  181. if (_timeout is { })
  182. {
  183. return;
  184. }
  185. _timeout = Application.AddTimeout (
  186. TimeSpan.FromMilliseconds (SpinDelay),
  187. () =>
  188. {
  189. Application.Invoke (() => AdvanceAnimation());
  190. return true;
  191. }
  192. );
  193. }
  194. private bool GetIsAsciiOnly ()
  195. {
  196. if (HasSpecialCharacters)
  197. {
  198. return false;
  199. }
  200. if (_sequence is { } && _sequence.Length > 0)
  201. {
  202. foreach (string frame in _sequence)
  203. {
  204. foreach (char c in frame)
  205. {
  206. if (!char.IsAscii (c))
  207. {
  208. return false;
  209. }
  210. }
  211. }
  212. return true;
  213. }
  214. return true;
  215. }
  216. private int GetSpinnerWidth ()
  217. {
  218. var max = 0;
  219. if (_sequence is { } && _sequence.Length > 0)
  220. {
  221. foreach (string frame in _sequence)
  222. {
  223. if (frame.Length > max)
  224. {
  225. max = frame.Length;
  226. }
  227. }
  228. }
  229. return max;
  230. }
  231. private void RemoveAutoSpinTimeout ()
  232. {
  233. if (_timeout is { })
  234. {
  235. Application.RemoveTimeout (_timeout);
  236. _timeout = null;
  237. }
  238. }
  239. private void SetBounce (bool bounce) { _bounce = bounce; }
  240. private void SetDelay (int delay)
  241. {
  242. if (delay > -1)
  243. {
  244. _delay = delay;
  245. }
  246. }
  247. private void SetSequence (string [] frames)
  248. {
  249. if (frames is { } && frames.Length > 0)
  250. {
  251. _style = new SpinnerStyle.Custom ();
  252. _sequence = frames;
  253. Width = GetSpinnerWidth ();
  254. }
  255. }
  256. private void SetStyle (SpinnerStyle style)
  257. {
  258. if (style is { })
  259. {
  260. _style = style;
  261. _sequence = style.Sequence;
  262. _delay = style.SpinDelay;
  263. _bounce = style.SpinBounce;
  264. Width = GetSpinnerWidth ();
  265. }
  266. }
  267. bool IDesignable.EnableForDesign ()
  268. {
  269. Style = new SpinnerStyle.Points ();
  270. SpinReverse = true;
  271. AutoSpin = true;
  272. return true;
  273. }
  274. }