Add support for indeterminate progress

This commit also changes the behavior of ProgressContext.IsFinished.
Only tasks that have been started will be taken into consideration,
and not indeterminate tasks.

Closes #329
Closes #331
This commit is contained in:
Patrik Svensson 2021-04-02 18:41:25 +02:00 committed by Phil Scott
parent 6121203fee
commit 6f16081f42
15 changed files with 196 additions and 42 deletions

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Spectre.Console;
@ -25,8 +26,12 @@ namespace ProgressExample
.Start(ctx =>
{
var random = new Random(DateTime.Now.Millisecond);
var tasks = CreateTasks(ctx, random);
// Create some tasks
var tasks = CreateTasks(ctx, random);
var warpTask = ctx.AddTask("Going to warp", autoStart: false).IsIndeterminate();
// Wait for all tasks (except the indeterminate one) to complete
while (!ctx.IsFinished)
{
// Increment progress
@ -44,13 +49,24 @@ namespace ProgressExample
// Simulate some delay
Thread.Sleep(100);
}
// Now start the "warp" task
warpTask.StartTask();
warpTask.IsIndeterminate(false);
while (!ctx.IsFinished)
{
warpTask.Increment(12 * random.NextDouble());
// Simulate some delay
Thread.Sleep(100);
}
});
// Done
AnsiConsole.MarkupLine("[green]Done![/]");
}
private static List<(ProgressTask, int)> CreateTasks(ProgressContext progress, Random random)
private static List<(ProgressTask Task, int Delay)> CreateTasks(ProgressContext progress, Random random)
{
var tasks = new List<(ProgressTask, int)>();
while (tasks.Count < 5)

View File

@ -20,7 +20,7 @@ namespace Spectre.Console.Tests.Unit
public string Render()
{
var console = new FakeConsole();
var context = new RenderContext(console.Profile.Capabilities);
var context = new RenderContext(console.Profile.ColorSystem, console.Profile.Capabilities);
console.Write(Column.Render(context, Task, TimeSpan.Zero));
return console.Output;
}

View File

@ -15,7 +15,7 @@ namespace Spectre.Console.Tests.Unit
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
// When
var result = ((IRenderable)text).Measure(new RenderContext(caps), 80);
var result = ((IRenderable)text).Measure(new RenderContext(ColorSystem.TrueColor, caps), 80);
// Then
result.Min.ShouldBe(6);
@ -29,7 +29,7 @@ namespace Spectre.Console.Tests.Unit
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
// When
var result = ((IRenderable)text).Measure(new RenderContext(caps), 80);
var result = ((IRenderable)text).Measure(new RenderContext(ColorSystem.TrueColor, caps), 80);
// Then
result.Max.ShouldBe(11);

View File

@ -6,6 +6,17 @@ namespace Spectre.Console
{
internal static class EnumerableExtensions
{
public static IEnumerable<T> Repeat<T>(this IEnumerable<T> source, int count)
{
while (count-- > 0)
{
foreach (var item in source)
{
yield return item;
}
}
}
public static int IndexOf<T>(this IEnumerable<T> source, T item)
where T : class
{

View File

@ -57,5 +57,22 @@ namespace Spectre.Console
task.Value = value;
return task;
}
/// <summary>
/// Sets whether the task is considered indeterminate or not.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="indeterminate">Whether the task is considered indeterminate or not.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ProgressTask IsIndeterminate(this ProgressTask task, bool indeterminate = true)
{
if (task is null)
{
throw new ArgumentNullException(nameof(task));
}
task.IsIndeterminate = indeterminate;
return task;
}
}
}

View File

@ -27,7 +27,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(renderable));
}
var context = new RenderContext(console.Profile.Capabilities);
var context = new RenderContext(console.Profile.ColorSystem, console.Profile.Capabilities);
var renderables = console.Pipeline.Process(context, new[] { renderable });
return GetSegments(console, context, renderables);

View File

