using System.Buffers; using System.ComponentModel; using System.Runtime.CompilerServices; using PropertyChanged.SourceGenerator; using TerminalUI.Models; namespace TerminalUI.Controls; public abstract partial class View : IView { private readonly List _disposables = new(); [Notify] private T? _dataContext; [Notify] private int? _minWidth; [Notify] private int? _maxWidth; [Notify] private int? _width; [Notify] private int _actualWidth; [Notify] private int? _minHeight; [Notify] private int? _maxHeight; [Notify] private int? _height; [Notify] private int _actualHeight; [Notify] private Margin _margin = new Margin(0, 0, 0, 0); [Notify] private string? _name; [Notify] private IApplicationContext? _applicationContext; private bool _attached; public bool Attached { get => _attached; set { if (_attached == value) return; _attached = value; if (value) { AttachChildren(); } } } public List Extensions { get; } = new(); public RenderMethod RenderMethod { get; set; } public event Action? Disposed; protected List RerenderProperties { get; } = new(); protected View() { RenderMethod = DefaultRenderer; RerenderProperties.Add(nameof(MinWidth)); RerenderProperties.Add(nameof(MaxWidth)); RerenderProperties.Add(nameof(MinHeight)); RerenderProperties.Add(nameof(MaxHeight)); RerenderProperties.Add(nameof(Margin)); ((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged; } public virtual Size GetRequestedSize() { var size = CalculateSize(); if (MinWidth.HasValue && size.Width < MinWidth.Value) size = size with {Width = MinWidth.Value}; else if (MaxWidth.HasValue && size.Width > MaxWidth.Value) size = size with {Width = MaxWidth.Value}; if (MinHeight.HasValue && size.Height < MinHeight.Value) size = size with {Height = MinHeight.Value}; else if (MaxHeight.HasValue && size.Height > MaxHeight.Value) size = size with {Height = MaxHeight.Value}; if (Margin.Left != 0 || Margin.Right != 0) size = size with {Width = size.Width + Margin.Left + Margin.Right}; if (Margin.Top != 0 || Margin.Bottom != 0) size = size with {Height = size.Height + Margin.Top + Margin.Bottom}; return size; } protected abstract Size CalculateSize(); protected virtual void AttachChildren() { } private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (Attached && e.PropertyName is not null && (e.PropertyName == nameof(IView.DataContext) || RerenderProperties.Contains(e.PropertyName) ) ) { ApplicationContext?.EventLoop.RequestRerender(); } } protected abstract bool DefaultRenderer(RenderContext renderContext, Position position, Size size); public bool Render(RenderContext renderContext, Position position, Size size) { if (!Attached) throw new InvalidOperationException("Cannot render unattached view"); ActualWidth = size.Width; ActualHeight = size.Height; if (RenderMethod is null) { throw new NullReferenceException( nameof(RenderMethod) + " is null, cannot render content of " + GetType().Name + " with DataContext of " + DataContext?.GetType().Name); } if (Margin.Left != 0 || Margin.Top != 0 || Margin.Right != 0 || Margin.Bottom != 0) { position = new Position( X: position.X + Margin.Left, Y: position.Y + Margin.Top ); size = new Size( size.Width - Margin.Left - Margin.Right, size.Height - Margin.Top - Margin.Bottom ); } return RenderMethod(renderContext, position, size); } protected void RenderEmpty(RenderContext renderContext, Position position, Size size) { var driver = renderContext.ConsoleDriver; var placeHolder = new string(ApplicationContext!.EmptyCharacter, size.Width); for (var i = 0; i < size.Height; i++) { driver.SetCursorPosition(position with {Y = position.Y + i}); driver.Write(placeHolder); } } public TChild CreateChild() where TChild : IView, new() { var child = new TChild(); return AddChild(child); } public TChild CreateChild(Func dataContextMapper) where TChild : IView, new() { var child = new TChild(); return AddChild(child, dataContextMapper); } public virtual TChild AddChild(TChild child) where TChild : IView { child.DataContext = DataContext; CopyCommonPropertiesToNewChild(child); var mapper = new DataContextMapper(this, child, d => d); AddDisposable(mapper); child.AddDisposable(mapper); return child; } public virtual TChild AddChild(TChild child, Func dataContextMapper) where TChild : IView { child.DataContext = dataContextMapper(DataContext); CopyCommonPropertiesToNewChild(child); var mapper = new DataContextMapper(this, child, dataContextMapper); AddDisposable(mapper); child.AddDisposable(mapper); return child; } private void CopyCommonPropertiesToNewChild(IView child) { child.ApplicationContext = ApplicationContext; child.Attached = Attached; } public virtual void RemoveChild(IView child) { var mappers = _disposables .Where(d => d is DataContextMapper mapper && mapper.Target == child) .ToList(); foreach (var mapper in mappers) { mapper.Dispose(); RemoveDisposable(mapper); } child.Attached = false; } public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable); public void RemoveDisposable(IDisposable disposable) => _disposables.Remove(disposable); public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { var arrayPool = ArrayPool.Shared; var disposablesCount = _disposables.Count; var disposables = arrayPool.Rent(disposablesCount); _disposables.CopyTo(disposables); for (var i = 0; i < disposablesCount; i++) { disposables[i].Dispose(); } arrayPool.Return(disposables, true); _disposables.Clear(); Disposed?.Invoke(this); } } }