using PropertyChanged.SourceGenerator; using TerminalUI.Color; using TerminalUI.Models; using TerminalUI.Traits; namespace TerminalUI.Controls; public sealed partial class Border : ContentView, 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); } }