diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Configuration/ConsoleApplicationConfiguration.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Configuration/ConsoleApplicationConfiguration.cs index c6c22e0..d6c4a25 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Configuration/ConsoleApplicationConfiguration.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Configuration/ConsoleApplicationConfiguration.cs @@ -3,4 +3,5 @@ public class ConsoleApplicationConfiguration { public string? ConsoleDriver { get; set; } + public bool DisableUtf8 { get; set; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs index 327e000..da58351 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs @@ -1,6 +1,7 @@ using TerminalUI.Color; namespace FileTime.ConsoleUI.App.Styling; +using IConsoleTheme = TerminalUI.Styling.ITheme; public interface ITheme { @@ -17,5 +18,6 @@ public interface ITheme IColor? SelectedTabBackgroundColor { get; } IColor? WarningForegroundColor { get; } IColor? ErrorForegroundColor { get; } + IConsoleTheme? ConsoleTheme { get; } ListViewItemTheme ListViewItemTheme { get; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs index 915b6d0..c6b2d91 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -1,13 +1,11 @@ using System.Collections.Specialized; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; +using FileTime.ConsoleUI.App.Configuration; using FileTime.ConsoleUI.App.KeyInputHandling; -using FileTime.Core.Command.CreateContainer; -using FileTime.Core.Models; -using FileTime.Core.Timeline; using GeneralInputKey; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using TerminalUI; using TerminalUI.ConsoleDrivers; @@ -23,40 +21,36 @@ public class App : IApplication private readonly ILifecycleService _lifecycleService; - private readonly IConsoleAppState _consoleAppState; - private readonly IAppKeyService _appKeyService; private readonly MainWindow _mainWindow; private readonly IApplicationContext _applicationContext; private readonly IConsoleDriver _consoleDriver; private readonly IAppState _appState; private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; private readonly IKeyInputHandlerService _keyInputHandlerService; private readonly Thread _renderThread; public App( ILifecycleService lifecycleService, IKeyInputHandlerService keyInputHandlerService, - IConsoleAppState consoleAppState, IAppKeyService appKeyService, MainWindow mainWindow, IApplicationContext applicationContext, IConsoleDriver consoleDriver, IAppState appState, - ILogger logger, - IServiceProvider serviceProvider) + IOptions consoleApplicationConfiguration, + ILogger logger) { _lifecycleService = lifecycleService; _keyInputHandlerService = keyInputHandlerService; - _consoleAppState = consoleAppState; _appKeyService = appKeyService; _mainWindow = mainWindow; _applicationContext = applicationContext; _consoleDriver = consoleDriver; _appState = appState; _logger = logger; - _serviceProvider = serviceProvider; + + _applicationContext.SupportUtf8Output = !consoleApplicationConfiguration.Value.DisableUtf8; _renderThread = new Thread(Render); } @@ -81,12 +75,6 @@ public class App : IApplication var focusManager = _applicationContext.FocusManager; - var command = _serviceProvider.GetRequiredService(); - command.Init(new FullName("local/C:/Test3"), "container1"); - var scheduler = _serviceProvider.GetRequiredService(); - - scheduler.AddCommand(command); - while (_applicationContext.IsRunning) { try diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs index e3f25ee..50fa472 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs @@ -1,32 +1,87 @@ using FileTime.App.Core.ViewModels.Timeline; +using FileTime.ConsoleUI.App.Styling; using TerminalUI.Controls; using TerminalUI.Extensions; +using TerminalUI.Models; +using TerminalUI.Styling.Controls; +using TerminalUI.ViewExtensions; namespace FileTime.ConsoleUI.App.Controls; public class Timeline { + private readonly IProgressBarTheme? _progressBarTheme; + + public Timeline(ITheme theme) + { + _progressBarTheme = theme.ConsoleTheme?.ControlThemes.ProgressBar; + } + public IView View() { var root = new Grid { ChildInitializer = { - new ItemsControl() + new ItemsControl { + Orientation = Orientation.Horizontal, ItemTemplate = () => { - var grid = new Grid() + var grid = new Grid { + Margin = "0 0 1 0", + Width = 20, + RowDefinitionsObject = "Auto Auto", ChildInitializer = { - new TextBlock() + new Grid { - - }.Setup(t => t.Bind( - t, - dc => dc.DisplayLabel.Value, - t => t.Text)) + ColumnDefinitionsObject = "* Auto", + ChildInitializer = + { + new TextBlock().Setup(t => t.Bind( + t, + dc => dc.DisplayLabel.Value, + t => t.Text)), + new TextBlock + { + Width = 5, + TextAlignment = TextAlignment.Right, + Extensions = {new GridPositionExtension(1, 0)} + }.Setup(t => t.Bind( + t, + dc => dc.TotalProgress.Value, + t => t.Text, + v => $"{v}%")), + } + }, + new ProgressBar + { + Theme = new ProgressBarTheme + { + ForegroundColor = _progressBarTheme?.ForegroundColor, + UnfilledForeground = _progressBarTheme?.UnfilledForeground, + FilledCharacter = '\u2594', + UnfilledCharacter = '\u2594', + Fraction1Per8Character = '\u2594', + Fraction2Per8Character = '\u2594', + Fraction3Per8Character = '\u2594', + Fraction4Per8Character = '\u2594', + Fraction5Per8Character = '\u2594', + Fraction6Per8Character = '\u2594', + Fraction7Per8Character = '\u2594', + FractionFull = '\u2594', + }, + Extensions = + { + new GridPositionExtension(0, 1) + } + } + .Setup(p => p.Bind( + p, + dc => dc.TotalProgress.Value, + p => p.Value)), } }; diff --git a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs index d6e206a..5115fcf 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs @@ -1,9 +1,15 @@ using FileTime.ConsoleUI.App; using FileTime.ConsoleUI.App.Styling; using TerminalUI.Color; +using TerminalUI.Styling; +using TerminalUI.Styling.Controls; +using ITheme = FileTime.ConsoleUI.App.Styling.ITheme; namespace FileTime.ConsoleUI.Styles; +using IConsoleTheme = TerminalUI.Styling.ITheme; +using ConsoleTheme = TerminalUI.Styling.Theme; + public record Theme( IColor? DefaultForegroundColor, IColor? DefaultForegroundAccentColor, @@ -19,6 +25,7 @@ public record Theme( IColor? WarningForegroundColor, IColor? ErrorForegroundColor, ListViewItemTheme ListViewItemTheme, + IConsoleTheme? ConsoleTheme, Type? ForegroundColors, Type? BackgroundColors) : ITheme, IColorSampleProvider; @@ -42,6 +49,19 @@ public static class DefaultThemes SelectedBackgroundColor: Color256Colors.Backgrounds.Gray, SelectedForegroundColor: Color256Colors.Foregrounds.Black ), + ConsoleTheme: new ConsoleTheme + { + ControlThemes = new ControlThemes + { + ProgressBar = new ProgressBarTheme + { + ForegroundColor = Color256Colors.Foregrounds.Blue, + BackgroundColor = Color256Colors.Backgrounds.Gray, + UnfilledForeground = Color256Colors.Foregrounds.Gray, + UnfilledBackground = Color256Colors.Backgrounds.Gray, + } + } + }, ForegroundColors: typeof(Color256Colors.Foregrounds), BackgroundColors: typeof(Color256Colors.Backgrounds) ); @@ -64,6 +84,19 @@ public static class DefaultThemes SelectedBackgroundColor: ConsoleColors.Backgrounds.Gray, SelectedForegroundColor: ConsoleColors.Foregrounds.Black ), + ConsoleTheme: new ConsoleTheme + { + ControlThemes = new ControlThemes + { + ProgressBar = new ProgressBarTheme + { + ForegroundColor = ConsoleColors.Foregrounds.Blue, + BackgroundColor = ConsoleColors.Backgrounds.Gray, + UnfilledForeground = ConsoleColors.Foregrounds.Gray, + UnfilledBackground = ConsoleColors.Backgrounds.Gray + } + } + }, ForegroundColors: typeof(ConsoleColors.Foregrounds), BackgroundColors: typeof(ConsoleColors.Backgrounds) ); diff --git a/src/ConsoleApp/FileTime.ConsoleUI/Program.cs b/src/ConsoleApp/FileTime.ConsoleUI/Program.cs index cb9cfc2..b6f42ef 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/Program.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/Program.cs @@ -9,8 +9,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Serilog; using Serilog.Debugging; +using TerminalUI; +using TerminalUI.Color; using TerminalUI.ConsoleDrivers; +using TerminalUI.Styling; +using ITheme = FileTime.ConsoleUI.App.Styling.ITheme; +Console.OutputEncoding = System.Text.Encoding.UTF8; IConsoleDriver? driver = null; (AppDataRoot, EnvironmentName) = Init.InitDevelopment(); @@ -27,6 +32,11 @@ try Log.Logger.Debug("Using driver {Driver}", driver.GetType().Name); driver.SetCursorVisible(false); + + var applicationContext = serviceProvider.GetRequiredService(); + var theme = serviceProvider.GetRequiredService(); + + applicationContext.Theme = theme.ConsoleTheme; var app = serviceProvider.GetRequiredService(); app.Run(); diff --git a/src/Core/FileTime.Core.Timeline/CommandScheduler.cs b/src/Core/FileTime.Core.Timeline/CommandScheduler.cs index 8be2142..372e4d9 100644 --- a/src/Core/FileTime.Core.Timeline/CommandScheduler.cs +++ b/src/Core/FileTime.Core.Timeline/CommandScheduler.cs @@ -13,7 +13,7 @@ public class CommandScheduler : ICommandScheduler private readonly Subject _containerToRefresh = new(); private readonly object _guard = new(); - private bool _isRunningEnabled = /*true*/ false; + private bool _isRunningEnabled = true; private bool _resourceIsInUse; public IObservable ContainerToRefresh { get; } diff --git a/src/FileTime.sln b/src/FileTime.sln index e6712a7..64a163b 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -131,6 +131,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.DependencyInject EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.Tests", "Library\TerminalUI.Tests\TerminalUI.Tests.csproj", "{30B6E288-F314-494B-8550-1329BFF664D2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.Examples", "Library\TerminalUI.Examples\TerminalUI.Examples.csproj", "{14AC0667-A660-4CFC-960E-77E5E5B46D15}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -357,6 +359,10 @@ Global {30B6E288-F314-494B-8550-1329BFF664D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {30B6E288-F314-494B-8550-1329BFF664D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {30B6E288-F314-494B-8550-1329BFF664D2}.Release|Any CPU.Build.0 = Release|Any CPU + {14AC0667-A660-4CFC-960E-77E5E5B46D15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14AC0667-A660-4CFC-960E-77E5E5B46D15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14AC0667-A660-4CFC-960E-77E5E5B46D15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14AC0667-A660-4CFC-960E-77E5E5B46D15}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -418,6 +424,7 @@ Global {91AE5B64-042B-4660-A8E8-D247E6E14A1E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {E72F6430-0E6E-4818-BD5F-114893ACB18E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {30B6E288-F314-494B-8550-1329BFF664D2} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {14AC0667-A660-4CFC-960E-77E5E5B46D15} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/Library/TerminalUI.Examples/Controls/ProgressBarExamples.cs b/src/Library/TerminalUI.Examples/Controls/ProgressBarExamples.cs new file mode 100644 index 0000000..b37c1aa --- /dev/null +++ b/src/Library/TerminalUI.Examples/Controls/ProgressBarExamples.cs @@ -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(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(100), new Position(0, 0)); + for (var i = 0; i < 10; i++) + { + RenderProgressBar(CreateProgressBar(10 * (i + 1)), new Position(0, i + 1)); + } + } + + private ProgressBar CreateProgressBar(int percent) => + new() + { + Value = percent, + Attached = true, + ApplicationContext = _applicationContext + }; + + private void RenderProgressBar(ProgressBar progressBar, Position position) + { + var renderContext = new RenderContext( + _driver, + true, + null, + null, + new() + ); + progressBar.Render(renderContext, position, new Size(10, 1)); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI.Examples/Mocks/MockRenderEngine.cs b/src/Library/TerminalUI.Examples/Mocks/MockRenderEngine.cs new file mode 100644 index 0000000..fae4268 --- /dev/null +++ b/src/Library/TerminalUI.Examples/Mocks/MockRenderEngine.cs @@ -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() + { + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI.Examples/Program.cs b/src/Library/TerminalUI.Examples/Program.cs new file mode 100644 index 0000000..6601068 --- /dev/null +++ b/src/Library/TerminalUI.Examples/Program.cs @@ -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(); +IServiceProvider provider = services.BuildServiceProvider(); + +var applicationContext = provider.GetRequiredService(); +applicationContext.Theme = new Theme +{ + ControlThemes = new ControlThemes + { + ProgressBar = new ProgressBarTheme + { + ForegroundColor = ConsoleColors.Foregrounds.Blue + } + } +}; + +Console.CursorVisible = false; +new ProgressBarExamples(applicationContext).LoadingExample(); +Console.CursorVisible = true; \ No newline at end of file diff --git a/src/Library/TerminalUI.Examples/TerminalUI.Examples.csproj b/src/Library/TerminalUI.Examples/TerminalUI.Examples.csproj new file mode 100644 index 0000000..55a520a --- /dev/null +++ b/src/Library/TerminalUI.Examples/TerminalUI.Examples.csproj @@ -0,0 +1,19 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/src/Library/TerminalUI/ApplicationContext.cs b/src/Library/TerminalUI/ApplicationContext.cs index ffd98ae..d5420cc 100644 --- a/src/Library/TerminalUI/ApplicationContext.cs +++ b/src/Library/TerminalUI/ApplicationContext.cs @@ -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) { diff --git a/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs index 62658fe..422ae51 100644 --- a/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs +++ b/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs @@ -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 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(); diff --git a/src/Library/TerminalUI/ConsoleDrivers/XTermDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/XTermDriver.cs index 0848763..e39a54f 100644 --- a/src/Library/TerminalUI/ConsoleDrivers/XTermDriver.cs +++ b/src/Library/TerminalUI/ConsoleDrivers/XTermDriver.cs @@ -7,6 +7,7 @@ namespace TerminalUI.ConsoleDrivers; public sealed class XTermDriver : DotnetDriver { private Position _initialCursorPosition; + public override bool Init() { _initialCursorPosition = GetCursorPosition(); diff --git a/src/Library/TerminalUI/Controls/Grid.cs b/src/Library/TerminalUI/Controls/Grid.cs index b89ac67..2566e46 100644 --- a/src/Library/TerminalUI/Controls/Grid.cs +++ b/src/Library/TerminalUI/Controls/Grid.cs @@ -153,6 +153,8 @@ public sealed class Grid : ChildContainerView, 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, true), @@ -177,29 +179,34 @@ public sealed class Grid : ChildContainerView, 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 columnWidths, ReadOnlySpan rowHeights, IReadOnlyDictionary<(int, int), List> viewsByPosition, @@ -207,7 +214,7 @@ public sealed class Grid : ChildContainerView, T>, IVisibilityChangeH int row, IReadOnlyList 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 : ChildContainerView, 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 : ChildContainerView, T>, IVisibilityChangeH } } + return true; + static Position GetRenderPosition( Position gridPosition, ReadOnlySpan columnWidths, diff --git a/src/Library/TerminalUI/Controls/ProgressBar.cs b/src/Library/TerminalUI/Controls/ProgressBar.cs new file mode 100644 index 0000000..e1278b5 --- /dev/null +++ b/src/Library/TerminalUI/Controls/ProgressBar.cs @@ -0,0 +1,168 @@ +using PropertyChanged.SourceGenerator; +using TerminalUI.Color; +using TerminalUI.Models; +using TerminalUI.Styling.Controls; + +namespace TerminalUI.Controls; + +public partial class ProgressBar : View, 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 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 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; +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index e12576a..bebaa60 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -78,16 +78,7 @@ public sealed partial class TextBlock : View, 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); diff --git a/src/Library/TerminalUI/Controls/TextBox.cs b/src/Library/TerminalUI/Controls/TextBox.cs index 55aa68e..03b97ed 100644 --- a/src/Library/TerminalUI/Controls/TextBox.cs +++ b/src/Library/TerminalUI/Controls/TextBox.cs @@ -104,16 +104,7 @@ public sealed partial class TextBox : View, 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); diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index 6300df3..184350e 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -67,7 +67,21 @@ public abstract partial class View : IView 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 : IView 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 : IView 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 : IView 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() where TChild : IView, new() diff --git a/src/Library/TerminalUI/IApplicationContext.cs b/src/Library/TerminalUI/IApplicationContext.cs index 73c351b..42361a3 100644 --- a/src/Library/TerminalUI/IApplicationContext.cs +++ b/src/Library/TerminalUI/IApplicationContext.cs @@ -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; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Styling/ControlThemes.cs b/src/Library/TerminalUI/Styling/ControlThemes.cs new file mode 100644 index 0000000..b62e5ca --- /dev/null +++ b/src/Library/TerminalUI/Styling/ControlThemes.cs @@ -0,0 +1,8 @@ +using TerminalUI.Styling.Controls; + +namespace TerminalUI.Styling; + +public class ControlThemes : IControlThemes +{ + public required IProgressBarTheme ProgressBar { get; init; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Styling/Controls/IProgressBarTheme.cs b/src/Library/TerminalUI/Styling/Controls/IProgressBarTheme.cs new file mode 100644 index 0000000..439a700 --- /dev/null +++ b/src/Library/TerminalUI/Styling/Controls/IProgressBarTheme.cs @@ -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; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Styling/Controls/ProgressBarTheme.cs b/src/Library/TerminalUI/Styling/Controls/ProgressBarTheme.cs new file mode 100644 index 0000000..a87b85b --- /dev/null +++ b/src/Library/TerminalUI/Styling/Controls/ProgressBarTheme.cs @@ -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; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Styling/IControlThemes.cs b/src/Library/TerminalUI/Styling/IControlThemes.cs new file mode 100644 index 0000000..b4944c2 --- /dev/null +++ b/src/Library/TerminalUI/Styling/IControlThemes.cs @@ -0,0 +1,8 @@ +using TerminalUI.Styling.Controls; + +namespace TerminalUI.Styling; + +public interface IControlThemes +{ + IProgressBarTheme ProgressBar { get; init; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Styling/ITheme.cs b/src/Library/TerminalUI/Styling/ITheme.cs new file mode 100644 index 0000000..c1a9196 --- /dev/null +++ b/src/Library/TerminalUI/Styling/ITheme.cs @@ -0,0 +1,6 @@ +namespace TerminalUI.Styling; + +public interface ITheme +{ + IControlThemes ControlThemes { get; init; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Styling/Theme.cs b/src/Library/TerminalUI/Styling/Theme.cs new file mode 100644 index 0000000..2bd50d0 --- /dev/null +++ b/src/Library/TerminalUI/Styling/Theme.cs @@ -0,0 +1,6 @@ +namespace TerminalUI.Styling; + +public class Theme : ITheme +{ + public required IControlThemes ControlThemes { get; init; } +} \ No newline at end of file