243 lines
10 KiB
C#
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);
|
|
}
|
|
} |