Преглед на файлове

Merge branch 'v2_develop' into v2_draw-over-a-modal-view_2478

BDisp преди 2 години
родител
ревизия
5cdf14eccc
променени са 3 файла, в които са добавени 199 реда и са изтрити 0 реда
  1. 75 0
      Terminal.Gui/Views/SpinnerView.cs
  2. 23 0
      UICatalog/Scenarios/Progress.cs
  3. 101 0
      UnitTests/Views/SpinnerViewTests.cs

+ 75 - 0
Terminal.Gui/Views/SpinnerView.cs

@@ -0,0 +1,75 @@
+using System;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// A 1x1 <see cref="View"/> based on <see cref="Label"/> which displays a spinning
+	/// line character.
+	/// </summary>
+	/// <remarks>
+	/// By default animation only occurs when you call <see cref="View.SetNeedsDisplay()"/>.
+	/// Use <see cref="AutoSpin"/> to make the automate calls to <see cref="View.SetNeedsDisplay()"/>.
+	/// </remarks>
+	public class SpinnerView : Label {
+		private Rune [] _runes = new Rune [] { '|', '/', '\u2500', '\\' };
+		private int _currentIdx = 0;
+		private DateTime _lastRender = DateTime.MinValue;
+		private object _timeout;
+
+		/// <summary>
+		/// Gets or sets the number of milliseconds to wait between characters
+		/// in the spin.  Defaults to 250.
+		/// </summary>
+		/// <remarks>This is the maximum speed the spinner will rotate at.  You still need to
+		/// call <see cref="View.SetNeedsDisplay()"/> or <see cref="SpinnerView.AutoSpin"/> to
+		/// advance/start animation.</remarks>
+		public int SpinDelayInMilliseconds { get; set; } = 250;
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="SpinnerView"/> class.
+		/// </summary>
+		public SpinnerView ()
+		{
+			Width = 1; Height = 1;
+		}
+
+		/// <inheritdoc/>
+		public override void Redraw (Rect bounds)
+		{
+			if (DateTime.Now - _lastRender > TimeSpan.FromMilliseconds (SpinDelayInMilliseconds)) {
+				_currentIdx = (_currentIdx + 1) % _runes.Length;
+				Text = "" + _runes [_currentIdx];
+				_lastRender = DateTime.Now;
+			}
+
+			base.Redraw (bounds);
+		}
+
+		/// <summary>
+		/// Automates spinning
+		/// </summary>
+		public void AutoSpin ()
+		{
+			if (_timeout != null) {
+				return;
+			}
+
+			_timeout = Application.MainLoop.AddTimeout (
+				TimeSpan.FromMilliseconds (SpinDelayInMilliseconds), (m) => {
+					Application.MainLoop.Invoke (this.SetNeedsDisplay);
+					return true;
+				});
+		}
+
+		/// <inheritdoc/>
+		protected override void Dispose (bool disposing)
+		{
+			if (_timeout != null) {
+				Application.MainLoop.RemoveTimeout (_timeout);
+				_timeout = null;
+			}
+
+			base.Dispose (disposing);
+		}
+	}
+}

+ 23 - 0
UICatalog/Scenarios/Progress.cs

@@ -3,6 +3,7 @@ using System;
 using System.Threading;
 using Terminal.Gui;
 using System.Linq;
