Files
FileTime2/src/Library/TerminalUI/Controls/Border.cs

243 lines
10 KiB
C#

using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.Models;
using TerminalUI.Traits;
namespace TerminalUI.Controls;
public sealed partial class Border<T> : ContentView<Border<T>, T>, IDisplayView
{
private record struct RenderState(
Position Position,
Size Size,
Thickness BorderThickness,
Thickness Padding,
char TopChar,
char LeftChar,
char RightChar,
char BottomChar,
char TopLeftChar,
char TopRightChar,
char BottomLeftChar,
char BottomRightChar,
IColor? Fill
);
private RenderState _lastRenderState;
[Notify] private Thickness _borderThickness = 1;
[Notify] private Thickness _padding = 0;
[Notify] private char _topChar = '─';
[Notify] private char _leftChar = '│';
[Notify] private char _rightChar = '│';
[Notify] private char _bottomChar = '─';
[Notify] private char _topLeftChar = '┌';
[Notify] private char _topRightChar = '┐';
[Notify] private char _bottomLeftChar = '└';
[Notify] private char _bottomRightChar = '┘';
[Notify] private IColor? _fill;
public Border()
{
RerenderProperties.Add(nameof(BorderThickness));
RerenderProperties.Add(nameof(Padding));
RerenderProperties.Add(nameof(TopChar));
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()
{
var size = new Size(
_borderThickness.Left + _borderThickness.Right + _padding.Left + _padding.Right,
_borderThickness.Top + _borderThickness.Bottom + _padding.Top + _padding.Bottom
);
if (Content is null || !Content.IsVisible) return size;
var contentSize = Content.GetRequestedSize();
return new Size(contentSize.Width + size.Width, contentSize.Height + size.Height);
}
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
{
if (ContentRendererMethod is null)
{
throw new NullReferenceException(
nameof(ContentRendererMethod)
+ " is null, cannot render content of "
+ Content?.GetType().Name
+ " with DataContext of "
+ DataContext?.GetType().Name);
}
var backgroundColor = Background ?? renderContext.Background;
var foregroundColor = Foreground ?? renderContext.Foreground;
var fillColor = Fill ?? Background ?? renderContext.Background;
var renderState = new RenderState(
position,
size,
_borderThickness,
_padding,
_topChar,
_leftChar,
_rightChar,
_bottomChar,
_topLeftChar,
_topRightChar,
_bottomLeftChar,
_bottomRightChar,
fillColor
);
var skipBorderRender = !renderContext.ForceRerender && !NeedsRerender(renderState);
if (!skipBorderRender)
{
_lastRenderState = renderState;
}
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);
childSize = new Size(
Width: childSize.Width - _padding.Left - _padding.Right,
Height: childSize.Height - _padding.Top - _padding.Bottom
);
}
// 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();
SetStyleColor(renderContext, foregroundColor, backgroundColor);
}
var updateCellsOnly = !contentRendered || skipBorderRender;
RenderTopBorder(renderContext, position, size, updateCellsOnly);
RenderBottomBorder(renderContext, position, size, updateCellsOnly);
RenderLeftBorder(renderContext, position, size, updateCellsOnly);
RenderRightBorder(renderContext, position, size, updateCellsOnly);
RenderTopLeftCorner(renderContext, position, updateCellsOnly);
RenderTopRightCorner(renderContext, position, size, updateCellsOnly);
RenderBottomLeftCorner(renderContext, position, size, updateCellsOnly);
RenderBottomRightCorner(renderContext, position, size, updateCellsOnly);
if (fillColor != null)
{
SetStyleColor(renderContext, foregroundColor, fillColor);
// Use the same array that children use. Also use that area, so we work 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 bool NeedsRerender(RenderState renderState) => renderState != _lastRenderState;
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, position, size, updateCellsOnly);
}
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, position, size, updateCellsOnly);
}
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, position, size, updateCellsOnly);
}
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, position, size, updateCellsOnly);
}
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, position, size, updateCellsOnly);
}
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, position, size, updateCellsOnly);
}
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, position, size, updateCellsOnly);
}
private void RenderBottomRightCorner(in RenderContext renderContext, Position position, Size size, bool updateCellsOnly)
{
if (_borderThickness.Right == 0 || _borderThickness.Bottom == 0) return;
position = new Position(
X: position.X + size.Width - _borderThickness.Right,
Y: position.Y + size.Height - _borderThickness.Bottom
);
size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Bottom);
RenderText(_bottomRightChar, renderContext, position, size, updateCellsOnly);
}
}