@ -9,7 +9,7 @@ namespace Spectre.Console.Internal
{
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
{
var context = new RenderContext(EncoderCapabilities.Default);
var context = new RenderContext(ColorSystem.TrueColor, EncoderCapabilities.Default);
var builder = new StringBuilder();
builder.Append("<pre style=\"font-size:90%;font-family:consolas,'Courier New',monospace\">\n");

View File

@ -20,7 +20,7 @@ namespace Spectre.Console.Internal
{
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
{
var context = new RenderContext(EncoderCapabilities.Default);
var context = new RenderContext(ColorSystem.TrueColor, EncoderCapabilities.Default);
var builder = new StringBuilder();
foreach (var renderable in renderables)

View File

@ -9,6 +9,11 @@ namespace Spectre.Console.Rendering
{
private readonly IReadOnlyCapabilities _capabilities;
/// <summary>
/// Gets the current color system.
/// </summary>
public ColorSystem ColorSystem { get; }
/// <summary>
/// Gets a value indicating whether or not unicode is supported.
/// </summary>
@ -28,17 +33,19 @@ namespace Spectre.Console.Rendering
/// <summary>
/// Initializes a new instance of the <see cref="RenderContext"/> class.
/// </summary>
/// <param name="colorSystem">The color system.</param>
/// <param name="capabilities">The capabilities.</param>
/// <param name="justification">The justification.</param>
public RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null)
: this(capabilities, justification, false)
public RenderContext(ColorSystem colorSystem, IReadOnlyCapabilities capabilities, Justify? justification = null)
: this(colorSystem, capabilities, justification, false)
{
}
private RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
private RenderContext(ColorSystem colorSystem, IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
{
_capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
ColorSystem = colorSystem;
Justification = justification;
SingleLine = singleLine;
}
@ -50,7 +57,7 @@ namespace Spectre.Console.Rendering
/// <returns>A new <see cref="RenderContext"/> instance.</returns>
public RenderContext WithJustification(Justify? justification)
{
return new RenderContext(_capabilities, justification, SingleLine);
return new RenderContext(ColorSystem, _capabilities, justification, SingleLine);
}
/// <summary>
@ -65,7 +72,7 @@ namespace Spectre.Console.Rendering
/// <returns>A new <see cref="RenderContext"/> instance.</returns>
internal RenderContext WithSingleLine()
{
return new RenderContext(_capabilities, Justification, true);
return new RenderContext(ColorSystem, _capabilities, Justification, true);
}
}
}

View File

@ -28,6 +28,11 @@ namespace Spectre.Console
/// </summary>
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
/// <summary>
/// Gets or sets the style of an indeterminate progress bar.
/// </summary>
public Style IndeterminateStyle { get; set; } = ProgressBar.DefaultPulseStyle;
/// <inheritdoc/>
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
{
@ -39,6 +44,8 @@ namespace Spectre.Console
CompletedStyle = CompletedStyle,
FinishedStyle = FinishedStyle,
RemainingStyle = RemainingStyle,
IndeterminateStyle = IndeterminateStyle,
IsIndeterminate = task.IsIndeterminate,
};
}
}

View File

@ -16,9 +16,9 @@ namespace Spectre.Console
private int _taskId;
/// <summary>
/// Gets a value indicating whether or not all tasks have completed.
/// Gets a value indicating whether or not all started tasks have completed.
/// </summary>
public bool IsFinished => _tasks.All(task => task.IsFinished);
public bool IsFinished => _tasks.Where(x => x.IsStarted).All(task => task.IsFinished);
internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
{
@ -28,20 +28,41 @@ namespace Spectre.Console
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
}
/// <summary>
/// Adds a task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="autoStart">Whether or not the task should start immediately.</param>
/// <param name="maxValue">The task's max value.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTask(string description, bool autoStart = true, double maxValue = 100)
{
return AddTask(description, new ProgressTaskSettings
{
AutoStart = autoStart,
MaxValue = maxValue,
});
}
/// <summary>
/// Adds a task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="settings">The task settings.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTask(string description, ProgressTaskSettings? settings = null)
public ProgressTask AddTask(string description, ProgressTaskSettings settings)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
lock (_taskLock)
{
settings ??= new ProgressTaskSettings();
var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);
_tasks.Add(task);
return task;
}
}

View File

@ -93,6 +93,12 @@ namespace Spectre.Console
/// </summary>
public TimeSpan? RemainingTime => GetRemainingTime();
/// <summary>
/// Gets or sets a value indicating whether the ProgressBar shows
/// actual values or generic, continuous progress feedback.
/// </summary>
public bool IsIndeterminate { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ProgressTask"/> class.
/// </summary>

View File

@ -16,5 +16,10 @@ namespace Spectre.Console
/// will be auto started. Defaults to <c>true</c>.
/// </summary>
public bool AutoStart { get; set; } = true;
/// <summary>
/// Gets the default progress task settings.
/// </summary>
internal static ProgressTaskSettings Default { get; } = new ProgressTaskSettings();
}
}

View File

