TerminalUI theming, Commands panel

This commit is contained in:
2023-08-15 20:23:58 +02:00
parent b792639635
commit d175c7bf7e
27 changed files with 604 additions and 77 deletions

View File

@@ -0,0 +1,63 @@
using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Controls;
using TerminalUI.Models;
namespace TerminalUI.Examples.Controls;
public class ProgressBarExamples
{
private readonly IApplicationContext _applicationContext;
private static readonly IConsoleDriver _driver;
static ProgressBarExamples()
{
_driver = new DotnetDriver();
_driver.Init();
}
public ProgressBarExamples(IApplicationContext applicationContext)
{
_applicationContext = applicationContext;
}
public void LoadingExample()
{
var progressBar = CreateProgressBar<object>(0);
for (var i = 0; i < 100; i++)
{
progressBar.Value = i;
RenderProgressBar(progressBar, new Position(0, 0));
Thread.Sleep(100);
}
}
public void PaletteExample()
{
RenderProgressBar(CreateProgressBar<object>(100), new Position(0, 0));
for (var i = 0; i < 10; i++)
{
RenderProgressBar(CreateProgressBar<object>(10 * (i + 1)), new Position(0, i + 1));
}
}
private ProgressBar<T> CreateProgressBar<T>(int percent) =>
new()
{
Value = percent,
Attached = true,
ApplicationContext = _applicationContext
};
private void RenderProgressBar<T>(ProgressBar<T> progressBar, Position position)
{
var renderContext = new RenderContext(
_driver,
true,
null,
null,
new()
);
progressBar.Render(renderContext, position, new Size(10, 1));
}
}

View File

@@ -0,0 +1,22 @@
using TerminalUI.Controls;
namespace TerminalUI.Examples.Mocks;
public class MockRenderEngine : IRenderEngine
{
public void RequestRerender(IView view)
{
}
public void VisibilityChanged(IView view)
{
}
public void AddViewToPermanentRenderGroup(IView view)
{
}
public void Run()
{
}
}

View File

