Fill not render space (Render engine, Border)

This commit is contained in:
2023-08-17 15:55:00 +02:00
parent 92726f1af9
commit df4fe93c81
17 changed files with 373 additions and 146 deletions

View File

@@ -2,6 +2,7 @@
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.ConsoleUI.App.Styling; using FileTime.ConsoleUI.App.Styling;
using GeneralInputKey; using GeneralInputKey;
using TerminalUI.Color;
using TerminalUI.Controls; using TerminalUI.Controls;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models; using TerminalUI.Models;
@@ -59,6 +60,7 @@ public class CommandPalette
Margin = 5, Margin = 5,
Padding = 1, Padding = 1,
MaxWidth = 50, MaxWidth = 50,
Fill = SpecialColor.None,
Content = new Grid<IRootViewModel> Content = new Grid<IRootViewModel>
{ {
RowDefinitionsObject = "Auto *", RowDefinitionsObject = "Auto *",

View File

@@ -52,14 +52,16 @@ public class ProgressBarExamples
private void RenderProgressBar<T>(ProgressBar<T> progressBar, Position position) private void RenderProgressBar<T>(ProgressBar<T> progressBar, Position position)
{ {
var s = new Size(10, 1);
var renderContext = new RenderContext( var renderContext = new RenderContext(
_driver, _driver,
true, true,
null, null,
null, null,
new(), 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);
} }
} }

View File

@@ -0,0 +1,96 @@
using TerminalUI.ConsoleDrivers;
using TerminalUI.Models;
namespace TerminalUI;
public static class Array2DHelper
{
public static void CombineArray2Ds<T, TResult>(T[,] array1, T[,] array2, Position array2Delta, TResult[,] array3, Func<T?, T?, TResult> 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<char> 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;
}
}
}

View File

