Patrik Svensson 6f16081f42 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
2021-04-03 09:42:49 -04:00

149 lines
5.6 KiB
C#

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;
public int? Width { get; set; }
public bool ShowRemaining { get; set; } = true;
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)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(4, width);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var completedBarCount = Math.Min(MaxValue, Math.Max(0, Value));
var isCompleted = completedBarCount >= MaxValue;
if (IsIndeterminate && !isCompleted)
{
foreach (var segment in RenderIndeterminate(context, width))
{
yield return segment;
}
yield break;
}
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(bar, barCount), style);
if (ShowValue)
{
yield return barCount == 0
? new Segment(value, style)
: new Segment(" " + value, style);
}
// More space available?
if (barCount < width)
{
var diff = width - barCount;
if (ShowValue)
{
diff = diff - value.Length - 1;
if (diff <= 0)
{
yield break;
}
}
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);
}
}
}