@@ -0,0 +1,32 @@
// See https://aka.ms/new-console-template for more information
using Microsoft.Extensions.DependencyInjection;
using TerminalUI;
using TerminalUI.Color;
using TerminalUI.DependencyInjection;
using TerminalUI.Examples.Controls;
using TerminalUI.Examples.Mocks;
using TerminalUI.Styling;
using TerminalUI.Styling.Controls;
Console.OutputEncoding = System.Text.Encoding.UTF8;
var services = new ServiceCollection()
.AddTerminalUi()
.AddSingleton<IRenderEngine, MockRenderEngine>();
IServiceProvider provider = services.BuildServiceProvider();
var applicationContext = provider.GetRequiredService<IApplicationContext>();
applicationContext.Theme = new Theme
{
ControlThemes = new ControlThemes
{
ProgressBar = new ProgressBarTheme
{
ForegroundColor = ConsoleColors.Foregrounds.Blue
}
}
};
Console.CursorVisible = false;
new ProgressBarExamples(applicationContext).LoadingExample();
Console.CursorVisible = true;

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TerminalUI.DependencyInjection\TerminalUI.DependencyInjection.csproj" />
<ProjectReference Include="..\TerminalUI\TerminalUI.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,7 @@
using FileTime.App.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Styling;
namespace TerminalUI;
@@ -16,8 +16,10 @@ public class ApplicationContext : IApplicationContext
public IFocusManager FocusManager => _focusManager.Value;
public ILoggerFactory? LoggerFactory => _loggerFactory.Value;
public IRenderEngine RenderEngine => _renderEngine.Value;
public ITheme? Theme { get; set; }
public bool IsRunning { get; set; }
public char EmptyCharacter { get; init; } = ' ';
public bool SupportUtf8Output { get; set; } = true;
public ApplicationContext(IServiceProvider serviceProvider)
{

View File

@@ -21,7 +21,7 @@ public class DotnetDriver : IConsoleDriver
var (x, y) = Console.GetCursorPosition();
return new(x, y);
}
public void Write(string text) => Console.Out.Write(text);
public void Write(ReadOnlySpan<char> text) => Console.Out.Write(text);
@@ -33,6 +33,7 @@ public class DotnetDriver : IConsoleDriver
public ConsoleKeyInfo ReadKey() => Console.ReadKey(true);
public void SetCursorVisible(bool cursorVisible) => Console.CursorVisible = cursorVisible;
public virtual void SetForegroundColor(IColor foreground)
{
if (foreground is not ConsoleColor consoleColor) throw new NotSupportedException();

View File

@@ -7,6 +7,7 @@ namespace TerminalUI.ConsoleDrivers;
public sealed class XTermDriver : DotnetDriver
{
private Position _initialCursorPosition;
public override bool Init()
{
_initialCursorPosition = GetCursorPosition();

View File

@@ -153,6 +153,8 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
{
if (size.Width == 0 || size.Height == 0) return false;
return WithCalculatedSize(
renderContext,
new Option<Size>(size, true),
@@ -177,29 +179,34 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
);
var viewsByPosition = GroupViewsByPosition(columnWidths.Length, rowHeights.Length);
var anyRendered = false;
for (var column = 0; column < columnWidths.Length; column++)
{
for (var row = 0; row < rowHeights.Length; row++)
{
RenderViewsByPosition(
childContext,
position,
columnWidths,
rowHeights,
viewsByPosition,
column,
row,
forceRerenderChildren
);
anyRendered =
RenderViewsByPosition(
childContext,
position,
size,
columnWidths,
rowHeights,
viewsByPosition,
column,
row,
forceRerenderChildren
)
|| anyRendered;
}
}
return true;
return anyRendered;
}
}
private void RenderViewsByPosition(RenderContext context,
private bool RenderViewsByPosition(RenderContext context,
Position gridPosition,
Size gridSize,
ReadOnlySpan<int> columnWidths,
ReadOnlySpan<int> rowHeights,
IReadOnlyDictionary<(int, int), List<IView>> viewsByPosition,
@@ -207,7 +214,7 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
int row,
IReadOnlyList<IView> forceRerenderChildren)
{
if (!viewsByPosition.TryGetValue((column, row), out var children)) return;
if (!viewsByPosition.TryGetValue((column, row), out var children)) return false;
var width = columnWidths[column];
var height = rowHeights[row];
@@ -221,6 +228,18 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
row
);
if (renderPosition.X + width > gridPosition.X + gridSize.Width)
{
renderSize = renderSize with {Width = gridPosition.X + gridSize.Width - renderPosition.X};
}
if (renderPosition.Y + height > gridPosition.Y + gridSize.Height)
{
renderSize = renderSize with {Height = gridPosition.Y + gridSize.Height - renderPosition.Y};
}
if (renderSize.Width == 0 || renderSize.Height == 0) return false;
var needsRerender = children.Any(forceRerenderChildren.Contains);
if (needsRerender)
{
@@ -251,6 +270,8 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
}
}
return true;
static Position GetRenderPosition(
Position gridPosition,
ReadOnlySpan<int> columnWidths,

View File

@@ -0,0 +1,168 @@
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.Models;
using TerminalUI.Styling.Controls;
namespace TerminalUI.Controls;
public partial class ProgressBar<T> : View<ProgressBar<T>, T>
{
private record RenderState(
Position Position,
Size Size,
int Minimum,
int Maximum,
int Value,
char? LeftCap,
char? RightCap,
char? Fill,
char? Unfilled,
IColor? UnfilledForeground,
IColor? UnfilledBackground);
private RenderState? _lastRenderState;
[Notify] private int _minimum = 0;
[Notify] private int _maximum = 100;
[Notify] private int _value = 0;
[Notify] private IProgressBarTheme? _theme;
private IProgressBarTheme? AppTheme => ApplicationContext?.Theme?.ControlThemes.ProgressBar;
public ProgressBar()
{
RerenderProperties.Add(nameof(Minimum));
RerenderProperties.Add(nameof(Maximum));
RerenderProperties.Add(nameof(Value));
RerenderProperties.Add(nameof(Theme));
}
protected override Size CalculateSize() => new(5, 1);
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
{
if (size.Width == 0 || size.Height == 0) return false;
var theme = AppTheme;
var foreground = Foreground ?? (_theme ?? theme)?.ForegroundColor ?? renderContext.Foreground;
var background = Background ?? (_theme ?? theme)?.BackgroundColor ?? renderContext.Background;
var unfilledForeground = (_theme ?? theme)?.UnfilledForeground ?? renderContext.Foreground;
var unfilledBackground = (_theme ?? theme)?.UnfilledBackground ?? renderContext.Background;
var unfilledCharacter = (_theme ?? theme)?.UnfilledCharacter ?? ApplicationContext?.EmptyCharacter ?? ' ';
var fillCharacter = (_theme ?? theme)?.FilledCharacter ?? '█';
var leftCap = (_theme ?? theme)?.LeftCap;
var rightCap = (_theme ?? theme)?.RightCap;
var renderState = new RenderState(
position,
size,
Minimum,
Maximum,
Value,
leftCap,
rightCap,
fillCharacter,
unfilledCharacter,
unfilledForeground,
unfilledBackground);
if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false;
_lastRenderState = renderState;
var driver = renderContext.ConsoleDriver;
var borderWidth =
(leftCap.HasValue ? 1 : 0)
+ (rightCap.HasValue ? 1 : 0);
var progress = (double) (Value - Minimum) / (Maximum - Minimum);
var progressAvailableSpace = size.Width - borderWidth;
var progressWidth = progress * progressAvailableSpace;
var progressQuotientWidth = (int) Math.Floor(progressWidth);
var progressRemainderWidth = progressAvailableSpace - progressQuotientWidth - 1;
if (progressRemainderWidth < 0) progressRemainderWidth = 0;
Span<char> filledText = stackalloc char[progressQuotientWidth];
var transientChar = unfilledCharacter;
filledText.Fill(fillCharacter);
if (ApplicationContext!.SupportUtf8Output)
{
var remained = progressWidth - progressQuotientWidth;
transientChar = remained switch
{
< 0.125 => unfilledCharacter,
< 0.250 => (_theme ?? theme)?.Fraction1Per8Character ?? '\u258F',
< 0.375 => (_theme ?? theme)?.Fraction2Per8Character ?? '\u258E',
< 0.500 => (_theme ?? theme)?.Fraction3Per8Character ?? '\u258D',
< 0.675 => (_theme ?? theme)?.Fraction4Per8Character ?? '\u258C',
< 0.750 => (_theme ?? theme)?.Fraction5Per8Character ?? '\u258B',
< 0.875 => (_theme ?? theme)?.Fraction6Per8Character ?? '\u258A',
< 0_001 => (_theme ?? theme)?.Fraction7Per8Character ?? '\u2589',
_ => (_theme ?? theme)?.FractionFull ?? '\u2588',
};
}
SetColor(driver, foreground, background);
// Left border
var textStartPosition = position;
if (leftCap.HasValue)
{
RenderText(leftCap.Value, driver, position, size with {Width = 1});
textStartPosition = textStartPosition with {X = textStartPosition.X + 1};
}
// Filled
RenderText(
filledText,
driver,
textStartPosition,
size with {Width = progressQuotientWidth}
);
// Transient character
if (progressQuotientWidth < progressAvailableSpace)
{
SetColor(driver, foreground, unfilledBackground);
RenderText(
transientChar,
driver,
textStartPosition with {X = textStartPosition.X + progressQuotientWidth},
size with {Width = 1}
);
}
// Unfilled
if (progressRemainderWidth != 0)
{
Span<char> unfilledText = stackalloc char[progressRemainderWidth];
unfilledText.Fill(unfilledCharacter);
SetColor(driver, unfilledForeground, unfilledBackground);
RenderText(
unfilledText,
driver,
textStartPosition with {X = textStartPosition.X + progressQuotientWidth + 1},
size with {Width = progressRemainderWidth}
);
}
// Right border
if (rightCap.HasValue)
{
SetColor(driver, foreground, background);
RenderText(
rightCap.Value,
driver,
position with {X = position.X + size.Width - 1},
size with {Width = 1});
}
return true;
}
private bool NeedsRerender(RenderState renderState)
=> _lastRenderState is null || _lastRenderState != renderState;
}

View File

@@ -78,16 +78,7 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
_placeholderRenderDone = false;
var driver = renderContext.ConsoleDriver;
driver.ResetColor();
if (foreground is not null)
{
driver.SetForegroundColor(foreground);
}
if (background is not null)
{
driver.SetBackgroundColor(background);
}
SetColor(driver, foreground, background);
RenderText(_textLines, driver, position, size, TransformText);

View File

@@ -104,16 +104,7 @@ public sealed partial class TextBox<T> : View<TextBox<T>, T>, IFocusable, IDispl
_lastRenderState = renderStatus;
var driver = renderContext.ConsoleDriver;
driver.ResetColor();
if (foreground is not null)
{
driver.SetForegroundColor(foreground);
}
if (background is not null)
{
driver.SetBackgroundColor(background);
}
SetColor(driver, foreground, background);
RenderEmpty(renderContext, position, size);

View File

@@ -67,7 +67,21 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
public virtual Size GetRequestedSize()
{
var size = CalculateSize();
Size size;
if (Width.HasValue && Height.HasValue)
{
size = new Size(Width.Value, Height.Value);
}
else
{
size = CalculateSize();
if (Width.HasValue)
size = size with {Width = Width.Value};
if (Height.HasValue)
size = size with {Height = Height.Value};
}
if (MinWidth.HasValue && size.Width < MinWidth.Value)
size = size with {Width = MinWidth.Value};
@@ -220,8 +234,14 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
text = text[..size.Width];
}
driver.SetCursorPosition(currentPosition);
driver.Write(text);
try
{
driver.SetCursorPosition(currentPosition);
driver.Write(text);
}
catch
{
}
}
}
@@ -289,6 +309,18 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
driver.Write(contentString);
}
}
protected void SetColor(IConsoleDriver driver, IColor? foreground, IColor? background)
{
driver.ResetColor();
if (foreground is not null)
{
driver.SetForegroundColor(foreground);
}
if(background is not null)
{
driver.SetBackgroundColor(background);
}
}
protected void SetColorsForDriver(in RenderContext renderContext)
{
@@ -296,15 +328,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
var foreground = Foreground ?? renderContext.Foreground;
var background = Background ?? renderContext.Background;
if (foreground is not null)
{
driver.SetForegroundColor(foreground);
}
if (background is not null)
{
driver.SetBackgroundColor(background);
}
SetColor(driver, foreground, background);
}
public TChild CreateChild<TChild>() where TChild : IView<T>, new()

