SpinnerView.cs 8.7 KB

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