240 lines
7.3 KiB
C#
240 lines
7.3 KiB
C#
using System.Buffers;
|
|
using System.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
using PropertyChanged.SourceGenerator;
|
|
using TerminalUI.Models;
|
|
|
|
namespace TerminalUI.Controls;
|
|
|
|
public abstract partial class View<T> : IView<T>
|
|
{
|
|
private readonly List<IDisposable> _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<object> Extensions { get; } = new();
|
|
public RenderMethod RenderMethod { get; set; }
|
|
public event Action<IView>? Disposed;
|
|
protected List<string> 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<TChild>() where TChild : IView<T>, new()
|
|
{
|
|
var child = new TChild();
|
|
return AddChild(child);
|
|
}
|
|
|
|
public TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
|
|
where TChild : IView<TDataContext>, new()
|
|
{
|
|
var child = new TChild();
|
|
return AddChild(child, dataContextMapper);
|
|
}
|
|
|
|
public virtual TChild AddChild<TChild>(TChild child) where TChild : IView<T>
|
|
{
|
|
child.DataContext = DataContext;
|
|
CopyCommonPropertiesToNewChild(child);
|
|
|
|
var mapper = new DataContextMapper<T, T>(this, child, d => d);
|
|
AddDisposable(mapper);
|
|
child.AddDisposable(mapper);
|
|
|
|
return child;
|
|
}
|
|
|
|
public virtual TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
|
|
where TChild : IView<TDataContext>
|
|
{
|
|
child.DataContext = dataContextMapper(DataContext);
|
|
CopyCommonPropertiesToNewChild(child);
|
|
|
|
var mapper = new DataContextMapper<T, TDataContext>(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<TDataContext>(IView<TDataContext> child)
|
|
{
|
|
var mappers = _disposables
|
|
.Where(d => d is DataContextMapper<T, TDataContext> 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<IDisposable>.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);
|
|
}
|
|
}
|
|
} |