SpinnerView.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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="View.SetNeedsDisplay()"/>.
  13. /// Use <see cref="AutoSpin"/> to make the automate calls to <see cref="View.SetNeedsDisplay()"/>.
  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="View.SetNeedsDisplay()"/> 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. }
  73. private void SetStyle (SpinnerStyle style)
  74. {
  75. if (style is not null) {
  76. _style = style;
  77. _sequence = style.Sequence;
  78. _delay = style.SpinDelay;
  79. _bounce = style.SpinBounce;
  80. Width = GetSpinnerWidth ();
  81. }
  82. }
  83. private void SetSequence (string [] frames)
  84. {
  85. if (frames is not null && frames.Length > 0) {
  86. _style = new SpinnerStyle.Custom ();
  87. _sequence = frames;
  88. Width = GetSpinnerWidth ();
  89. }
  90. }
  91. private void SetDelay (int delay)
  92. {
  93. if (delay > -1) {
  94. _delay = delay;
  95. }
  96. }
  97. private void SetBounce (bool bounce)
  98. {
  99. _bounce = bounce;
  100. }
  101. private int GetSpinnerWidth ()
  102. {
  103. int max = 0;
  104. if (_sequence is not null && _sequence.Length > 0) {
  105. foreach (string frame in _sequence) {
  106. if (frame.Length > max) {
  107. max = frame.Length;
  108. }
  109. }
  110. }
  111. return max;
  112. }
  113. private bool GetIsAsciiOnly ()
  114. {
  115. if (HasSpecialCharacters) {
  116. return false;
  117. }
  118. if (_sequence is not null && _sequence.Length > 0) {
  119. foreach (string frame in _sequence) {
  120. foreach (char c in frame) {
  121. if (!char.IsAscii (c)) {
  122. return false;
  123. }
  124. }
  125. }
  126. return true;
  127. }
  128. return true;
  129. }
  130. /// <inheritdoc/>
  131. public override void OnDrawContent (Rect contentArea)
  132. {
  133. if (DateTime.Now - _lastRender > TimeSpan.FromMilliseconds (SpinDelay)) {
  134. //_currentIdx = (_currentIdx + 1) % Sequence.Length;
  135. if (Sequence is not null && Sequence.Length > 1) {
  136. int d = 1;
  137. if ((_bounceReverse && !SpinReverse) || (!_bounceReverse && SpinReverse)) {
  138. d = -1;
  139. }
  140. _currentIdx += d;
  141. if (_currentIdx >= Sequence.Length) {
  142. if (SpinBounce) {
  143. if (SpinReverse) {
  144. _bounceReverse = false;
  145. } else {
  146. _bounceReverse = true;
  147. }
  148. _currentIdx = Sequence.Length - 1;
  149. } else {
  150. _currentIdx = 0;
  151. }
  152. }
  153. if (_currentIdx < 0) {
  154. if (SpinBounce) {
  155. if (SpinReverse) {
  156. _bounceReverse = true;
  157. } else {
  158. _bounceReverse = false;
  159. }
  160. _currentIdx = 1;
  161. } else {
  162. _currentIdx = Sequence.Length - 1;
  163. }
  164. }
  165. Text = "" + Sequence [_currentIdx]; //.EnumerateRunes;
  166. }
  167. _lastRender = DateTime.Now;
  168. }
  169. base.OnDrawContent (contentArea);
  170. }
  171. /// <summary>
  172. /// Automates spinning
  173. /// </summary>
  174. public void AutoSpin ()
  175. {
  176. if (_timeout != null) {
  177. return;
  178. }
  179. _timeout = Application.MainLoop.AddTimeout (
  180. TimeSpan.FromMilliseconds (SpinDelay), (m) => {
  181. Application.MainLoop.Invoke (this.SetNeedsDisplay);
  182. return true;
  183. });
  184. }
  185. /// <inheritdoc/>
  186. protected override void Dispose (bool disposing)
  187. {
  188. if (_timeout != null) {
  189. Application.MainLoop.RemoveTimeout (_timeout);
  190. _timeout = null;
  191. }
  192. base.Dispose (disposing);
  193. }
  194. }
  195. }