SpinnerView.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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(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. 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 () { return true; }
  163. /// <inheritdoc />
  164. protected override bool OnDrawingContent ()
  165. {
  166. Render ();
  167. return true;
  168. }
  169. /// <summary>
  170. /// Renders the current frame of the spinner.
  171. /// </summary>
  172. public void Render ()
  173. {
  174. if (Sequence is { Length: > 0 } && _currentIdx < Sequence.Length)
  175. {
  176. Move (Viewport.X, Viewport.Y);
  177. View.Driver?.AddStr (Sequence [_currentIdx]);
  178. }
  179. }
  180. /// <inheritdoc/>
  181. protected override void Dispose (bool disposing)
  182. {
  183. RemoveAutoSpinTimeout ();
  184. base.Dispose (disposing);
  185. }
  186. private void AddAutoSpinTimeout ()
  187. {
  188. if (_timeout is { })
  189. {
  190. return;
  191. }
  192. _timeout = Application.AddTimeout (
  193. TimeSpan.FromMilliseconds (SpinDelay),
  194. () =>
  195. {
  196. Application.Invoke (() => AdvanceAnimation());
  197. return true;
  198. }
  199. );
  200. }
  201. private bool GetIsAsciiOnly ()
  202. {
  203. if (HasSpecialCharacters)
  204. {
  205. return false;
  206. }
  207. if (_sequence is { } && _sequence.Length > 0)
  208. {
  209. foreach (string frame in _sequence)
  210. {
  211. foreach (char c in frame)
  212. {
  213. if (!char.IsAscii (c))
  214. {
  215. return false;
  216. }
  217. }
  218. }
  219. return true;
  220. }
  221. return true;
  222. }
  223. private int GetSpinnerWidth ()
  224. {
  225. var max = 0;
  226. if (_sequence is { } && _sequence.Length > 0)
  227. {
  228. foreach (string frame in _sequence)
  229. {
  230. if (frame.Length > max)
  231. {
  232. max = frame.Length;
  233. }
  234. }
  235. }
  236. return max;
  237. }
  238. private void RemoveAutoSpinTimeout ()
  239. {
  240. if (_timeout is { })
  241. {
  242. Application.RemoveTimeout (_timeout);
  243. _timeout = null;
  244. }
  245. }
  246. private void SetBounce (bool bounce) { _bounce = bounce; }
  247. private void SetDelay (int delay)
  248. {
  249. if (delay > -1)
  250. {
  251. _delay = delay;
  252. }
  253. }
  254. private void SetSequence (string [] frames)
  255. {
  256. if (frames is { } && frames.Length > 0)
  257. {
  258. _style = new SpinnerStyle.Custom ();
  259. _sequence = frames;
  260. Width = GetSpinnerWidth ();
  261. }
  262. }
  263. private void SetStyle (SpinnerStyle style)
  264. {
  265. if (style is { })
  266. {
  267. _style = style;
  268. _sequence = style.Sequence;
  269. _delay = style.SpinDelay;
  270. _bounce = style.SpinBounce;
  271. Width = GetSpinnerWidth ();
  272. }
  273. }
  274. bool IDesignable.EnableForDesign ()
  275. {
  276. Style = new SpinnerStyle.Points ();
  277. SpinReverse = true;
  278. AutoSpin = true;
  279. return true;
  280. }
  281. }