@@ -1,4 +1,5 @@
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.Traits; using TerminalUI.Traits;
@@ -16,6 +17,7 @@ public sealed partial class Border<T> : ContentView<Border<T>, T>, IDisplayView
[Notify] private char _topRightChar = '┐'; [Notify] private char _topRightChar = '┐';
[Notify] private char _bottomLeftChar = '└'; [Notify] private char _bottomLeftChar = '└';
[Notify] private char _bottomRightChar = '┘'; [Notify] private char _bottomRightChar = '┘';
[Notify] private IColor? _fill;
public Border() public Border()
{ {
@@ -25,6 +27,9 @@ public sealed partial class Border<T> : ContentView<Border<T>, T>, IDisplayView
RerenderProperties.Add(nameof(LeftChar)); RerenderProperties.Add(nameof(LeftChar));
RerenderProperties.Add(nameof(RightChar)); RerenderProperties.Add(nameof(RightChar));
RerenderProperties.Add(nameof(BottomChar)); RerenderProperties.Add(nameof(BottomChar));
RerenderProperties.Add(nameof(TopLeftChar));
RerenderProperties.Add(nameof(TopRightChar));
RerenderProperties.Add(nameof(Fill));
} }
protected override Size CalculateSize() protected override Size CalculateSize()
@@ -51,12 +56,19 @@ public sealed partial class Border<T> : ContentView<Border<T>, T>, IDisplayView
+ DataContext?.GetType().Name); + 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 childPosition = new Position(X: position.X + _borderThickness.Left, Y: position.Y + _borderThickness.Top);
var childSize = new Size( var childSize = new Size(
Width: size.Width - _borderThickness.Left - _borderThickness.Right, Width: size.Width - _borderThickness.Left - _borderThickness.Right,
Height: size.Height - _borderThickness.Top - _borderThickness.Bottom 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) 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); childPosition = new Position(X: childPosition.X + _padding.Left, Y: childPosition.Y + _padding.Top);
@@ -66,85 +78,117 @@ public sealed partial class Border<T> : ContentView<Border<T>, 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) if (contentRendered)
{ {
var driver = renderContext.ConsoleDriver; var driver = renderContext.ConsoleDriver;
driver.ResetStyle(); driver.ResetStyle();
SetColorsForDriver(renderContext); SetStyleColor(renderContext, foregroundColor, backgroundColor);
}
RenderTopBorder(renderContext, position, size); var updateCellsOnly = !contentRendered;
RenderBottomBorder(renderContext, position, size); RenderTopBorder(renderContext, position, size, updateCellsOnly);
RenderLeftBorder(renderContext, position, size); RenderBottomBorder(renderContext, position, size, updateCellsOnly);
RenderRightBorder(renderContext, position, size); RenderLeftBorder(renderContext, position, size, updateCellsOnly);
RenderRightBorder(renderContext, position, size, updateCellsOnly);
RenderTopLeftCorner(renderContext, position); RenderTopLeftCorner(renderContext, position, updateCellsOnly);
RenderTopRightCorner(renderContext, position, size); RenderTopRightCorner(renderContext, position, size, updateCellsOnly);
RenderBottomLeftCorner(renderContext, position, size); RenderBottomLeftCorner(renderContext, position, size, updateCellsOnly);
RenderBottomRightCorner(renderContext, position, size); 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; 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}; position = position with {X = position.X + _borderThickness.Left};
size = new Size(Width: size.Width - _borderThickness.Left - _borderThickness.Right, Height: _borderThickness.Top); 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); 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); 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}; position = position with {Y = position.Y + _borderThickness.Top};
size = new Size(Width: _borderThickness.Left, Height: size.Height - _borderThickness.Top - _borderThickness.Bottom); 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); 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); 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; if (_borderThickness.Left == 0 || _borderThickness.Top == 0) return;
var size = new Size(Width: _borderThickness.Left, Height: _borderThickness.Top); 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; if (_borderThickness.Right == 0 || _borderThickness.Top == 0) return;
position = position with {X = position.X + size.Width - _borderThickness.Right}; position = position with {X = position.X + size.Width - _borderThickness.Right};
size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Top); 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; if (_borderThickness.Left == 0 || _borderThickness.Bottom == 0) return;
position = position with {Y = position.Y + size.Height - _borderThickness.Bottom}; position = position with {Y = position.Y + size.Height - _borderThickness.Bottom};
size = new Size(Width: _borderThickness.Left, 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; if (_borderThickness.Right == 0 || _borderThickness.Bottom == 0) return;
@@ -153,6 +197,6 @@ public sealed partial class Border<T> : ContentView<Border<T>, T>, IDisplayView
Y: position.Y + size.Height - _borderThickness.Bottom Y: position.Y + size.Height - _borderThickness.Bottom
); );
size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Bottom); size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Bottom);
RenderText(_bottomRightChar, renderContext.ConsoleDriver, position, size); RenderText(_bottomRightChar, renderContext, position, size, updateCellsOnly);
} }
} }

View File

