SpinnerView.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  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;
  7. /// <summary>A <see cref="View"/> which displays (by default) a spinning line character.</summary>
  8. /// <remarks>
  9. /// By default animation only occurs when you call <see cref="SpinnerView.AdvanceAnimation()"/>. Use
  10. /// <see cref="AutoSpin"/> to make the automate calls to <see cref="SpinnerView.AdvanceAnimation()"/>.
  11. /// </remarks>
  12. public class SpinnerView : View
  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()"/> 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="Application.Invoke"/></remarks>
  102. public void AdvanceAnimation ()
  103. {
  104. if (DateTime.Now - _lastRender > TimeSpan.FromMilliseconds (SpinDelay))
  105. {
  106. if (Sequence is { } && Sequence.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. if (SpinReverse)
  119. {
  120. _bounceReverse = false;
  121. }
  122. else
  123. {
  124. _bounceReverse = true;
  125. }
  126. _currentIdx = Sequence.Length - 1;
  127. }
  128. else
  129. {
  130. _currentIdx = 0;
  131. }
  132. }
  133. if (_currentIdx < 0)
  134. {
  135. if (SpinBounce)
  136. {
  137. if (SpinReverse)
  138. {
  139. _bounceReverse = true;
  140. }
  141. else
  142. {
  143. _bounceReverse = false;
  144. }
  145. _currentIdx = 1;
  146. }
  147. else
  148. {
  149. _currentIdx = Sequence.Length - 1;
  150. }
  151. }
  152. Text = "" + Sequence [_currentIdx]; //.EnumerateRunes;
  153. }
  154. _lastRender = DateTime.Now;
  155. }
  156. SetNeedsDisplay ();
  157. }
  158. /// <inheritdoc/>
  159. protected override void Dispose (bool disposing)
  160. {
  161. RemoveAutoSpinTimeout ();
  162. base.Dispose (disposing);
  163. }
  164. private void AddAutoSpinTimeout ()
  165. {
  166. if (_timeout is { })
  167. {
  168. return;
  169. }
  170. _timeout = Application.AddTimeout (
  171. TimeSpan.FromMilliseconds (SpinDelay),
  172. () =>
  173. {
  174. Application.Invoke (AdvanceAnimation);
  175. return true;
  176. }
  177. );
  178. }
  179. private bool GetIsAsciiOnly ()
  180. {
  181. if (HasSpecialCharacters)
  182. {
  183. return false;
  184. }
  185. if (_sequence is { } && _sequence.Length > 0)
  186. {
  187. foreach (string frame in _sequence)
  188. {
  189. foreach (char c in frame)
  190. {
  191. if (!char.IsAscii (c))
  192. {
  193. return false;
  194. }
  195. }
  196. }
  197. return true;
  198. }
  199. return true;
  200. }
  201. private int GetSpinnerWidth ()
  202. {
  203. var max = 0;
  204. if (_sequence is { } && _sequence.Length > 0)
  205. {
  206. foreach (string frame in _sequence)
  207. {
  208. if (frame.Length > max)
  209. {
  210. max = frame.Length;
  211. }
  212. }
  213. }
  214. return max;
  215. }
  216. private void RemoveAutoSpinTimeout ()
  217. {
  218. if (_timeout is { })
  219. {
  220. Application.RemoveTimeout (_timeout);
  221. _timeout = null;
  222. }
  223. }
  224. private void SetBounce (bool bounce) { _bounce = bounce; }
  225. private void SetDelay (int delay)
  226. {
  227. if (delay > -1)
  228. {
  229. _delay = delay;
  230. }
  231. }
  232. private void SetSequence (string [] frames)
  233. {
  234. if (frames is { } && frames.Length > 0)
  235. {
  236. _style = new SpinnerStyle.Custom ();
  237. _sequence = frames;
  238. Width = GetSpinnerWidth ();
  239. }
  240. }
  241. private void SetStyle (SpinnerStyle style)
  242. {
  243. if (style is { })
  244. {
  245. _style = style;
  246. _sequence = style.Sequence;
  247. _delay = style.SpinDelay;
  248. _bounce = style.SpinBounce;
  249. Width = GetSpinnerWidth ();
  250. }
  251. }
  252. }