View File

@@ -1,6 +1,6 @@
using FileTime.App.Core.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Styling;
namespace TerminalUI;
@@ -12,4 +12,6 @@ public interface IApplicationContext
ILoggerFactory? LoggerFactory { get; }
char EmptyCharacter { get; }
IFocusManager FocusManager { get; }
bool SupportUtf8Output { get; set; }
ITheme? Theme { get; set; }
}

View File

@@ -0,0 +1,8 @@
using TerminalUI.Styling.Controls;
namespace TerminalUI.Styling;
public class ControlThemes : IControlThemes
{
public required IProgressBarTheme ProgressBar { get; init; }
}

View File

@@ -0,0 +1,23 @@
using TerminalUI.Color;
namespace TerminalUI.Styling.Controls;
public interface IProgressBarTheme
{
IColor? ForegroundColor { get; init; }
IColor? BackgroundColor { get; init; }
IColor? UnfilledForeground { get; init; }
IColor? UnfilledBackground { get; init; }
char? FilledCharacter { get; init; }
char? UnfilledCharacter { get; init; }
char? Fraction1Per8Character { get; init; }
char? Fraction2Per8Character { get; init; }
char? Fraction3Per8Character { get; init; }
char? Fraction4Per8Character { get; init; }
char? Fraction5Per8Character { get; init; }
char? Fraction6Per8Character { get; init; }
char? Fraction7Per8Character { get; init; }
char? FractionFull { get; init; }
char? LeftCap { get; init; }
char? RightCap { get; init; }
}