@@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public abstract class ChildContainerView<TConcrete, T> public abstract class ChildCollectionView<TConcrete, T>
: View<TConcrete, T>, IChildContainer<T> : View<TConcrete, T>, IChildContainer<T>
where TConcrete : View<TConcrete, T> where TConcrete : View<TConcrete, T>
{ {
@@ -12,7 +12,7 @@ public abstract class ChildContainerView<TConcrete, T>
public ReadOnlyObservableCollection<IView> Children { get; } public ReadOnlyObservableCollection<IView> Children { get; }
public ChildInitializer<T> ChildInitializer { get; } public ChildInitializer<T> ChildInitializer { get; }
protected ChildContainerView() protected ChildCollectionView()
{ {
ChildInitializer = new ChildInitializer<T>(this); ChildInitializer = new ChildInitializer<T>(this);
Children = new ReadOnlyObservableCollection<IView>(_children); Children = new ReadOnlyObservableCollection<IView>(_children);

View File

@@ -48,7 +48,7 @@ public abstract partial class ContentView<TConcrete, T>
{ {
if (_placeholderRenderDone) return false; if (_placeholderRenderDone) return false;
_placeholderRenderDone = true; _placeholderRenderDone = true;
RenderEmpty(renderContext, position, size); RenderEmpty(renderContext, position, size, false);
return true; return true;
} }

View File

@@ -7,7 +7,7 @@ using TerminalUI.ViewExtensions;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeHandler public sealed class Grid<T> : ChildCollectionView<Grid<T>, T>, IVisibilityChangeHandler
{ {
private readonly List<IView> _forceRerenderChildren = new(); private readonly List<IView> _forceRerenderChildren = new();
private readonly object _forceRerenderChildrenLock = new(); private readonly object _forceRerenderChildrenLock = new();
@@ -202,7 +202,8 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
} }
} }
private bool RenderViewsByPosition(RenderContext context, private bool RenderViewsByPosition(
in RenderContext context,
Position gridPosition, Position gridPosition,
Size gridSize, Size gridSize,
ReadOnlySpan<int> columnWidths, ReadOnlySpan<int> columnWidths,
@@ -238,39 +239,26 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
if (!viewsByPosition.TryGetValue((column, row), out var children)) if (!viewsByPosition.TryGetValue((column, row), out var children))
{ {
RenderEmpty(context, renderPosition, renderSize); RenderEmpty(context, renderPosition, renderSize, false);
return true; return true;
} }
var needsRerender = children.Any(forceRerenderChildren.Contains); var needsRerender = children.Any(forceRerenderChildren.Contains);
var updatedContext = context;
if (needsRerender) if (needsRerender)
{ {
context = new RenderContext( updatedContext = context with {ForceRerender = true};
context.ConsoleDriver, RenderEmpty(updatedContext, renderPosition, renderSize, false);
true,
context.Foreground,
context.Background,
context.Statistics,
context.TextFormat
);
RenderEmpty(context, renderPosition, renderSize);
} }
//This implies that children further back in the list will be rendered on top of children placed before in the list. //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) if (rendered && !needsRerender)
{ {
needsRerender = true; needsRerender = true;
context = new RenderContext( updatedContext = context with {ForceRerender = true};
context.ConsoleDriver,
true,
context.Foreground,
context.Background,
context.Statistics,
context.TextFormat
);
} }
} }

View File

@@ -162,6 +162,8 @@ public sealed partial class ItemsControl<TDataContext, TItem>
? childSize.Height ? childSize.Height
: childSize.Width; : childSize.Width;
} }
// TODO: clean non used space
return neededRerender; return neededRerender;
} }

View File

@@ -232,6 +232,8 @@ public sealed partial class ListView<TDataContext, TItem> : View<ListView<TDataC
item.Render(renderContext, position with {X = position.X + deltaX}, size with {Width = width}); item.Render(renderContext, position with {X = position.X + deltaX}, size with {Width = width});
deltaX = nextDeltaX; deltaX = nextDeltaX;
} }
//TODO: render empty to remaining space
return true; return true;
} }
@@ -277,23 +279,20 @@ public sealed partial class ListView<TDataContext, TItem> : View<ListView<TDataC
if (lastItemIndex > listViewItems.Length) if (lastItemIndex > listViewItems.Length)
lastItemIndex = listViewItems.Length; lastItemIndex = listViewItems.Length;
var anyRendered = false;
for (var i = renderStartIndex; i < lastItemIndex; i++) for (var i = renderStartIndex; i < lastItemIndex; i++)
{ {
var item = listViewItems[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; deltaY += requestedItemSize.Height;
} }
var driver = ApplicationContext!.ConsoleDriver; // TODO: this should only render if deltaY is changed compared to last render or if last render was a horizontal
var placeholder = new string(' ', size.Width); RenderEmpty(renderContext, position with {Y = position.Y + deltaY}, size with {Height = size.Height - deltaY}, false);
driver.ResetStyle();
for (var i = deltaY; i < size.Height; i++)
{
driver.SetCursorPosition(position with {Y = position.Y + i});
driver.Write(placeholder);
}
return true; return anyRendered;
} }
private ReadOnlySpan<ListViewItem<TItem, TDataContext>> InstantiateItemViews() private ReadOnlySpan<ListViewItem<TItem, TDataContext>> InstantiateItemViews()

