Controls, Startup&Driver improvements

This commit is contained in:
2023-08-09 11:54:32 +02:00
parent 2528487ff6
commit d549733b71
49 changed files with 875 additions and 120 deletions

View File

@@ -10,7 +10,7 @@ public abstract class ContentView<T>: View<T>, IContentRenderer
ContentRendererMethod = DefaultContentRender;
}
public IView? Content { get; set; }
public Action<Position> ContentRendererMethod { get; set; }
public Action<Position, Size> ContentRendererMethod { get; set; }
private void DefaultContentRender(Position position) => Content?.Render(position);
private void DefaultContentRender(Position position, Size size) => Content?.Render(position, size);
}

View File

@@ -0,0 +1,248 @@
using System.Collections.ObjectModel;
using TerminalUI.Extensions;
using TerminalUI.Models;
using TerminalUI.ViewExtensions;
namespace TerminalUI.Controls;
public class Grid<T> : View<T>
{
private const int ToBeCalculated = -1;
private readonly ObservableCollection<IView> _children = new();
public ReadOnlyObservableCollection<IView> Children { get; }
public GridChildInitializer<T> ChildInitializer { get; }
public ObservableCollection<RowDefinition> RowDefinitions { get; } = new();
public ObservableCollection<ColumnDefinition> ColumnDefinitions { get; } = new();
public object? ColumnDefinitionsObject
{
get => ColumnDefinitions;
set
{
if (value is IEnumerable<ColumnDefinition> columnDefinitions)
{
ColumnDefinitions.Clear();
foreach (var columnDefinition in columnDefinitions)
{
ColumnDefinitions.Add(columnDefinition);
}
}
else if (value is string s)
{
SetColumnDefinitions(s);
}
else
{
throw new NotSupportedException();
}
}
}
public object? RowDefinitionsObject
{
get => RowDefinitions;
set
{
if (value is IEnumerable<RowDefinition> rowDefinitions)
{
RowDefinitions.Clear();
foreach (var rowDefinition in rowDefinitions)
{
RowDefinitions.Add(rowDefinition);
}
}
else if (value is string s)
{
SetRowDefinitions(s);
}
else
{
throw new NotSupportedException();
}
}
}
public Grid()
{
ChildInitializer = new GridChildInitializer<T>(this);
Children = new ReadOnlyObservableCollection<IView>(_children);
_children.CollectionChanged += (o, e) =>
{
if (Attached)
{
if (e.NewItems?.OfType<IView>() is { } newItems)
{
foreach (var newItem in newItems)
{
newItem.Attached = true;
}
}
ApplicationContext?.EventLoop.RequestRerender();
}
};
}
public override Size GetRequestedSize() => throw new NotImplementedException();
protected override void DefaultRenderer(Position position, Size size)
{
//TODO: Optimize it, dont calculate all of these only if there is Auto value(s)
var columns = ColumnDefinitions.Count;
Span<int> allWidth = stackalloc int[columns * RowDefinitions.Count];
Span<int> allHeight = stackalloc int[columns * RowDefinitions.Count];
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
allWidth.SetToMatrix(childSize.Width, x, y, columns);
allHeight.SetToMatrix(childSize.Height, x, y, columns);
}
Span<int> columnWidths = stackalloc int[columns];
Span<int> rowHeights = stackalloc int[RowDefinitions.Count];
for (var i = 0; i < columnWidths.Length; i++)
{
if (ColumnDefinitions[i].Type == GridUnitType.Pixel)
{
columnWidths[i] = ColumnDefinitions[i].Value;
}
else if (ColumnDefinitions[i].Type == GridUnitType.Star)
{
columnWidths[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < RowDefinitions.Count; j++)
{
max = Math.Max(max, allWidth.GetFromMatrix(i, j, columns));
}
columnWidths[i] = max;
}
}
for (var i = 0; i < rowHeights.Length; i++)
{
if (RowDefinitions[i].Type == GridUnitType.Pixel)
{
rowHeights[i] = RowDefinitions[i].Value;
}
else if (RowDefinitions[i].Type == GridUnitType.Star)
{
rowHeights[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < columns; j++)
{
max = Math.Max(max, allHeight.GetFromMatrix(j, i, columns));
}
rowHeights[i] = max;
}
}
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
var width = columnWidths[x];
var height = rowHeights[y];
var left = 0;
var top = 0;
for (var i = 0; i < x; i++)
{
left += columnWidths[i];
}
for (var i = 0; i < y; i++)
{
top += rowHeights[i];
}
child.Render(new Position(left, top), new Size(width, height));
}
}
public void SetRowDefinitions(string value)
{
var values = value.Split(' ');
RowDefinitions.Clear();
foreach (var v in values)
{
if (v == "Auto")
{
RowDefinitions.Add(RowDefinition.Auto);
}
else if (v.EndsWith("*"))
{
var starValue = int.Parse(v[0..^1]);
RowDefinitions.Add(RowDefinition.Star(starValue));
}
else if (int.TryParse(v, out var pixelValue))
{
RowDefinitions.Add(RowDefinition.Pixel(pixelValue));
}
else
{
throw new ArgumentException("Invalid row definition: " + v);
}
}
}
public void SetColumnDefinitions(string value)
{
var values = value.Split(' ');
ColumnDefinitions.Clear();
foreach (var v in values)
{
if (v == "Auto")
{
ColumnDefinitions.Add(ColumnDefinition.Auto);
}
else if (v.EndsWith("*"))
{
var starValue = int.Parse(v[0..^1]);
ColumnDefinitions.Add(ColumnDefinition.Star(starValue));
}
else if (int.TryParse(v, out var pixelValue))
{
ColumnDefinitions.Add(ColumnDefinition.Pixel(pixelValue));
}
else
{
throw new ArgumentException("Invalid column definition: " + v);
}
}
}
public override TChild AddChild<TChild>(TChild child)
{
child = base.AddChild(child);
_children.Add(child);
return child;
}
public override TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TDataContext : default
{
child = base.AddChild(child, dataContextMapper);
_children.Add(child);
return child;
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections;
namespace TerminalUI.Controls;
public record ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext>(IView<TTargetDataContext> Child, Func<TSourceDataContext?, TTargetDataContext?> DataContextMapper);
public class GridChildInitializer<T> : IEnumerable<IView>
{
private readonly Grid<T> _grid;
public GridChildInitializer(Grid<T> grid)
{
_grid = grid;
}
public void Add(IView<T> item) => _grid.AddChild(item);
public void Add<TDataContext>(ChildWithDataContextMapper<T, TDataContext> item)
=> _grid.AddChild(item.Child, item.DataContextMapper);
public IEnumerator<IView> GetEnumerator() => _grid.Children.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View File

@@ -0,0 +1,23 @@
namespace TerminalUI.Controls;
public enum GridUnitType
{
Auto,
Pixel,
Star
}
public record struct RowDefinition(GridUnitType Type, int Value)
{
public static RowDefinition Auto => new(GridUnitType.Auto, 0);
public static RowDefinition Pixel(int value) => new(GridUnitType.Pixel, value);
public static RowDefinition Star(int value) => new(GridUnitType.Star, value);
}
public record struct ColumnDefinition(GridUnitType Type, int Value)
{
public static ColumnDefinition Auto => new(GridUnitType.Auto, 0);
public static ColumnDefinition Pixel(int value) => new(GridUnitType.Pixel, value);
public static ColumnDefinition Star(int value) => new(GridUnitType.Star, value);
}

View File

@@ -7,10 +7,20 @@ namespace TerminalUI.Controls;
public interface IView : INotifyPropertyChanged, IDisposableCollection
{
object? DataContext { get; set; }
Action<Position> RenderMethod { get; set; }
IApplicationContext? ApplicationContext { get; init;}
int? MinWidth { get; set; }
int? MaxWidth { get; set; }
int? Width { get; set; }
int? MinHeight { get; set; }
int? MaxHeight { get; set; }
int? Height { get; set; }
bool Attached { get; set; }
Size GetRequestedSize();
IApplicationContext? ApplicationContext { get; set; }
List<object> Extensions { get; }
Action<Position, Size> RenderMethod { get; set; }
event Action<IView> Disposed;
void Render(Position position);
void Render(Position position, Size size);
}
public interface IView<T> : IView
@@ -28,4 +38,9 @@ public interface IView<T> : IView
TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>, new();
TChild AddChild<TChild>(TChild child) where TChild : IView<T>;
TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>;
}

View File

@@ -1,5 +1,6 @@
using System.Buffers;
using System.Collections.ObjectModel;
using System.Security.Cryptography.X509Certificates;
using DeclarativeProperty;
using TerminalUI.Models;
@@ -14,6 +15,23 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
private object? _itemsSource;
private ListViewItem<TItem>[]? _listViewItems;
private int _listViewItemLength;
private int _selectedIndex = 0;
private int _renderStartIndex = 0;
private Size _requestedItemSize = new(0, 0);
public int SelectedIndex
{
get => _selectedIndex;
set
{
if (_selectedIndex != value)
{
_selectedIndex = value;
OnPropertyChanged();
ApplicationContext?.EventLoop.RequestRerender();
}
}
}
public object? ItemsSource
{
@@ -64,13 +82,53 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate;
protected override void DefaultRenderer(Position position)
public override Size GetRequestedSize()
{
if (_listViewItems is null || _listViewItems.Length == 0)
return new Size(0, 0);
var itemSize = _listViewItems[0].GetRequestedSize();
_requestedItemSize = itemSize;
return itemSize with {Height = itemSize.Height * _listViewItems.Length};
}
protected override void DefaultRenderer(Position position, Size size)
{
var listViewItems = InstantiateItemViews();
var deltaY = 0;
foreach (var item in listViewItems)
if (listViewItems.Length == 0) return;
var requestedItemSize = _requestedItemSize;
var itemsToRender = listViewItems.Length;
var heightNeeded = requestedItemSize.Height * listViewItems.Length;
var renderStartIndex = _renderStartIndex;
if (heightNeeded < size.Height)
{
item.Render(position with {PosY = position.PosY + deltaY++});
var maxItemsToRender = (int) Math.Floor((double) size.Height / requestedItemSize.Height);
if (SelectedIndex < renderStartIndex)
{
renderStartIndex = SelectedIndex - 1;
}
else if (SelectedIndex > renderStartIndex + maxItemsToRender)
{
renderStartIndex = SelectedIndex - maxItemsToRender + 1;
}
if(renderStartIndex < 0)
renderStartIndex = 0;
else if (renderStartIndex + maxItemsToRender > listViewItems.Length)
renderStartIndex = listViewItems.Length - maxItemsToRender;
_renderStartIndex = renderStartIndex;
}
var deltaY = 0;
for (var i = renderStartIndex; i < itemsToRender && i < listViewItems.Length; i++)
{
var item = listViewItems[i];
item.Render(position with {Y = position.Y + deltaY}, requestedItemSize);
deltaY += requestedItemSize.Height;
}
}

View File

@@ -4,7 +4,13 @@ namespace TerminalUI.Controls;
public class ListViewItem<T> : ContentView<T>
{
protected override void DefaultRenderer(Position position)
public override Size GetRequestedSize()
{
if (Content is null) return new Size(0, 0);
return Content.GetRequestedSize();
}
protected override void DefaultRenderer(Position position, Size size)
{
if (ContentRendererMethod is null)
{
@@ -16,6 +22,6 @@ public class ListViewItem<T> : ContentView<T>
+ DataContext?.GetType().Name);
}
ContentRendererMethod(position);
ContentRendererMethod(position, size);
}
}

View File

@@ -0,0 +1,24 @@
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public partial class Rectangle<T> : View<T>
{
[Notify] private IColor? _fill;
public override Size GetRequestedSize() => new(Width ?? 0, Height ?? 0);
protected override void DefaultRenderer(Position position, Size size)
{
var s = new string('█', Width ?? size.Width);
ApplicationContext?.ConsoleDriver.SetBackgroundColor(Fill ?? new Color.ConsoleColor(System.ConsoleColor.Yellow, ColorType.Background));
ApplicationContext?.ConsoleDriver.SetForegroundColor(Fill ?? new Color.ConsoleColor(System.ConsoleColor.Yellow, ColorType.Foreground));
var height = Height ?? size.Height;
for (var i = 0; i < height; i++)
{
ApplicationContext?.ConsoleDriver.SetCursorPosition(position with {Y = position.Y + i});
ApplicationContext?.ConsoleDriver.Write(s);
}
}
}

View File

@@ -1,4 +1,5 @@
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.Extensions;
using TerminalUI.Models;
@@ -27,7 +28,9 @@ public partial class TextBlock<T> : View<T>
RerenderProperties.Add(nameof(Background));
}
protected override void DefaultRenderer(Position position)
public override Size GetRequestedSize() => new(Text?.Length ?? 0, 1);
protected override void DefaultRenderer(Position position, Size size)
{
var driver = ApplicationContext!.ConsoleDriver;
var renderContext = new RenderContext(position, Text, _foreground, _background);

View File

@@ -10,8 +10,30 @@ public abstract partial class View<T> : IView<T>
{
private readonly List<IDisposable> _disposables = new();
[Notify] private T? _dataContext;
public Action<Position> RenderMethod { get; set; }
public IApplicationContext? ApplicationContext { get; init; }
[Notify] private int? _minWidth;
[Notify] private int? _maxWidth;
[Notify] private int? _width;
[Notify] private int? _minHeight;
[Notify] private int? _maxHeight;
[Notify] private int? _height;
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 Action<Position, Size> RenderMethod { get; set; }
public IApplicationContext? ApplicationContext { get; set; }
public event Action<IView>? Disposed;
protected List<string> RerenderProperties { get; } = new();
@@ -20,22 +42,27 @@ public abstract partial class View<T> : IView<T>
RenderMethod = DefaultRenderer;
((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged;
}
public abstract Size GetRequestedSize();
protected virtual void AttachChildren()
{
}
private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is not null
&& (e.PropertyName == nameof(IView.DataContext)
if (e.PropertyName is not null
&& (e.PropertyName == nameof(IView.DataContext)
|| RerenderProperties.Contains(e.PropertyName)
)
)
)
{
ApplicationContext?.EventLoop.RequestRerender();
}
}
protected abstract void DefaultRenderer(Position position);
protected abstract void DefaultRenderer(Position position, Size size);
public void Render(Position position)
public void Render(Position position, Size size)
{
if (RenderMethod is null)
{
@@ -47,16 +74,27 @@ public abstract partial class View<T> : IView<T>
+ DataContext?.GetType().Name);
}
RenderMethod(position);
RenderMethod(position, size);
}
public TChild CreateChild<TChild>() where TChild : IView<T>, new()
{
var child = new TChild
{
DataContext = DataContext,
ApplicationContext = ApplicationContext
};
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;
child.ApplicationContext = ApplicationContext;
var mapper = new DataContextMapper<T>(this, d => child.DataContext = d);
AddDisposable(mapper);
child.AddDisposable(mapper);
@@ -64,14 +102,12 @@ public abstract partial class View<T> : IView<T>
return child;
}
public TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>, new()
public virtual TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>
{
var child = new TChild
{
DataContext = dataContextMapper(DataContext),
ApplicationContext = ApplicationContext
};
child.DataContext = dataContextMapper(DataContext);
child.ApplicationContext = ApplicationContext;
var mapper = new DataContextMapper<T>(this, d => child.DataContext = dataContextMapper(d));
AddDisposable(mapper);
child.AddDisposable(mapper);