From df4fe93c81633b08e01d19036cad87d28025001f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Thu, 17 Aug 2023 15:55:00 +0200 Subject: [PATCH] Fill not render space (Render engine, Border) --- .../Controls/CommandPalette.cs | 2 + .../Controls/ProgressBarExamples.cs | 6 +- src/Library/TerminalUI/Array2DHelper.cs | 96 ++++++++++++++++++ src/Library/TerminalUI/Controls/Border.cs | 98 ++++++++++++++----- ...ontainerView.cs => ChildCollectionView.cs} | 4 +- .../TerminalUI/Controls/ContentView.cs | 2 +- src/Library/TerminalUI/Controls/Grid.cs | 32 ++---- .../TerminalUI/Controls/ItemsControl.cs | 2 + src/Library/TerminalUI/Controls/ListView.cs | 19 ++-- .../TerminalUI/Controls/ProgressBar.cs | 26 ++--- src/Library/TerminalUI/Controls/Rectangle.cs | 34 ++++--- src/Library/TerminalUI/Controls/StackPanel.cs | 17 ++-- src/Library/TerminalUI/Controls/TextBlock.cs | 9 +- src/Library/TerminalUI/Controls/TextBox.cs | 17 ++-- src/Library/TerminalUI/Controls/View.cs | 80 ++++++++------- .../TerminalUI/Models/RenderContext.cs | 8 +- src/Library/TerminalUI/RenderEngine.cs | 67 ++++++++++++- 17 files changed, 373 insertions(+), 146 deletions(-) create mode 100644 src/Library/TerminalUI/Array2DHelper.cs rename src/Library/TerminalUI/Controls/{ChildContainerView.cs => ChildCollectionView.cs} (95%) diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs index 2a25486..b9a59b2 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs @@ -2,6 +2,7 @@ using FileTime.App.CommandPalette.ViewModels; using FileTime.ConsoleUI.App.Styling; using GeneralInputKey; +using TerminalUI.Color; using TerminalUI.Controls; using TerminalUI.Extensions; using TerminalUI.Models; @@ -59,6 +60,7 @@ public class CommandPalette Margin = 5, Padding = 1, MaxWidth = 50, + Fill = SpecialColor.None, Content = new Grid { RowDefinitionsObject = "Auto *", diff --git a/src/Library/TerminalUI.Examples/Controls/ProgressBarExamples.cs b/src/Library/TerminalUI.Examples/Controls/ProgressBarExamples.cs index a7b4254..861c392 100644 --- a/src/Library/TerminalUI.Examples/Controls/ProgressBarExamples.cs +++ b/src/Library/TerminalUI.Examples/Controls/ProgressBarExamples.cs @@ -52,14 +52,16 @@ public class ProgressBarExamples private void RenderProgressBar(ProgressBar progressBar, Position position) { + var s = new Size(10, 1); var renderContext = new RenderContext( _driver, true, null, null, new(), - new TextFormatContext(true) + new TextFormatContext(true), + new bool[s.Width,s.Height] ); - progressBar.Render(renderContext, position, new Size(10, 1)); + progressBar.Render(renderContext, position, s); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Array2DHelper.cs b/src/Library/TerminalUI/Array2DHelper.cs new file mode 100644 index 0000000..3fa9f4c --- /dev/null +++ b/src/Library/TerminalUI/Array2DHelper.cs @@ -0,0 +1,96 @@ +using TerminalUI.ConsoleDrivers; +using TerminalUI.Models; + +namespace TerminalUI; + +public static class Array2DHelper +{ + public static void CombineArray2Ds(T[,] array1, T[,] array2, Position array2Delta, TResult[,] array3, Func func) + where T : struct + where TResult : struct + { + var array1Size = new Size(array1.GetLength(0), array1.GetLength(1)); + var array2Size = new Size(array2.GetLength(0), array2.GetLength(1)); + var array3Size = new Size(array3.GetLength(0), array3.GetLength(1)); + + var maxX = int.Max(array1Size.Width, array2Size.Width + array2Delta.X); + maxX = int.Max(maxX, array3Size.Width); + + var maxY = int.Max(array1Size.Height, array2Size.Height + array2Delta.Y); + maxY = int.Max(maxY, array3Size.Height); + + + for (var x = 0; x < maxX; x++) + { + for (var y = 0; y < maxY; y++) + { + if (x >= array3Size.Width + || y >= array3Size.Height) continue; + + T? v1 = x < array1Size.Width && y < array1Size.Height ? array1[x, y] : null; + + + var array2X = x - array2Delta.X; + var array2Y = y - array2Delta.Y; + T? v2 = array2X >= 0 + && array2X < array2Size.Width + && array2Y >= 0 + && array2Y < array2Size.Height + ? array2[array2X, array2Y] + : null; + + array3[x, y] = func(v1, v2); + } + } + } + + public static void RenderEmpty( + IConsoleDriver driver, + bool[,] updatedCells, + bool[,] resultCells, + char fillChar, + Position position, + Size size + ) + { + var endX = position.X + size.Width; + var endY = position.Y + size.Height; + for (var y = position.Y; y < endY; y++) + { + for (var x = position.X; x < endX; x++) + { + if (updatedCells[x, y]) continue; + + var startIndex = x; + while (x < endX && !updatedCells[x, y]) + { + x++; + } + + RenderEmpty(driver, resultCells, fillChar, startIndex, x, y); + } + } + } + + private static void RenderEmpty( + IConsoleDriver driver, + bool[,] resultCells, + char fillChar, + int startX, + int endX, + int y + ) + { + var length = endX - startX; + Span text = stackalloc char[length]; + text.Fill(fillChar); + + driver.SetCursorPosition(new Position(startX, y)); + driver.Write(text); + + for (var x = startX; x < endX; x++) + { + resultCells[x, y] = true; + } + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/Border.cs b/src/Library/TerminalUI/Controls/Border.cs index 89cc9fe..0394c4d 100644 --- a/src/Library/TerminalUI/Controls/Border.cs +++ b/src/Library/TerminalUI/Controls/Border.cs @@ -1,4 +1,5 @@ using PropertyChanged.SourceGenerator; +using TerminalUI.Color; using TerminalUI.Models; using TerminalUI.Traits; @@ -16,6 +17,7 @@ public sealed partial class Border : ContentView, T>, IDisplayView [Notify] private char _topRightChar = '┐'; [Notify] private char _bottomLeftChar = '└'; [Notify] private char _bottomRightChar = '┘'; + [Notify] private IColor? _fill; public Border() { @@ -25,6 +27,9 @@ public sealed partial class Border : ContentView, T>, IDisplayView RerenderProperties.Add(nameof(LeftChar)); RerenderProperties.Add(nameof(RightChar)); RerenderProperties.Add(nameof(BottomChar)); + RerenderProperties.Add(nameof(TopLeftChar)); + RerenderProperties.Add(nameof(TopRightChar)); + RerenderProperties.Add(nameof(Fill)); } protected override Size CalculateSize() @@ -51,12 +56,19 @@ public sealed partial class Border : ContentView, T>, IDisplayView + DataContext?.GetType().Name); } + var backgroundColor = Background ?? renderContext.Background; + var foregroundColor = Foreground ?? renderContext.Foreground; + var fillColor = Fill ?? Background ?? renderContext.Background; + var childPosition = new Position(X: position.X + _borderThickness.Left, Y: position.Y + _borderThickness.Top); var childSize = new Size( Width: size.Width - _borderThickness.Left - _borderThickness.Right, Height: size.Height - _borderThickness.Top - _borderThickness.Bottom ); + var childPositionWithoutPadding = childPosition; + var childSizeWithoutPadding = childSize; + if (_padding.Left > 0 || _padding.Top > 0 || _padding.Right > 0 || _padding.Bottom > 0) { childPosition = new Position(X: childPosition.X + _padding.Left, Y: childPosition.Y + _padding.Top); @@ -66,85 +78,117 @@ public sealed partial class Border : ContentView, T>, IDisplayView ); } - var contentRendered = ContentRendererMethod(renderContext, childPosition, childSize); + // Same size as the original. + // Although wasting memory, but we would have to delta the position when setting "updatedcells" + // It is easier and also covers the fact the children use a different array + var borderChildUpdatedCells = new bool[ + renderContext.UpdatedCells.GetLength(0), + renderContext.UpdatedCells.GetLength(1) + ]; + var childRenderContext = renderContext with {UpdatedCells = borderChildUpdatedCells}; + + var contentRendered = ContentRendererMethod(childRenderContext, childPosition, childSize); if (contentRendered) { var driver = renderContext.ConsoleDriver; driver.ResetStyle(); - SetColorsForDriver(renderContext); + SetStyleColor(renderContext, foregroundColor, backgroundColor); + } - RenderTopBorder(renderContext, position, size); - RenderBottomBorder(renderContext, position, size); - RenderLeftBorder(renderContext, position, size); - RenderRightBorder(renderContext, position, size); + var updateCellsOnly = !contentRendered; + RenderTopBorder(renderContext, position, size, updateCellsOnly); + RenderBottomBorder(renderContext, position, size, updateCellsOnly); + RenderLeftBorder(renderContext, position, size, updateCellsOnly); + RenderRightBorder(renderContext, position, size, updateCellsOnly); - RenderTopLeftCorner(renderContext, position); - RenderTopRightCorner(renderContext, position, size); - RenderBottomLeftCorner(renderContext, position, size); - RenderBottomRightCorner(renderContext, position, size); + RenderTopLeftCorner(renderContext, position, updateCellsOnly); + RenderTopRightCorner(renderContext, position, size, updateCellsOnly); + RenderBottomLeftCorner(renderContext, position, size, updateCellsOnly); + RenderBottomRightCorner(renderContext, position, size, updateCellsOnly); - //TODO render padding + if (fillColor != null) + { + SetStyleColor(renderContext, foregroundColor, fillColor); + + // Use the same array that children use. Also use that area, so we working only inside the border + Array2DHelper.RenderEmpty( + renderContext.ConsoleDriver, + borderChildUpdatedCells, + borderChildUpdatedCells, + ApplicationContext!.EmptyCharacter, + childPositionWithoutPadding, + childSizeWithoutPadding + ); + + //Write back the changes to the original array + Array2DHelper.CombineArray2Ds( + renderContext.UpdatedCells, + borderChildUpdatedCells, + new Position(0, 0), + renderContext.UpdatedCells, + (a, b) => (a ?? false) || (b ?? false) + ); } return contentRendered; } - private void RenderTopBorder(in RenderContext renderContext, Position position, Size size) + private void RenderTopBorder(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly) { position = position with {X = position.X + _borderThickness.Left}; size = new Size(Width: size.Width - _borderThickness.Left - _borderThickness.Right, Height: _borderThickness.Top); - RenderText(_topChar, renderContext.ConsoleDriver, position, size); + RenderText(_topChar, renderContext, position, size, updateCellsOnly); } - private void RenderBottomBorder(in RenderContext renderContext, Position position, Size size) + private void RenderBottomBorder(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly) { position = new Position(X: position.X + _borderThickness.Left, Y: position.Y + size.Height - _borderThickness.Bottom); size = new Size(Width: size.Width - _borderThickness.Left - _borderThickness.Right, Height: _borderThickness.Bottom); - RenderText(_bottomChar, renderContext.ConsoleDriver, position, size); + RenderText(_bottomChar, renderContext, position, size, updateCellsOnly); } - private void RenderLeftBorder(in RenderContext renderContext, Position position, Size size) + private void RenderLeftBorder(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly) { position = position with {Y = position.Y + _borderThickness.Top}; size = new Size(Width: _borderThickness.Left, Height: size.Height - _borderThickness.Top - _borderThickness.Bottom); - RenderText(_leftChar, renderContext.ConsoleDriver, position, size); + RenderText(_leftChar, renderContext, position, size, updateCellsOnly); } - private void RenderRightBorder(in RenderContext renderContext, Position position, Size size) + private void RenderRightBorder(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly) { position = new Position(X: position.X + size.Width - _borderThickness.Right, Y: position.Y + _borderThickness.Top); size = new Size(Width: _borderThickness.Right, Height: size.Height - _borderThickness.Top - _borderThickness.Bottom); - RenderText(_rightChar, renderContext.ConsoleDriver, position, size); + RenderText(_rightChar, renderContext, position, size, updateCellsOnly); } - private void RenderTopLeftCorner(in RenderContext renderContext, Position position) + private void RenderTopLeftCorner(in RenderContext renderContext, Position position, bool updateCellsOnly) { if (_borderThickness.Left == 0 || _borderThickness.Top == 0) return; var size = new Size(Width: _borderThickness.Left, Height: _borderThickness.Top); - RenderText(_topLeftChar, renderContext.ConsoleDriver, position, size); + RenderText(_topLeftChar, renderContext, position, size, updateCellsOnly); } - private void RenderTopRightCorner(in RenderContext renderContext, Position position, Size size) + private void RenderTopRightCorner(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly) { if (_borderThickness.Right == 0 || _borderThickness.Top == 0) return; position = position with {X = position.X + size.Width - _borderThickness.Right}; size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Top); - RenderText(_topRightChar, renderContext.ConsoleDriver, position, size); + RenderText(_topRightChar, renderContext, position, size, updateCellsOnly); } - private void RenderBottomLeftCorner(in RenderContext renderContext, Position position, Size size) + private void RenderBottomLeftCorner(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly) { if (_borderThickness.Left == 0 || _borderThickness.Bottom == 0) return; position = position with {Y = position.Y + size.Height - _borderThickness.Bottom}; size = new Size(Width: _borderThickness.Left, Height: _borderThickness.Bottom); - RenderText(_bottomLeftChar, renderContext.ConsoleDriver, position, size); + RenderText(_bottomLeftChar, renderContext, position, size, updateCellsOnly); } - private void RenderBottomRightCorner(in RenderContext renderContext, Position position, Size size) + private void RenderBottomRightCorner(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly) { if (_borderThickness.Right == 0 || _borderThickness.Bottom == 0) return; @@ -153,6 +197,6 @@ public sealed partial class Border : ContentView, T>, IDisplayView Y: position.Y + size.Height - _borderThickness.Bottom ); size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Bottom); - RenderText(_bottomRightChar, renderContext.ConsoleDriver, position, size); + RenderText(_bottomRightChar, renderContext, position, size, updateCellsOnly); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ChildContainerView.cs b/src/Library/TerminalUI/Controls/ChildCollectionView.cs similarity index 95% rename from src/Library/TerminalUI/Controls/ChildContainerView.cs rename to src/Library/TerminalUI/Controls/ChildCollectionView.cs index bc88b22..c3b514e 100644 --- a/src/Library/TerminalUI/Controls/ChildContainerView.cs +++ b/src/Library/TerminalUI/Controls/ChildCollectionView.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; namespace TerminalUI.Controls; -public abstract class ChildContainerView +public abstract class ChildCollectionView : View, IChildContainer where TConcrete : View { @@ -12,7 +12,7 @@ public abstract class ChildContainerView public ReadOnlyObservableCollection Children { get; } public ChildInitializer ChildInitializer { get; } - protected ChildContainerView() + protected ChildCollectionView() { ChildInitializer = new ChildInitializer(this); Children = new ReadOnlyObservableCollection(_children); diff --git a/src/Library/TerminalUI/Controls/ContentView.cs b/src/Library/TerminalUI/Controls/ContentView.cs index 1292ed3..52dd73d 100644 --- a/src/Library/TerminalUI/Controls/ContentView.cs +++ b/src/Library/TerminalUI/Controls/ContentView.cs @@ -48,7 +48,7 @@ public abstract partial class ContentView { if (_placeholderRenderDone) return false; _placeholderRenderDone = true; - RenderEmpty(renderContext, position, size); + RenderEmpty(renderContext, position, size, false); return true; } diff --git a/src/Library/TerminalUI/Controls/Grid.cs b/src/Library/TerminalUI/Controls/Grid.cs index b2f0c77..70aca32 100644 --- a/src/Library/TerminalUI/Controls/Grid.cs +++ b/src/Library/TerminalUI/Controls/Grid.cs @@ -7,7 +7,7 @@ using TerminalUI.ViewExtensions; namespace TerminalUI.Controls; -public sealed class Grid : ChildContainerView, T>, IVisibilityChangeHandler +public sealed class Grid : ChildCollectionView, T>, IVisibilityChangeHandler { private readonly List _forceRerenderChildren = new(); private readonly object _forceRerenderChildrenLock = new(); @@ -202,7 +202,8 @@ public sealed class Grid : ChildContainerView, T>, IVisibilityChangeH } } - private bool RenderViewsByPosition(RenderContext context, + private bool RenderViewsByPosition( + in RenderContext context, Position gridPosition, Size gridSize, ReadOnlySpan columnWidths, @@ -238,39 +239,26 @@ public sealed class Grid : ChildContainerView, T>, IVisibilityChangeH if (!viewsByPosition.TryGetValue((column, row), out var children)) { - RenderEmpty(context, renderPosition, renderSize); + RenderEmpty(context, renderPosition, renderSize, false); return true; } var needsRerender = children.Any(forceRerenderChildren.Contains); + var updatedContext = context; if (needsRerender) { - context = new RenderContext( - context.ConsoleDriver, - true, - context.Foreground, - context.Background, - context.Statistics, - context.TextFormat - ); - RenderEmpty(context, renderPosition, renderSize); + updatedContext = context with {ForceRerender = true}; + RenderEmpty(updatedContext, renderPosition, renderSize, false); } //This implies that children further back in the list will be rendered on top of children placed before in the list. - foreach (var child in children.Where(child => child.IsVisible)) + foreach (var child in children) { - var rendered = child.Render(context, renderPosition, renderSize); + var rendered = child.Render(updatedContext, renderPosition, renderSize); if (rendered && !needsRerender) { needsRerender = true; - context = new RenderContext( - context.ConsoleDriver, - true, - context.Foreground, - context.Background, - context.Statistics, - context.TextFormat - ); + updatedContext = context with {ForceRerender = true}; } } diff --git a/src/Library/TerminalUI/Controls/ItemsControl.cs b/src/Library/TerminalUI/Controls/ItemsControl.cs index a39a8fd..89c9d88 100644 --- a/src/Library/TerminalUI/Controls/ItemsControl.cs +++ b/src/Library/TerminalUI/Controls/ItemsControl.cs @@ -162,6 +162,8 @@ public sealed partial class ItemsControl ? childSize.Height : childSize.Width; } + + // TODO: clean non used space return neededRerender; } diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs index 0cc1844..7789572 100644 --- a/src/Library/TerminalUI/Controls/ListView.cs +++ b/src/Library/TerminalUI/Controls/ListView.cs @@ -232,6 +232,8 @@ public sealed partial class ListView : View : View listViewItems.Length) lastItemIndex = listViewItems.Length; + var anyRendered = false; for (var i = renderStartIndex; i < lastItemIndex; i++) { var item = listViewItems[i]; - item.Render(renderContext, position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width}); + anyRendered = + item.Render(renderContext, position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width}) + || anyRendered; deltaY += requestedItemSize.Height; } - var driver = ApplicationContext!.ConsoleDriver; - var placeholder = new string(' ', size.Width); - driver.ResetStyle(); - for (var i = deltaY; i < size.Height; i++) - { - driver.SetCursorPosition(position with {Y = position.Y + i}); - driver.Write(placeholder); - } + // TODO: this should only render if deltaY is changed compared to last render or if last render was a horizontal + RenderEmpty(renderContext, position with {Y = position.Y + deltaY}, size with {Height = size.Height - deltaY}, false); - return true; + return anyRendered; } private ReadOnlySpan> InstantiateItemViews() diff --git a/src/Library/TerminalUI/Controls/ProgressBar.cs b/src/Library/TerminalUI/Controls/ProgressBar.cs index b97e40d..b23b03b 100644 --- a/src/Library/TerminalUI/Controls/ProgressBar.cs +++ b/src/Library/TerminalUI/Controls/ProgressBar.cs @@ -67,7 +67,7 @@ public partial class ProgressBar : View, T> unfilledForeground, unfilledBackground); - if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false; + var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderState); var utf8Support = ApplicationContext!.SupportUtf8Output; var unfilledCharacter = unfilledCharacterS.GetChar(utf8Support); @@ -117,16 +117,17 @@ public partial class ProgressBar : View, T> var textStartPosition = position; if (leftCap.HasValue) { - RenderText(leftCap.Value, driver, position, size with {Width = 1}); + RenderText(leftCap.Value, renderContext, position, size with {Width = 1}, skipRender); textStartPosition = textStartPosition with {X = textStartPosition.X + 1}; } // Filled RenderText( filledText, - driver, + renderContext, textStartPosition, - size with {Width = progressQuotientWidth} + size with {Width = progressQuotientWidth}, + skipRender ); // Transient character @@ -135,9 +136,10 @@ public partial class ProgressBar : View, T> SetStyleColor(renderContext, foreground, unfilledBackground); RenderText( transientChar, - driver, + renderContext, textStartPosition with {X = textStartPosition.X + progressQuotientWidth}, - size with {Width = 1} + size with {Width = 1}, + skipRender ); } @@ -150,9 +152,10 @@ public partial class ProgressBar : View, T> SetStyleColor(renderContext, unfilledForeground, unfilledBackground); RenderText( unfilledText, - driver, + renderContext, textStartPosition with {X = textStartPosition.X + progressQuotientWidth + 1}, - size with {Width = progressRemainderWidth} + size with {Width = progressRemainderWidth}, + skipRender ); } @@ -162,12 +165,13 @@ public partial class ProgressBar : View, T> SetStyleColor(renderContext, foreground, background); RenderText( rightCap.Value, - driver, + renderContext, position with {X = position.X + size.Width - 1}, - size with {Width = 1}); + size with {Width = 1}, + skipRender); } - return true; + return !skipRender; } private bool NeedsRerender(RenderState renderState) diff --git a/src/Library/TerminalUI/Controls/Rectangle.cs b/src/Library/TerminalUI/Controls/Rectangle.cs index 80e0428..4bfdf27 100644 --- a/src/Library/TerminalUI/Controls/Rectangle.cs +++ b/src/Library/TerminalUI/Controls/Rectangle.cs @@ -13,32 +13,40 @@ public sealed partial class Rectangle : View, T>, IDisplayView IColor? Color); private RenderState? _lastRenderState; + + [Notify] private IColor? _fill; + + public Rectangle() + { + RerenderProperties.Add(nameof(Fill)); + } + protected override Size CalculateSize() => new(Width ?? 0, Height ?? 0); protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size) { - var color = Background ?? renderContext.Background; - var renderState = new RenderState(position, size, color); - if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false; + var fillColor = Fill ?? Background ?? renderContext.Background; + var renderState = new RenderState(position, size, fillColor); + var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderState); _lastRenderState = renderState; var driver = renderContext.ConsoleDriver; - var s = new string(' ', size.Width); driver.ResetStyle(); - if (color is not null) + if (fillColor is not null) { - driver.SetBackgroundColor(color); + driver.SetBackgroundColor(fillColor); } - var height = size.Height; - for (var i = 0; i < height; i++) - { - driver.SetCursorPosition(position with {Y = position.Y + i}); - driver.Write(s); - } + RenderEmpty( + renderContext, + position, + size, + skipRender, + false + ); - return true; + return !skipRender; } private bool NeedsRerender(RenderState renderState) diff --git a/src/Library/TerminalUI/Controls/StackPanel.cs b/src/Library/TerminalUI/Controls/StackPanel.cs index 91ef7fd..67e234b 100644 --- a/src/Library/TerminalUI/Controls/StackPanel.cs +++ b/src/Library/TerminalUI/Controls/StackPanel.cs @@ -4,7 +4,7 @@ using TerminalUI.Traits; namespace TerminalUI.Controls; -public sealed partial class StackPanel : ChildContainerView, T>, IVisibilityChangeHandler +public sealed partial class StackPanel : ChildCollectionView, T>, IVisibilityChangeHandler { private readonly List _forceRerenderChildren = new(); private readonly object _forceRerenderChildrenLock = new(); @@ -39,7 +39,10 @@ public sealed partial class StackPanel : ChildContainerView, T> return new Size(width, height); } - protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size) + protected override bool DefaultRenderer( + in RenderContext renderContext, + Position position, + Size size) { var neededRerender = false; IReadOnlyList forceRerenderChildren; @@ -102,9 +105,10 @@ public sealed partial class StackPanel : ChildContainerView, T> SetColorsForDriver(renderContext); RenderText( text, - renderContext.ConsoleDriver, + renderContext, position with {X = position.X + delta}, - size with {Width = leftWidth} + size with {Width = leftWidth}, + !neededRerender ); } else @@ -116,9 +120,10 @@ public sealed partial class StackPanel : ChildContainerView, T> SetColorsForDriver(renderContext); RenderText( text, - renderContext.ConsoleDriver, + renderContext, position with {Y = position.Y + delta}, - size with {Height = leftHeight} + size with {Height = leftHeight}, + !neededRerender ); } diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index b8699d9..aeb8bcd 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -58,7 +58,7 @@ public sealed partial class TextBlock : View, T>, IDisplayView background, _textFormat); - if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false; + var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderState); _lastRenderState = renderState; @@ -67,7 +67,7 @@ public sealed partial class TextBlock : View, T>, IDisplayView if (_placeholderRenderDone) { _placeholderRenderDone = true; - RenderEmpty(renderContext, position, size); + RenderEmpty(renderContext, position, size, skipRender); } return false; @@ -75,12 +75,11 @@ public sealed partial class TextBlock : View, T>, IDisplayView _placeholderRenderDone = false; - var driver = renderContext.ConsoleDriver; SetStyleColor(renderContext, foreground, background, _textFormat); - RenderText(_textLines, driver, position, size, TransformText); + RenderText(_textLines, renderContext, position, size, skipRender, TransformText); - return true; + return !skipRender; } private string TransformText(string text, Position position, Size size) diff --git a/src/Library/TerminalUI/Controls/TextBox.cs b/src/Library/TerminalUI/Controls/TextBox.cs index 14bc106..03fdf35 100644 --- a/src/Library/TerminalUI/Controls/TextBox.cs +++ b/src/Library/TerminalUI/Controls/TextBox.cs @@ -100,38 +100,39 @@ public sealed partial class TextBox : View, T>, IFocusable, IDispl foreground, background); - if (!renderContext.ForceRerender && !NeedsRerender(renderStatus)) return false; + var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderStatus); _lastRenderState = renderStatus; var driver = renderContext.ConsoleDriver; SetStyleColor(renderContext, foreground, background); - RenderEmpty(renderContext, position, size); + RenderEmpty(renderContext, position, size, skipRender); if (PasswordChar is { } passwordChar && !char.IsControl(passwordChar)) { for (var i = 0; i < _textLines.Count; i++) { var pos = position with {Y = position.Y + i}; - RenderPasswordTextLine(_textLines[i], passwordChar, driver, pos, size); + RenderPasswordTextLine(_textLines[i], passwordChar, renderContext, pos, size, skipRender); } } else { - RenderText(_textLines, driver, position, size); + RenderText(_textLines, renderContext, position, size, skipRender); } _cursorPosition = position + _relativeCursorPosition; - return true; + return !skipRender; } private void RenderPasswordTextLine( string sourceText, char passwordChar, - IConsoleDriver driver, + in RenderContext renderContext, Position position, - Size size) + Size size, + bool updateCellsOnly) { Span text = stackalloc char[sourceText.Length]; for (var j = 0; j < text.Length; j++) @@ -139,7 +140,7 @@ public sealed partial class TextBox : View, T>, IFocusable, IDispl text[j] = passwordChar; } - RenderText(text, driver, position, size); + RenderText(text, renderContext, position, size, updateCellsOnly); } private bool NeedsRerender(RenderState renderState) diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index 0af6fee..592db80 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -6,7 +6,6 @@ using System.Runtime.CompilerServices; using GeneralInputKey; using PropertyChanged.SourceGenerator; using TerminalUI.Color; -using TerminalUI.ConsoleDrivers; using TerminalUI.Models; using TerminalUI.TextFormat; using TerminalUI.Traits; @@ -200,12 +199,18 @@ public abstract partial class View : IView where TConcrete : Vi return renderResult; } - protected void RenderEmpty(in RenderContext renderContext, Position position, Size size) + protected void RenderEmpty(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly, bool resetStyle = true) { + UpdateCells(renderContext.UpdatedCells, position, size.Width, size.Height); + if (updateCellsOnly) return; var driver = renderContext.ConsoleDriver; - driver.ResetStyle(); + if (resetStyle) + { + driver.ResetStyle(); + } - var placeHolder = new string(ApplicationContext!.EmptyCharacter, size.Width); + Span placeHolder = stackalloc char[size.Width]; + placeHolder.Fill(ApplicationContext!.EmptyCharacter); for (var i = 0; i < size.Height; i++) { driver.SetCursorPosition(position with {Y = position.Y + i}); @@ -213,13 +218,30 @@ public abstract partial class View : IView where TConcrete : Vi } } + private void UpdateCells(bool[,] renderContextUpdatedCells, Position position, int sizeWidth, int sizeHeight) + { + for (var x = 0; x < sizeWidth; x++) + { + for (var y = 0; y < sizeHeight; y++) + { + renderContextUpdatedCells[position.X + x, position.Y + y] = true; + } + } + } + protected void RenderText( IList textLines, - IConsoleDriver driver, + in RenderContext renderContext, Position position, Size size, + bool updateCellsOnly, TextTransformer? textTransformer = null) { + UpdateCells(renderContext.UpdatedCells, position, size.Width, size.Height); + + if (updateCellsOnly) return; + + var driver = renderContext.ConsoleDriver; for (var i = 0; i < textLines.Count; i++) { var currentPosition = position with {Y = position.Y + i}; @@ -234,6 +256,10 @@ public abstract partial class View : IView where TConcrete : Vi { text = text[..size.Width]; } + else if (text.Length < size.Width) + { + text = text.PadRight(size.Width); + } try { @@ -247,38 +273,17 @@ public abstract partial class View : IView where TConcrete : Vi } protected void RenderText( - string text, - IConsoleDriver driver, + in ReadOnlySpan text, + in RenderContext renderContext, Position position, Size size, - TextTransformer? textTransformer = null) + bool updateCellsOnly) { - for (var i = 0; i < size.Height; i++) - { - var currentPosition = position with {Y = position.Y + i}; - var finalText = text; + UpdateCells(renderContext.UpdatedCells, position, size.Width, size.Height); - if (textTransformer is not null) - { - finalText = textTransformer(finalText, currentPosition, size); - } + if (updateCellsOnly) return; - if (finalText.Length > size.Width) - { - finalText = finalText[..size.Width]; - } - - driver.SetCursorPosition(currentPosition); - driver.Write(finalText); - } - } - - protected void RenderText( - in ReadOnlySpan text, - IConsoleDriver driver, - Position position, - Size size) - { + var driver = renderContext.ConsoleDriver; for (var i = 0; i < size.Height; i++) { var currentPosition = position with {Y = position.Y + i}; @@ -296,10 +301,16 @@ public abstract partial class View : IView where TConcrete : Vi protected void RenderText( char content, - IConsoleDriver driver, + in RenderContext renderContext, Position position, - Size size) + Size size, + bool updateCellsOnly) { + UpdateCells(renderContext.UpdatedCells, position, size.Width, size.Height); + + if (updateCellsOnly) return; + + var driver = renderContext.ConsoleDriver; var contentString = new string(content, size.Width); for (var i = 0; i < size.Height; i++) @@ -323,6 +334,7 @@ public abstract partial class View : IView where TConcrete : Vi { t.ApplyFormat(driver, renderContext.TextFormat); } + if (foreground is not null) { driver.SetForegroundColor(foreground); diff --git a/src/Library/TerminalUI/Models/RenderContext.cs b/src/Library/TerminalUI/Models/RenderContext.cs index 0ab4965..882e8fb 100644 --- a/src/Library/TerminalUI/Models/RenderContext.cs +++ b/src/Library/TerminalUI/Models/RenderContext.cs @@ -16,6 +16,7 @@ public readonly ref struct RenderContext public IColor? Background { get; init; } public RenderStatistics Statistics { get; init; } public TextFormatContext TextFormat { get; init; } + public bool[,] UpdatedCells { get; init; } public RenderContext( IConsoleDriver consoleDriver, @@ -23,7 +24,8 @@ public readonly ref struct RenderContext IColor? foreground, IColor? background, RenderStatistics statistics, - TextFormatContext textFormat) + TextFormatContext textFormat, + bool[,] updatedCells) { RenderId = _renderId++; @@ -33,6 +35,7 @@ public readonly ref struct RenderContext Background = background; Statistics = statistics; TextFormat = textFormat; + UpdatedCells = updatedCells; } public static RenderContext Empty => @@ -42,6 +45,7 @@ public readonly ref struct RenderContext null, null, new RenderStatistics(), - new TextFormatContext(false) + new TextFormatContext(false), + new bool[0, 0] ); } \ No newline at end of file diff --git a/src/Library/TerminalUI/RenderEngine.cs b/src/Library/TerminalUI/RenderEngine.cs index 72855ce..c760b83 100644 --- a/src/Library/TerminalUI/RenderEngine.cs +++ b/src/Library/TerminalUI/RenderEngine.cs @@ -1,4 +1,5 @@ -using TerminalUI.Controls; +using TerminalUI.ConsoleDrivers; +using TerminalUI.Controls; using TerminalUI.Models; using TerminalUI.TextFormat; using TerminalUI.Traits; @@ -14,6 +15,9 @@ public class RenderEngine : IRenderEngine private readonly List _forcedTemporaryViewsToRender = new(); private bool _rerenderRequested = true; private bool _lastCursorVisible; + private bool[,]? _updatedCells; + private bool[,]? _filledCells; + private bool[,]? _lastFilledCells; public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop) { @@ -77,6 +81,18 @@ public class RenderEngine : IRenderEngine var initialPosition = new Position(0, 0); var size = driver.GetWindowSize(); + //TODO: this could be stack allocated when sizes are small + if (_updatedCells is null + || _updatedCells.GetLength(0) != size.Width + || _updatedCells.GetLength(1) != size.Height) + { + _updatedCells = new bool[size.Width, size.Height]; + } + else + { + ClearArray2D(_updatedCells); + } + RenderViews( forcedTemporaryViewsToRender, new RenderContext( @@ -85,7 +101,8 @@ public class RenderEngine : IRenderEngine null, null, new RenderStatistics(), - new TextFormatContext(driver.SupportsAnsiEscapeSequence) + new TextFormatContext(driver.SupportsAnsiEscapeSequence), + _updatedCells ), initialPosition, size); @@ -98,11 +115,41 @@ public class RenderEngine : IRenderEngine null, null, new RenderStatistics(), - new TextFormatContext(driver.SupportsAnsiEscapeSequence) + new TextFormatContext(driver.SupportsAnsiEscapeSequence), + _updatedCells ), initialPosition, size); + if (_lastFilledCells is not null + && _lastFilledCells.GetLength(0) == size.Width + && _lastFilledCells.GetLength(1) == size.Height) + { + Array2DHelper.CombineArray2Ds( + _updatedCells, + _lastFilledCells, + new Position(0, 0), + _updatedCells, + (a, b) => (a ?? false) || (b ?? false) + ); + } + + if (_filledCells is null + || _filledCells.GetLength(0) != size.Width + || _filledCells.GetLength(1) != size.Height) + { + _filledCells = new bool[size.Width, size.Height]; + } + else + { + ClearArray2D(_filledCells); + } + + driver.ResetStyle(); + Array2DHelper.RenderEmpty(driver, _updatedCells, _filledCells, _applicationContext.EmptyCharacter, initialPosition, size); + + (_lastFilledCells, _filledCells) = (_filledCells, _lastFilledCells); + if (_applicationContext.FocusManager.Focused is { } focused) { focused.SetCursorPosition(driver); @@ -129,6 +176,20 @@ public class RenderEngine : IRenderEngine } } + private void ClearArray2D(T[,] array, T defaultValue = default!) + { + var maxX = array.GetLength(0); + var maxY = array.GetLength(1); + + for (var x = 0; x < maxX; x++) + { + for (var y = 0; y < maxY; y++) + { + array[x, y] = defaultValue; + } + } + } + public void AddViewToPermanentRenderGroup(IView view) { lock (_lock)