@ -62,7 +62,7 @@ namespace Spectre.Console
_stopwatch.Start();
}
var renderContext = new RenderContext(_console.Profile.Capabilities);
var renderContext = new RenderContext(_console.Profile.ColorSystem, _console.Profile.Capabilities);
var delta = _stopwatch.Elapsed - _lastUpdate;
_lastUpdate = _stopwatch.Elapsed;

View File

@ -1,12 +1,16 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal sealed class ProgressBar : Renderable, IHasCulture
{
private const int PULSESIZE = 20;
private const int PULSESPEED = 15;
public double Value { get; set; }
public double MaxValue { get; set; } = 100;
@ -15,11 +19,15 @@ namespace Spectre.Console
public char UnicodeBar { get; set; } = '━';
public char AsciiBar { get; set; } = '-';
public bool ShowValue { get; set; }
public bool IsIndeterminate { get; set; }
public CultureInfo? Culture { get; set; }
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
public Style IndeterminateStyle { get; set; } = DefaultPulseStyle;
internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23);
protected override Measurement Measure(RenderContext context, int maxWidth)
{
@ -30,43 +38,49 @@ namespace Spectre.Console
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var completed = Math.Min(MaxValue, Math.Max(0, Value));
var completedBarCount = Math.Min(MaxValue, Math.Max(0, Value));
var isCompleted = completedBarCount >= MaxValue;
var token = !context.Unicode ? AsciiBar : UnicodeBar;
var style = completed >= MaxValue ? FinishedStyle : CompletedStyle;
var bars = Math.Max(0, (int)(width * (completed / MaxValue)));
var value = completed.ToString(Culture ?? CultureInfo.InvariantCulture);
if (ShowValue)
if (IsIndeterminate && !isCompleted)
{
bars = bars - value.Length - 1;
bars = Math.Max(0, bars);
foreach (var segment in RenderIndeterminate(context, width))
{
yield return segment;
}
yield break;
}
if (bars < 0)
var bar = !context.Unicode ? AsciiBar : UnicodeBar;
var style = isCompleted ? FinishedStyle : CompletedStyle;
var barCount = Math.Max(0, (int)(width * (completedBarCount / MaxValue)));
// Show value?
var value = completedBarCount.ToString(Culture ?? CultureInfo.InvariantCulture);
if (ShowValue)
{
barCount = barCount - value.Length - 1;
barCount = Math.Max(0, barCount);
}
if (barCount < 0)
{
yield break;
}
yield return new Segment(new string(token, bars), style);
yield return new Segment(new string(bar, barCount), style);
if (ShowValue)
{
// TODO: Fix this at some point
if (bars == 0)
{
yield return new Segment(value, style);
}
else
{
yield return new Segment(" " + value, style);
}
yield return barCount == 0
? new Segment(value, style)
: new Segment(" " + value, style);
}
if (bars < width)
// More space available?
if (barCount < width)
{
var diff = width - bars;
var diff = width - barCount;
if (ShowValue)
{
diff = diff - value.Length - 1;
@ -76,9 +90,59 @@ namespace Spectre.Console
}
}
var remainingToken = ShowRemaining ? token : ' ';
var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
var remainingToken = ShowRemaining && !legacy ? bar : ' ';
yield return new Segment(new string(remainingToken, diff), RemainingStyle);
}
}
private IEnumerable<Segment> RenderIndeterminate(RenderContext context, int width)
{
var bar = context.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString();
var style = IndeterminateStyle ?? DefaultPulseStyle;
IEnumerable<Segment> GetPulseSegments()
{
// For 1-bit and 3-bit colors, fall back to
// a simpler versions with only two colors.
if (context.ColorSystem == ColorSystem.NoColors ||
context.ColorSystem == ColorSystem.Legacy)
{
// First half of the pulse
var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2);
// Second half of the pulse
var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
var bar2 = legacy ? " " : bar;
segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2)));
foreach (var segment in segments)
{
yield return segment;
}
yield break;
}
for (var index = 0; index < PULSESIZE; index++)
{
var position = index / (float)PULSESIZE;
var fade = 0.5f + ((float)Math.Cos(position * Math.PI * 2) / 2.0f);
var color = style.Foreground.Blend(style.Background, fade);
yield return new Segment(bar, new Style(foreground: color));
}
}
// Get the pulse segments
var pulseSegments = GetPulseSegments();
pulseSegments = pulseSegments.Repeat((width / PULSESIZE) + 2);
// Repeat the pulse segments
var currentTime = (DateTime.Now - DateTime.Today).TotalSeconds;
var offset = (int)(currentTime * PULSESPEED) % PULSESIZE;
return pulseSegments.Skip(offset).Take(width);
}
}
}