From 91b74a462ee8a1fa20a1b7bd9b6fbc9e3bf989d2 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 27 Mar 2023 20:13:39 +0100 Subject: [PATCH] Add SpinnerView --- Terminal.Gui/Views/SpinnerView.cs | 74 +++++++++++++++++++++++++++++ UICatalog/Scenarios/Progress.cs | 12 ++++- UnitTests/Views/SpinnerViewTests.cs | 71 +++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 Terminal.Gui/Views/SpinnerView.cs create mode 100644 UnitTests/Views/SpinnerViewTests.cs diff --git a/Terminal.Gui/Views/SpinnerView.cs b/Terminal.Gui/Views/SpinnerView.cs new file mode 100644 index 000000000..06984462c --- /dev/null +++ b/Terminal.Gui/Views/SpinnerView.cs @@ -0,0 +1,74 @@ +using System; + +namespace Terminal.Gui { + + /// + /// A 1x1 based on which displays a spinning + /// line character. + /// + /// + /// By default animation only occurs when you call . + /// Use to make the automate calls to . + /// + public class SpinnerView : Label { + private Rune [] runes = new Rune [] { '|', '/', '\u2500', '\\' }; + private int currentIdx = 0; + private DateTime lastRender = DateTime.MinValue; + private object _timeout; + + /// + /// Gets or sets the number of milliseconds to wait between characters + /// in the spin. Defaults to 250. + /// + /// This is the maximum speed the spinner will rotate at. You still need to + /// call or to + /// advance/start animation. + public int SpinDelayInMilliseconds { get; set; } = 250; + + /// + /// Creates a new instance of the class. + /// + public SpinnerView () + { + Width = 1; Height = 1; + } + + /// + 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); + } + + /// + /// Automates spinning + /// + public void AutoSpin() + { + if(_timeout != null) { + return; + } + + _timeout = Application.MainLoop.AddTimeout ( + TimeSpan.FromMilliseconds (SpinDelayInMilliseconds), (m) => { + Application.MainLoop.Invoke (this.SetNeedsDisplay); + return true; + }); + } + + /// + protected override void Dispose (bool disposing) + { + if (_timeout != null) { + Application.MainLoop.RemoveTimeout (_timeout); + } + + base.Dispose (disposing); + } + } +} \ No newline at end of file diff --git a/UICatalog/Scenarios/Progress.cs b/UICatalog/Scenarios/Progress.cs index 40cbb5aa9..94f278e71 100644 --- a/UICatalog/Scenarios/Progress.cs +++ b/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; @@ -77,13 +79,19 @@ namespace UICatalog.Scenarios { ActivityProgressBar = new ProgressBar () { X = Pos.Right (LeftFrame) + 1, Y = Pos.Bottom (startButton) + 1, - Width = Dim.Fill (), + Width = Dim.Fill (1), Height = 1, Fraction = 0.25F, ColorScheme = Colors.Error }; Add (ActivityProgressBar); + Spinner = new SpinnerView { + X = Pos.Right (ActivityProgressBar), + Y = ActivityProgressBar.Y + }; + Add (Spinner); + PulseProgressBar = new ProgressBar () { X = Pos.Right (LeftFrame) + 1, Y = Pos.Bottom (ActivityProgressBar) + 1, @@ -129,6 +137,7 @@ namespace UICatalog.Scenarios { ActivityProgressBar.Fraction += 0.01F; } PulseProgressBar.Pulse (); + Spinner.SetNeedsDisplay (); } } } @@ -196,6 +205,7 @@ namespace UICatalog.Scenarios { _mainLoopTimeout = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (_mainLooopTimeoutTick), (loop) => { mainLoopTimeoutDemo.Pulse (); + return true; }); }; diff --git a/UnitTests/Views/SpinnerViewTests.cs b/UnitTests/Views/SpinnerViewTests.cs new file mode 100644 index 000000000..3a6d6c482 --- /dev/null +++ b/UnitTests/Views/SpinnerViewTests.cs @@ -0,0 +1,71 @@ +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_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); + } + [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; + } + } +} \ No newline at end of file