View File

@@ -67,7 +67,7 @@ public partial class ProgressBar<T> : View<ProgressBar<T>, T>
unfilledForeground, unfilledForeground,
unfilledBackground); unfilledBackground);
if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false; var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderState);
var utf8Support = ApplicationContext!.SupportUtf8Output; var utf8Support = ApplicationContext!.SupportUtf8Output;
var unfilledCharacter = unfilledCharacterS.GetChar(utf8Support); var unfilledCharacter = unfilledCharacterS.GetChar(utf8Support);
@@ -117,16 +117,17 @@ public partial class ProgressBar<T> : View<ProgressBar<T>, T>
var textStartPosition = position; var textStartPosition = position;
if (leftCap.HasValue) 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}; textStartPosition = textStartPosition with {X = textStartPosition.X + 1};
} }
// Filled // Filled
RenderText( RenderText(
filledText, filledText,
driver, renderContext,
textStartPosition, textStartPosition,
size with {Width = progressQuotientWidth} size with {Width = progressQuotientWidth},
skipRender
); );
// Transient character // Transient character
@@ -135,9 +136,10 @@ public partial class ProgressBar<T> : View<ProgressBar<T>, T>
SetStyleColor(renderContext, foreground, unfilledBackground); SetStyleColor(renderContext, foreground, unfilledBackground);
RenderText( RenderText(
transientChar, transientChar,
driver, renderContext,
textStartPosition with {X = textStartPosition.X + progressQuotientWidth}, textStartPosition with {X = textStartPosition.X + progressQuotientWidth},
size with {Width = 1} size with {Width = 1},
skipRender
); );
} }
@@ -150,9 +152,10 @@ public partial class ProgressBar<T> : View<ProgressBar<T>, T>
SetStyleColor(renderContext, unfilledForeground, unfilledBackground); SetStyleColor(renderContext, unfilledForeground, unfilledBackground);
RenderText( RenderText(
unfilledText, unfilledText,
driver, renderContext,
textStartPosition with {X = textStartPosition.X + progressQuotientWidth + 1}, textStartPosition with {X = textStartPosition.X + progressQuotientWidth + 1},
size with {Width = progressRemainderWidth} size with {Width = progressRemainderWidth},
skipRender
); );
} }
@@ -162,12 +165,13 @@ public partial class ProgressBar<T> : View<ProgressBar<T>, T>
SetStyleColor(renderContext, foreground, background); SetStyleColor(renderContext, foreground, background);
RenderText( RenderText(
rightCap.Value, rightCap.Value,
driver, renderContext,
position with {X = position.X + size.Width - 1}, 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) private bool NeedsRerender(RenderState renderState)

View File

@@ -13,32 +13,40 @@ public sealed partial class Rectangle<T> : View<Rectangle<T>, T>, IDisplayView
IColor? Color); IColor? Color);
private RenderState? _lastRenderState; private RenderState? _lastRenderState;
[Notify] private IColor? _fill;
public Rectangle()
{
RerenderProperties.Add(nameof(Fill));
}
protected override Size CalculateSize() => new(Width ?? 0, Height ?? 0); protected override Size CalculateSize() => new(Width ?? 0, Height ?? 0);
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size) protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
{ {
var color = Background ?? renderContext.Background; var fillColor = Fill ?? Background ?? renderContext.Background;
var renderState = new RenderState(position, size, color); var renderState = new RenderState(position, size, fillColor);
if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false; var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderState);
_lastRenderState = renderState; _lastRenderState = renderState;
var driver = renderContext.ConsoleDriver; var driver = renderContext.ConsoleDriver;
var s = new string(' ', size.Width);
driver.ResetStyle(); driver.ResetStyle();
if (color is not null) if (fillColor is not null)
{ {
driver.SetBackgroundColor(color); driver.SetBackgroundColor(fillColor);
} }
var height = size.Height; RenderEmpty(
for (var i = 0; i < height; i++) renderContext,
{ position,
driver.SetCursorPosition(position with {Y = position.Y + i}); size,
driver.Write(s); skipRender,
} false
);
return true; return !skipRender;
} }
private bool NeedsRerender(RenderState renderState) private bool NeedsRerender(RenderState renderState)

