mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
Merge branch 'v2_develop' into v2_draw-over-a-modal-view_2478
This commit is contained in:
75
Terminal.Gui/Views/SpinnerView.cs
Normal file
75
Terminal.Gui/Views/SpinnerView.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
UnitTests/Views/SpinnerViewTests.cs
Normal file
101
UnitTests/Views/SpinnerViewTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user