+using System.Runtime.CompilerServices;
 
 namespace UICatalog.Scenarios {
 	// 
@@ -20,6 +21,7 @@ namespace UICatalog.Scenarios {
 			internal TextField Speed { get; private set; }
 			internal ProgressBar ActivityProgressBar { get; private set; }
 			internal ProgressBar PulseProgressBar { get; private set; }
+			internal SpinnerView Spinner { get; private set; }
 			internal Action StartBtnClick;
 			internal Action StopBtnClick;
 			internal Action PulseBtnClick = null;
@@ -84,6 +86,14 @@ namespace UICatalog.Scenarios {
 				};
 				Add (ActivityProgressBar);
 
+				Spinner = new SpinnerView {
+					X = Pos.Right (ActivityProgressBar),
+					Y = ActivityProgressBar.Y,
+					Visible = false,
+
+				};
+				Add (Spinner);
+
 				PulseProgressBar = new ProgressBar () {
 					X = Pos.Right (LeftFrame) + 1,
 					Y = Pos.Bottom (ActivityProgressBar) + 1,
@@ -109,12 +119,23 @@ namespace UICatalog.Scenarios {
 			{
 				Started = true;
 				StartBtnClick?.Invoke ();
+				Application.MainLoop.Invoke(()=>{
+					Spinner.Visible = true;
+					ActivityProgressBar.Width = Dim.Fill(1);
+					this.LayoutSubviews();
+				});
 			}
 
 			internal void Stop ()
 			{
 				Started = false;
 				StopBtnClick?.Invoke ();
+
+				Application.MainLoop.Invoke(()=>{
+					Spinner.Visible = false;
+					ActivityProgressBar.Width = Dim.Fill();
+					this.LayoutSubviews();
+				});
 			}
 
 			internal void Pulse ()
@@ -129,6 +150,7 @@ namespace UICatalog.Scenarios {
 						ActivityProgressBar.Fraction += 0.01F;
 					}
 					PulseProgressBar.Pulse ();
+					Spinner.SetNeedsDisplay ();
 				}
 			}
 		}
@@ -196,6 +218,7 @@ namespace UICatalog.Scenarios {
 
 				_mainLoopTimeout = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (_mainLooopTimeoutTick), (loop) => {
 					mainLoopTimeoutDemo.Pulse ();
+					
 					return true;
 				});
 			};

+ 101 - 0
UnitTests/Views/SpinnerViewTests.cs

@@ -0,0 +1,101 @@
+using System.Threading.Tasks;
+using Terminal.Gui;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace UnitTests.Views {
+	public class SpinnerViewTests {
+
+		readonly ITestOutputHelper output;
+
+		public SpinnerViewTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestSpinnerView_AutoSpin()
+		{
+			var view = GetSpinnerView ();
+
+			Assert.Empty (Application.MainLoop.timeouts);
+			view.AutoSpin ();
+			Assert.NotEmpty (Application.MainLoop.timeouts);
+
+			//More calls to AutoSpin do not add more timeouts
+			Assert.Equal (1,Application.MainLoop.timeouts.Count);
+			view.AutoSpin ();
+			view.AutoSpin ();
+			view.AutoSpin ();
+			Assert.Equal (1, Application.MainLoop.timeouts.Count);
+
+			// Dispose clears timeout
+			Assert.NotEmpty (Application.MainLoop.timeouts);
+			view.Dispose ();
+			Assert.Empty (Application.MainLoop.timeouts);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestSpinnerView_ThrottlesAnimation ()
+		{
+			var view = GetSpinnerView ();
+
+			view.Redraw (view.Bounds);
+
+			var expected = "/";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			view.SetNeedsDisplay ();
+			view.Redraw (view.Bounds);
+
+			expected = "/";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			view.SetNeedsDisplay ();
+			view.Redraw (view.Bounds);
+
+			expected = "/";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			Task.Delay (400).Wait();
+
+			view.SetNeedsDisplay ();
+			view.Redraw (view.Bounds);
+
+			expected = "─";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+		}
+		[Fact, AutoInitShutdown]
+		public void TestSpinnerView_NoThrottle ()
+		{
+			var view = GetSpinnerView ();
+			view.SpinDelayInMilliseconds = 0;
+
+			view.Redraw (view.Bounds);
+
+
+			var expected = @"─";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			view.SetNeedsDisplay ();
+			view.Redraw (view.Bounds);
+
+
+			expected = @"\";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+		}
+
+		private SpinnerView GetSpinnerView ()
+		{
+			var view = new SpinnerView ();
+
+			Application.Top.Add (view);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (1, view.Width);
+			Assert.Equal (1, view.Height);
+
+			return view;
+		}
+	}
+}