SpinnerView.cs 6.4 KB

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