View File

@@ -4,7 +4,7 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public sealed partial class StackPanel<T> : ChildContainerView<StackPanel<T>, T>, IVisibilityChangeHandler public sealed partial class StackPanel<T> : ChildCollectionView<StackPanel<T>, T>, IVisibilityChangeHandler
{ {
private readonly List<IView> _forceRerenderChildren = new(); private readonly List<IView> _forceRerenderChildren = new();
private readonly object _forceRerenderChildrenLock = new(); private readonly object _forceRerenderChildrenLock = new();
@@ -39,7 +39,10 @@ public sealed partial class StackPanel<T> : ChildContainerView<StackPanel<T>, T>
return new Size(width, height); 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; var neededRerender = false;
IReadOnlyList<IView> forceRerenderChildren; IReadOnlyList<IView> forceRerenderChildren;
@@ -102,9 +105,10 @@ public sealed partial class StackPanel<T> : ChildContainerView<StackPanel<T>, T>
SetColorsForDriver(renderContext); SetColorsForDriver(renderContext);
RenderText( RenderText(
text, text,
renderContext.ConsoleDriver, renderContext,
position with {X = position.X + delta}, position with {X = position.X + delta},
size with {Width = leftWidth} size with {Width = leftWidth},
!neededRerender
); );
} }
else else
@@ -116,9 +120,10 @@ public sealed partial class StackPanel<T> : ChildContainerView<StackPanel<T>, T>
SetColorsForDriver(renderContext); SetColorsForDriver(renderContext);
RenderText( RenderText(
text, text,
renderContext.ConsoleDriver, renderContext,
position with {Y = position.Y + delta}, position with {Y = position.Y + delta},
size with {Height = leftHeight} size with {Height = leftHeight},
!neededRerender
); );
} }

View File

@@ -58,7 +58,7 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
background, background,
_textFormat); _textFormat);
if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false; var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderState);
_lastRenderState = renderState; _lastRenderState = renderState;
@@ -67,7 +67,7 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
if (_placeholderRenderDone) if (_placeholderRenderDone)
{ {
_placeholderRenderDone = true; _placeholderRenderDone = true;
RenderEmpty(renderContext, position, size); RenderEmpty(renderContext, position, size, skipRender);
} }
return false; return false;
@@ -75,12 +75,11 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
_placeholderRenderDone = false; _placeholderRenderDone = false;
var driver = renderContext.ConsoleDriver;
SetStyleColor(renderContext, foreground, background, _textFormat); 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) private string TransformText(string text, Position position, Size size)

View File

