Fill not render space (Render engine, Border)
This commit is contained in:
@@ -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<IRootViewModel>
|
||||
{
|
||||
RowDefinitionsObject = "Auto *",
|
||||
|
||||
@@ -52,14 +52,16 @@ public class ProgressBarExamples
|
||||
|
||||
private void RenderProgressBar<T>(ProgressBar<T> 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);
|
||||
}
|
||||
}
|
||||
96
src/Library/TerminalUI/Array2DHelper.cs
Normal file
96
src/Library/TerminalUI/Array2DHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T> : ContentView<Border<T>, 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<T> : ContentView<Border<T>, 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<T> : ContentView<Border<T>, 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<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)
|
||||
{
|
||||
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<T> : ContentView<Border<T>, 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);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public abstract class ChildContainerView<TConcrete, T>
|
||||
public abstract class ChildCollectionView<TConcrete, T>
|
||||
: View<TConcrete, T>, IChildContainer<T>
|
||||
where TConcrete : View<TConcrete, T>
|
||||
{
|
||||
@@ -12,7 +12,7 @@ public abstract class ChildContainerView<TConcrete, T>
|
||||
public ReadOnlyObservableCollection<IView> Children { get; }
|
||||
public ChildInitializer<T> ChildInitializer { get; }
|
||||
|
||||
protected ChildContainerView()
|
||||
protected ChildCollectionView()
|
||||
{
|
||||
ChildInitializer = new ChildInitializer<T>(this);
|
||||
Children = new ReadOnlyObservableCollection<IView>(_children);
|
||||
@@ -48,7 +48,7 @@ public abstract partial class ContentView<TConcrete, T>
|
||||
{
|
||||
if (_placeholderRenderDone) return false;
|
||||
_placeholderRenderDone = true;
|
||||
RenderEmpty(renderContext, position, size);
|
||||
RenderEmpty(renderContext, position, size, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using TerminalUI.ViewExtensions;
|
||||
|
||||
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 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,
|
||||
Size gridSize,
|
||||
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))
|
||||
{
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,8 @@ public sealed partial class ItemsControl<TDataContext, TItem>
|
||||
: childSize.Width;
|
||||
}
|
||||
|
||||
// TODO: clean non used space
|
||||
|
||||
return neededRerender;
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +233,8 @@ public sealed partial class ListView<TDataContext, TItem> : View<ListView<TDataC
|
||||
deltaX = nextDeltaX;
|
||||
}
|
||||
|
||||
//TODO: render empty to remaining space
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -277,23 +279,20 @@ public sealed partial class ListView<TDataContext, TItem> : View<ListView<TDataC
|
||||
if (lastItemIndex > 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<ListViewItem<TItem, TDataContext>> InstantiateItemViews()
|
||||
|
||||
@@ -67,7 +67,7 @@ public partial class ProgressBar<T> : View<ProgressBar<T>, 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<T> : View<ProgressBar<T>, 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<T> : View<ProgressBar<T>, 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<T> : View<ProgressBar<T>, 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<T> : View<ProgressBar<T>, 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)
|
||||
|
||||
@@ -13,32 +13,40 @@ public sealed partial class Rectangle<T> : View<Rectangle<T>, 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)
|
||||
|
||||
@@ -4,7 +4,7 @@ using TerminalUI.Traits;
|
||||
|
||||
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 object _forceRerenderChildrenLock = new();
|
||||
@@ -39,7 +39,10 @@ public sealed partial class StackPanel<T> : ChildContainerView<StackPanel<T>, 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<IView> forceRerenderChildren;
|
||||
@@ -102,9 +105,10 @@ public sealed partial class StackPanel<T> : ChildContainerView<StackPanel<T>, 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<T> : ChildContainerView<StackPanel<T>, T>
|
||||
SetColorsForDriver(renderContext);
|
||||
RenderText(
|
||||
text,
|
||||
renderContext.ConsoleDriver,
|
||||
renderContext,
|
||||
position with {Y = position.Y + delta},
|
||||
size with {Height = leftHeight}
|
||||
size with {Height = leftHeight},
|
||||
!neededRerender
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, 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<T> : View<TextBlock<T>, 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<T> : View<TextBlock<T>, 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)
|
||||
|
||||
@@ -100,38 +100,39 @@ public sealed partial class TextBox<T> : View<TextBox<T>, 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<char> text = stackalloc char[sourceText.Length];
|
||||
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;
|
||||
}
|
||||
|
||||
RenderText(text, driver, position, size);
|
||||
RenderText(text, renderContext, position, size, updateCellsOnly);
|
||||
}
|
||||
|
||||
private bool NeedsRerender(RenderState renderState)
|
||||
|
||||
@@ -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<TConcrete, T> : IView<T> 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<char> 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<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(
|
||||
IList<string> 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<TConcrete, T> : IView<T> 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<TConcrete, T> : IView<T> where TConcrete : Vi
|
||||
}
|
||||
|
||||
protected void RenderText(
|
||||
string text,
|
||||
IConsoleDriver driver,
|
||||
in ReadOnlySpan<char> 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<char> 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<TConcrete, T> : IView<T> 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<TConcrete, T> : IView<T> where TConcrete : Vi
|
||||
{
|
||||
t.ApplyFormat(driver, renderContext.TextFormat);
|
||||
}
|
||||
|
||||
if (foreground is not null)
|
||||
{
|
||||
driver.SetForegroundColor(foreground);
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
@@ -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<IView> _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>(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)
|
||||
|
||||
Reference in New Issue
Block a user