View File

@@ -0,0 +1,43 @@
using TerminalUI.Color;
namespace TerminalUI.Styling.Controls;
public class ProgressBarTheme : IProgressBarTheme
{
public ProgressBarTheme(){}
public ProgressBarTheme(ProgressBarTheme theme)
{
ForegroundColor = theme.ForegroundColor;
BackgroundColor = theme.BackgroundColor;
UnfilledForeground = theme.UnfilledForeground;
UnfilledBackground = theme.UnfilledBackground;
FilledCharacter = theme.FilledCharacter;
UnfilledCharacter = theme.UnfilledCharacter;
Fraction1Per8Character = theme.Fraction1Per8Character;
Fraction2Per8Character = theme.Fraction2Per8Character;
Fraction3Per8Character = theme.Fraction3Per8Character;
Fraction4Per8Character = theme.Fraction4Per8Character;
Fraction5Per8Character = theme.Fraction5Per8Character;
Fraction6Per8Character = theme.Fraction6Per8Character;
Fraction7Per8Character = theme.Fraction7Per8Character;
FractionFull = theme.FractionFull;
LeftCap = theme.LeftCap;
RightCap = theme.RightCap;
}
public IColor? ForegroundColor { get; init; }
public IColor? BackgroundColor { get; init; }
public IColor? UnfilledForeground { get; init; }
public IColor? UnfilledBackground { get; init; }
public char? FilledCharacter { get; init; }
public char? UnfilledCharacter { get; init; }
public char? Fraction1Per8Character { get; init; }
public char? Fraction2Per8Character { get; init; }
public char? Fraction3Per8Character { get; init; }
public char? Fraction4Per8Character { get; init; }
public char? Fraction5Per8Character { get; init; }
public char? Fraction6Per8Character { get; init; }
public char? Fraction7Per8Character { get; init; }
public char? FractionFull { get; init; }
public char? LeftCap { get; init; }
public char? RightCap { get; init; }
}

View File

@@ -0,0 +1,8 @@
using TerminalUI.Styling.Controls;
namespace TerminalUI.Styling;
public interface IControlThemes
{
IProgressBarTheme ProgressBar { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace TerminalUI.Styling;
public interface ITheme
{
IControlThemes ControlThemes { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace TerminalUI.Styling;
public class Theme : ITheme
{
public required IControlThemes ControlThemes { get; init; }
}