@@ -100,38 +100,39 @@ public sealed partial class TextBox<T> : View<TextBox<T>, T>, IFocusable, IDispl
foreground, foreground,
background); background);
if (!renderContext.ForceRerender && !NeedsRerender(renderStatus)) return false; var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderStatus);
_lastRenderState = renderStatus; _lastRenderState = renderStatus;
var driver = renderContext.ConsoleDriver; var driver = renderContext.ConsoleDriver;
SetStyleColor(renderContext, foreground, background); SetStyleColor(renderContext, foreground, background);
RenderEmpty(renderContext, position, size); RenderEmpty(renderContext, position, size, skipRender);
if (PasswordChar is { } passwordChar && !char.IsControl(passwordChar)) if (PasswordChar is { } passwordChar && !char.IsControl(passwordChar))
{ {
for (var i = 0; i < _textLines.Count; i++) for (var i = 0; i < _textLines.Count; i++)
{ {
var pos = position with {Y = position.Y + 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 else
{ {
RenderText(_textLines, driver, position, size); RenderText(_textLines, renderContext, position, size, skipRender);
} }
_cursorPosition = position + _relativeCursorPosition; _cursorPosition = position + _relativeCursorPosition;
return true; return !skipRender;
} }
private void RenderPasswordTextLine( private void RenderPasswordTextLine(
string sourceText, string sourceText,
char passwordChar, char passwordChar,
IConsoleDriver driver, in RenderContext renderContext,
Position position, Position position,
Size size) Size size,
bool updateCellsOnly)
{ {
Span<char> text = stackalloc char[sourceText.Length]; Span<char> text = stackalloc char[sourceText.Length];
for (var j = 0; j < text.Length; j++) for (var j = 0; j < text.Length; j++)
@@ -139,7 +140,7 @@ public sealed partial class TextBox<T> : View<TextBox<T>, T>, IFocusable, IDispl
text[j] = passwordChar; text[j] = passwordChar;
} }
RenderText(text, driver, position, size); RenderText(text, renderContext, position, size, updateCellsOnly);
} }
private bool NeedsRerender(RenderState renderState) private bool NeedsRerender(RenderState renderState)

View File

@@ -6,7 +6,6 @@ using System.Runtime.CompilerServices;
using GeneralInputKey; using GeneralInputKey;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.TextFormat; using TerminalUI.TextFormat;
using TerminalUI.Traits; using TerminalUI.Traits;
@@ -200,12 +199,18 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
return renderResult; 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; var driver = renderContext.ConsoleDriver;
driver.ResetStyle(); if (resetStyle)
{
driver.ResetStyle();
}
var placeHolder = new string(ApplicationContext!.EmptyCharacter, size.Width); Span<char> placeHolder = stackalloc char[size.Width];
placeHolder.Fill(ApplicationContext!.EmptyCharacter);
for (var i = 0; i < size.Height; i++) for (var i = 0; i < size.Height; i++)
{ {
driver.SetCursorPosition(position with {Y = position.Y + i}); driver.SetCursorPosition(position with {Y = position.Y + i});
@@ -213,13 +218,30 @@ public abstract partial class View<TConcrete, T> : IView<T> 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( protected void RenderText(
IList<string> textLines, IList<string> textLines,
IConsoleDriver driver, in RenderContext renderContext,
Position position, Position position,
Size size, Size size,
bool updateCellsOnly,
TextTransformer? textTransformer = null) 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++) for (var i = 0; i < textLines.Count; i++)
{ {
var currentPosition = position with {Y = position.Y + i}; var currentPosition = position with {Y = position.Y + i};
@@ -234,6 +256,10 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
{ {
text = text[..size.Width]; text = text[..size.Width];
} }
else if (text.Length < size.Width)
{
text = text.PadRight(size.Width);
}
try try
{ {
@@ -247,38 +273,17 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
} }
protected void RenderText( protected void RenderText(
string text, in ReadOnlySpan<char> text,
IConsoleDriver driver, in RenderContext renderContext,
Position position, Position position,
Size size, Size size,
TextTransformer? textTransformer = null) bool updateCellsOnly)
{ {
for (var i = 0; i < size.Height; i++) UpdateCells(renderContext.UpdatedCells, position, size.Width, size.Height);
{
var currentPosition = position with {Y = position.Y + i};
var finalText = text;
if (textTransformer is not null) if (updateCellsOnly) return;
{
finalText = textTransformer(finalText, currentPosition, size);
}
if (finalText.Length > size.Width) var driver = renderContext.ConsoleDriver;
{
finalText = finalText[..size.Width];
}
driver.SetCursorPosition(currentPosition);
driver.Write(finalText);
}
}
protected void RenderText(
in ReadOnlySpan<char> text,
IConsoleDriver driver,
Position position,
Size size)
{
for (var i = 0; i < size.Height; i++) for (var i = 0; i < size.Height; i++)
{ {
var currentPosition = position with {Y = position.Y + i}; var currentPosition = position with {Y = position.Y + i};
@@ -296,10 +301,16 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
protected void RenderText( protected void RenderText(
char content, char content,
IConsoleDriver driver, in RenderContext renderContext,
Position position, 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); var contentString = new string(content, size.Width);
for (var i = 0; i < size.Height; i++) for (var i = 0; i < size.Height; i++)
@@ -323,6 +334,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
{ {
t.ApplyFormat(driver, renderContext.TextFormat); t.ApplyFormat(driver, renderContext.TextFormat);
} }
if (foreground is not null) if (foreground is not null)
{ {
driver.SetForegroundColor(foreground); driver.SetForegroundColor(foreground);

View File

@@ -16,6 +16,7 @@ public readonly ref struct RenderContext
public IColor? Background { get; init; } public IColor? Background { get; init; }
public RenderStatistics Statistics { get; init; } public RenderStatistics Statistics { get; init; }
public TextFormatContext TextFormat { get; init; } public TextFormatContext TextFormat { get; init; }
public bool[,] UpdatedCells { get; init; }
public RenderContext( public RenderContext(
IConsoleDriver consoleDriver, IConsoleDriver consoleDriver,
@@ -23,7 +24,8 @@ public readonly ref struct RenderContext
IColor? foreground, IColor? foreground,
IColor? background, IColor? background,
RenderStatistics statistics, RenderStatistics statistics,
TextFormatContext textFormat) TextFormatContext textFormat,
bool[,] updatedCells)
{ {
RenderId = _renderId++; RenderId = _renderId++;
@@ -33,6 +35,7 @@ public readonly ref struct RenderContext
Background = background; Background = background;
Statistics = statistics; Statistics = statistics;
TextFormat = textFormat; TextFormat = textFormat;
UpdatedCells = updatedCells;
} }
public static RenderContext Empty => public static RenderContext Empty =>
@@ -42,6 +45,7 @@ public readonly ref struct RenderContext
null, null,
null, null,
new RenderStatistics(), new RenderStatistics(),
new TextFormatContext(false) new TextFormatContext(false),
new bool[0, 0]
); );
} }

View File

@@ -1,4 +1,5 @@
using TerminalUI.Controls; using TerminalUI.ConsoleDrivers;
using TerminalUI.Controls;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.TextFormat; using TerminalUI.TextFormat;
using TerminalUI.Traits; using TerminalUI.Traits;
@@ -14,6 +15,9 @@ public class RenderEngine : IRenderEngine
private readonly List<IView> _forcedTemporaryViewsToRender = new(); private readonly List<IView> _forcedTemporaryViewsToRender = new();
private bool _rerenderRequested = true; private bool _rerenderRequested = true;
private bool _lastCursorVisible; private bool _lastCursorVisible;
private bool[,]? _updatedCells;
private bool[,]? _filledCells;
private bool[,]? _lastFilledCells;
public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop) public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop)
{ {
@@ -77,6 +81,18 @@ public class RenderEngine : IRenderEngine
var initialPosition = new Position(0, 0); var initialPosition = new Position(0, 0);
var size = driver.GetWindowSize(); 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( RenderViews(
forcedTemporaryViewsToRender, forcedTemporaryViewsToRender,
new RenderContext( new RenderContext(
@@ -85,7 +101,8 @@ public class RenderEngine : IRenderEngine
null, null,
null, null,
new RenderStatistics(), new RenderStatistics(),
new TextFormatContext(driver.SupportsAnsiEscapeSequence) new TextFormatContext(driver.SupportsAnsiEscapeSequence),
_updatedCells
), ),
initialPosition, initialPosition,
size); size);
@@ -98,11 +115,41 @@ public class RenderEngine : IRenderEngine
null, null,
null, null,
new RenderStatistics(), new RenderStatistics(),
new TextFormatContext(driver.SupportsAnsiEscapeSequence) new TextFormatContext(driver.SupportsAnsiEscapeSequence),
_updatedCells
), ),
initialPosition, initialPosition,
size); 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) if (_applicationContext.FocusManager.Focused is { } focused)
{ {
focused.SetCursorPosition(driver); focused.SetCursorPosition(driver);
@@ -129,6 +176,20 @@ public class RenderEngine : IRenderEngine
} }
} }
private void ClearArray2D<T>(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) public void AddViewToPermanentRenderGroup(IView view)
{ {
lock (_lock) lock (_lock)