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

This commit is contained in:
BDisp
2023-04-03 15:44:59 +01:00
3 changed files with 199 additions and 0 deletions

View File

@@ -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);
}
}
}

View File

@@ -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;
});
};

View File

@@ -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;
}
}
}