Console upgrades, PossibleCommands VM
This commit is contained in:
@@ -41,6 +41,14 @@ public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
|
||||
};
|
||||
}
|
||||
|
||||
protected override void AttachChildren()
|
||||
{
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Attached = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override TChild AddChild<TChild>(TChild child)
|
||||
{
|
||||
child = base.AddChild(child);
|
||||
|
||||
@@ -1,16 +1,65 @@
|
||||
using TerminalUI.Models;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using TerminalUI.Models;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public abstract class ContentView<T>: View<T>, IContentRenderer
|
||||
public abstract partial class ContentView<T> : View<T>, IContentRenderer<T>
|
||||
{
|
||||
private bool _placeholderRenderDone;
|
||||
[Notify] private RenderMethod _contentRendererMethod;
|
||||
private IView<T>? _content;
|
||||
|
||||
public IView<T>? Content
|
||||
{
|
||||
get => _content;
|
||||
set
|
||||
{
|
||||
if (Equals(value, _content)) return;
|
||||
|
||||
if (_content is not null)
|
||||
{
|
||||
RemoveChild(_content);
|
||||
}
|
||||
|
||||
_content = value;
|
||||
|
||||
if (_content is not null)
|
||||
{
|
||||
AddChild(_content);
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
protected ContentView()
|
||||
{
|
||||
ContentRendererMethod = DefaultContentRender;
|
||||
_contentRendererMethod = DefaultContentRender;
|
||||
RerenderProperties.Add(nameof(Content));
|
||||
RerenderProperties.Add(nameof(ContentRendererMethod));
|
||||
}
|
||||
public IView? Content { get; set; }
|
||||
public Action<Position, Size> ContentRendererMethod { get; set; }
|
||||
|
||||
private void DefaultContentRender(Position position, Size size) => Content?.Render(position, size);
|
||||
protected override void AttachChildren()
|
||||
{
|
||||
base.AttachChildren();
|
||||
if (Content is not null)
|
||||
{
|
||||
Content.Attached = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool DefaultContentRender(RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
if (Content is null)
|
||||
{
|
||||
if (_placeholderRenderDone) return false;
|
||||
_placeholderRenderDone = true;
|
||||
RenderEmpty(renderContext, position, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
_placeholderRenderDone = false;
|
||||
return Content.Render(renderContext, position, size);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TerminalUI.Extensions;
|
||||
using TerminalUI.Models;
|
||||
@@ -8,15 +9,79 @@ namespace TerminalUI.Controls;
|
||||
|
||||
public class Grid<T> : ChildContainerView<T>
|
||||
{
|
||||
private List<RowDefinition> _rowDefinitions = new() {RowDefinition.Star(1)};
|
||||
private List<ColumnDefinition> _columnDefinitions = new() {ColumnDefinition.Star(1)};
|
||||
private ILogger<Grid<T>>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger<Grid<T>>();
|
||||
|
||||
private delegate void WithSizes(Span<int> widths, Span<int> heights);
|
||||
private delegate void WithSizes(RenderContext renderContext, Span<int> widths, Span<int> heights);
|
||||
|
||||
private delegate TResult WithSizes<TResult>(Span<int> widths, Span<int> heights);
|
||||
private delegate TResult WithSizes<TResult>(RenderContext renderContext, Span<int> widths, Span<int> heights);
|
||||
|
||||
private const int ToBeCalculated = -1;
|
||||
public ObservableCollection<RowDefinition> RowDefinitions { get; } = new() {RowDefinition.Star(1)};
|
||||
public ObservableCollection<ColumnDefinition> ColumnDefinitions { get; } = new() {ColumnDefinition.Star(1)};
|
||||
|
||||
public IReadOnlyList<RowDefinition> RowDefinitions
|
||||
{
|
||||
get => _rowDefinitions;
|
||||
set
|
||||
{
|
||||
var nextValue = value;
|
||||
if (value.Count == 0)
|
||||
{
|
||||
nextValue = new List<RowDefinition> {RowDefinition.Star(1)};
|
||||
}
|
||||
|
||||
var needUpdate = nextValue.Count != _rowDefinitions.Count;
|
||||
if (!needUpdate)
|
||||
{
|
||||
for (var i = 0; i < nextValue.Count; i++)
|
||||
{
|
||||
if (!nextValue[i].Equals(_rowDefinitions[i]))
|
||||
{
|
||||
needUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
_rowDefinitions = nextValue.ToList();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<ColumnDefinition> ColumnDefinitions
|
||||
{
|
||||
get => _columnDefinitions;
|
||||
set
|
||||
{
|
||||
var nextValue = value;
|
||||
if (value.Count == 0)
|
||||
{
|
||||
nextValue = new List<ColumnDefinition> {ColumnDefinition.Star(1)};
|
||||
}
|
||||
|
||||
var needUpdate = nextValue.Count != _columnDefinitions.Count;
|
||||
if (!needUpdate)
|
||||
{
|
||||
for (var i = 0; i < nextValue.Count; i++)
|
||||
{
|
||||
if (!nextValue[i].Equals(_columnDefinitions[i]))
|
||||
{
|
||||
needUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
_columnDefinitions = nextValue.ToList();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object? ColumnDefinitionsObject
|
||||
{
|
||||
@@ -25,11 +90,7 @@ public class Grid<T> : ChildContainerView<T>
|
||||
{
|
||||
if (value is IEnumerable<ColumnDefinition> columnDefinitions)
|
||||
{
|
||||
ColumnDefinitions.Clear();
|
||||
foreach (var columnDefinition in columnDefinitions)
|
||||
{
|
||||
ColumnDefinitions.Add(columnDefinition);
|
||||
}
|
||||
ColumnDefinitions = columnDefinitions.ToList();
|
||||
}
|
||||
else if (value is string s)
|
||||
{
|
||||
@@ -49,11 +110,7 @@ public class Grid<T> : ChildContainerView<T>
|
||||
{
|
||||
if (value is IEnumerable<RowDefinition> rowDefinitions)
|
||||
{
|
||||
RowDefinitions.Clear();
|
||||
foreach (var rowDefinition in rowDefinitions)
|
||||
{
|
||||
RowDefinitions.Add(rowDefinition);
|
||||
}
|
||||
RowDefinitions = rowDefinitions.ToList();
|
||||
}
|
||||
else if (value is string s)
|
||||
{
|
||||
@@ -66,85 +123,135 @@ public class Grid<T> : ChildContainerView<T>
|
||||
}
|
||||
}
|
||||
|
||||
public override Size GetRequestedSize()
|
||||
=> WithCalculatedSize((columnWidths, rowHeights) =>
|
||||
{
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
|
||||
for (var i = 0; i < columnWidths.Length; i++)
|
||||
protected override Size CalculateSize()
|
||||
=> WithCalculatedSize(
|
||||
RenderContext.Empty,
|
||||
new Option<Size>(new Size(0, 0), false),
|
||||
(_, columnWidths, rowHeights) =>
|
||||
{
|
||||
width += columnWidths[i];
|
||||
}
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
|
||||
for (var i = 0; i < rowHeights.Length; i++)
|
||||
for (var i = 0; i < columnWidths.Length; i++)
|
||||
{
|
||||
width += columnWidths[i];
|
||||
}
|
||||
|
||||
for (var i = 0; i < rowHeights.Length; i++)
|
||||
{
|
||||
height += rowHeights[i];
|
||||
}
|
||||
|
||||
return new Size(width, height);
|
||||
});
|
||||
|
||||
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
|
||||
=> WithCalculatedSize(
|
||||
renderContext,
|
||||
new Option<Size>(size, true),
|
||||
(context, columnWidths, rowHeights) =>
|
||||
{
|
||||
height += rowHeights[i];
|
||||
}
|
||||
|
||||
return new Size(width, height);
|
||||
}, new Option<Size>(new Size(0, 0), false));
|
||||
|
||||
protected override void DefaultRenderer(Position position, Size size)
|
||||
=> WithCalculatedSize((columnWidths, rowHeights) =>
|
||||
{
|
||||
foreach (var child in Children)
|
||||
{
|
||||
var positionExtension = child.GetExtension<GridPositionExtension>();
|
||||
var x = positionExtension?.Column ?? 0;
|
||||
var y = positionExtension?.Row ?? 0;
|
||||
|
||||
if (x > columnWidths.Length)
|
||||
foreach (var child in Children)
|
||||
{
|
||||
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", child, x, y);
|
||||
x = 0;
|
||||
var (x, y) = GetViewColumnAndRow(child, columnWidths.Length, rowHeights.Length);
|
||||
|
||||
var width = columnWidths[x];
|
||||
var height = rowHeights[y];
|
||||
|
||||
var left = position.X;
|
||||
var top = position.Y;
|
||||
|
||||
for (var i = 0; i < x; i++)
|
||||
{
|
||||
left += columnWidths[i];
|
||||
}
|
||||
|
||||
for (var i = 0; i < y; i++)
|
||||
{
|
||||
top += rowHeights[i];
|
||||
}
|
||||
|
||||
child.Render(context, new Position(left, top), new Size(width, height));
|
||||
}
|
||||
|
||||
if (y > rowHeights.Length)
|
||||
{
|
||||
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", child, x, y);
|
||||
y = 0;
|
||||
}
|
||||
return true;
|
||||
|
||||
var width = columnWidths[x];
|
||||
var height = rowHeights[y];
|
||||
/*var viewsByPosition = GroupViewsByPosition(columnWidths, rowHeights);
|
||||
CleanUnusedArea(viewsByPosition, columnWidths, rowHeights);*/
|
||||
});
|
||||
|
||||
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(position.X + left, position.Y + top), new Size(width, height));
|
||||
}
|
||||
}, new Option<Size>(size, true));
|
||||
|
||||
private void WithCalculatedSize(WithSizes actionWithSizes, Option<Size> size)
|
||||
/*private void CleanUnusedArea(Dictionary<(int, int),List<IView>> viewsByPosition, Span<int> columnWidths, Span<int> rowHeights)
|
||||
{
|
||||
WithCalculatedSize(Helper, size);
|
||||
|
||||
object? Helper(Span<int> widths, Span<int> heights)
|
||||
for (var x = 0; x < columnWidths.Length; x++)
|
||||
{
|
||||
actionWithSizes(widths, heights);
|
||||
for (var y = 0; y < rowHeights.Length; y++)
|
||||
{
|
||||
if (!viewsByPosition.TryGetValue((x, y), out var list)) continue;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
/*private Dictionary<(int, int), List<IView>> GroupViewsByPosition(int columns, int rows)
|
||||
{
|
||||
Dictionary<ValueTuple<int, int>, List<IView>> viewsByPosition = new();
|
||||
foreach (var child in Children)
|
||||
{
|
||||
var (x, y) = GetViewColumnAndRow(child, columns, rows);
|
||||
if (viewsByPosition.TryGetValue((x, y), out var list))
|
||||
{
|
||||
list.Add(child);
|
||||
}
|
||||
else
|
||||
{
|
||||
viewsByPosition[(x, y)] = new List<IView> {child};
|
||||
}
|
||||
}
|
||||
|
||||
return viewsByPosition;
|
||||
}*/
|
||||
|
||||
private ValueTuple<int, int> GetViewColumnAndRow(IView view, int columns, int rows)
|
||||
{
|
||||
var positionExtension = view.GetExtension<GridPositionExtension>();
|
||||
var x = positionExtension?.Column ?? 0;
|
||||
var y = positionExtension?.Row ?? 0;
|
||||
|
||||
if (x > columns)
|
||||
{
|
||||
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y);
|
||||
x = 0;
|
||||
}
|
||||
|
||||
if (y > rows)
|
||||
{
|
||||
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y);
|
||||
y = 0;
|
||||
}
|
||||
|
||||
return (x, y);
|
||||
}
|
||||
|
||||
private void WithCalculatedSize(RenderContext renderContext, Option<Size> size, WithSizes actionWithSizes)
|
||||
{
|
||||
WithCalculatedSize(renderContext, size, Helper);
|
||||
|
||||
object? Helper(RenderContext renderContext1, Span<int> widths, Span<int> heights)
|
||||
{
|
||||
actionWithSizes(renderContext1, widths, heights);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private TResult WithCalculatedSize<TResult>(WithSizes<TResult> actionWithSizes, Option<Size> size)
|
||||
private TResult WithCalculatedSize<TResult>(RenderContext renderContext, Option<Size> size, WithSizes<TResult> actionWithSizes)
|
||||
{
|
||||
//TODO: Optimize it, dont calculate all of these, only if there is Auto value(s)
|
||||
var columns = ColumnDefinitions.Count;
|
||||
var rows = RowDefinitions.Count;
|
||||
|
||||
if (columns < 1) columns = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
Debug.Assert(columns > 0, "Columns must contain at least one element");
|
||||
Debug.Assert(rows > 0, "Rows must contain at least one element");
|
||||
|
||||
Span<int> allWidth = stackalloc int[columns * rows];
|
||||
Span<int> allHeight = stackalloc int[columns * rows];
|
||||
@@ -152,9 +259,7 @@ public class Grid<T> : ChildContainerView<T>
|
||||
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 (x, y) = GetViewColumnAndRow(child, columns, rows);
|
||||
|
||||
allWidth.SetToMatrix(childSize.Width, x, y, columns);
|
||||
allHeight.SetToMatrix(childSize.Height, x, y, columns);
|
||||
@@ -246,60 +351,64 @@ public class Grid<T> : ChildContainerView<T>
|
||||
}
|
||||
}
|
||||
|
||||
return actionWithSizes(columnWidths, rowHeights);
|
||||
return actionWithSizes(renderContext, columnWidths, rowHeights);
|
||||
}
|
||||
|
||||
public void SetRowDefinitions(string value)
|
||||
{
|
||||
var values = value.Split(' ');
|
||||
RowDefinitions.Clear();
|
||||
var rowDefinitions = new List<RowDefinition>();
|
||||
|
||||
foreach (var v in values)
|
||||
{
|
||||
if (v == "Auto")
|
||||
{
|
||||
RowDefinitions.Add(RowDefinition.Auto);
|
||||
rowDefinitions.Add(RowDefinition.Auto);
|
||||
}
|
||||
else if (v.EndsWith("*"))
|
||||
{
|
||||
var starValue = v.Length == 1 ? 1 : int.Parse(v[..^1]);
|
||||
RowDefinitions.Add(RowDefinition.Star(starValue));
|
||||
rowDefinitions.Add(RowDefinition.Star(starValue));
|
||||
}
|
||||
else if (int.TryParse(v, out var pixelValue))
|
||||
{
|
||||
RowDefinitions.Add(RowDefinition.Pixel(pixelValue));
|
||||
rowDefinitions.Add(RowDefinition.Pixel(pixelValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Invalid row definition: " + v);
|
||||
}
|
||||
}
|
||||
|
||||
RowDefinitions = rowDefinitions;
|
||||
}
|
||||
|
||||
public void SetColumnDefinitions(string value)
|
||||
{
|
||||
var values = value.Split(' ');
|
||||
ColumnDefinitions.Clear();
|
||||
var columnDefinitions = new List<ColumnDefinition>();
|
||||
|
||||
foreach (var v in values)
|
||||
{
|
||||
if (v == "Auto")
|
||||
{
|
||||
ColumnDefinitions.Add(ColumnDefinition.Auto);
|
||||
columnDefinitions.Add(ColumnDefinition.Auto);
|
||||
}
|
||||
else if (v.EndsWith("*"))
|
||||
{
|
||||
var starValue = v.Length == 1 ? 1 : int.Parse(v[..^1]);
|
||||
ColumnDefinitions.Add(ColumnDefinition.Star(starValue));
|
||||
columnDefinitions.Add(ColumnDefinition.Star(starValue));
|
||||
}
|
||||
else if (int.TryParse(v, out var pixelValue))
|
||||
{
|
||||
ColumnDefinitions.Add(ColumnDefinition.Pixel(pixelValue));
|
||||
columnDefinitions.Add(ColumnDefinition.Pixel(pixelValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Invalid column definition: " + v);
|
||||
}
|
||||
}
|
||||
|
||||
ColumnDefinitions = columnDefinitions;
|
||||
}
|
||||
}
|
||||
@@ -4,23 +4,29 @@ using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public delegate bool RenderMethod(RenderContext renderContext, Position position, Size size);
|
||||
|
||||
public interface IView : INotifyPropertyChanged, IDisposableCollection
|
||||
{
|
||||
object? DataContext { get; set; }
|
||||
int? MinWidth { get; set; }
|
||||
int? MaxWidth { get; set; }
|
||||
int? Width { get; set; }
|
||||
int ActualWidth { get; }
|
||||
int? MinHeight { get; set; }
|
||||
int? MaxHeight { get; set; }
|
||||
int? Height { get; set; }
|
||||
int ActualHeight { get; }
|
||||
Margin Margin { get; set; }
|
||||
bool Attached { get; set; }
|
||||
Size GetRequestedSize();
|
||||
string? Name { get; set; }
|
||||
IApplicationContext? ApplicationContext { get; set; }
|
||||
List<object> Extensions { get; }
|
||||
|
||||
Action<Position, Size> RenderMethod { get; set; }
|
||||
RenderMethod RenderMethod { get; set; }
|
||||
event Action<IView> Disposed;
|
||||
void Render(Position position, Size size);
|
||||
|
||||
Size GetRequestedSize();
|
||||
bool Render(RenderContext renderContext, Position position, Size size);
|
||||
}
|
||||
|
||||
public interface IView<T> : IView
|
||||
@@ -39,8 +45,10 @@ public interface IView<T> : IView
|
||||
TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
|
||||
where TChild : IView<TDataContext>, new();
|
||||
|
||||
public TChild AddChild<TChild>(TChild child) where TChild : IView<T>;
|
||||
TChild AddChild<TChild>(TChild child) where TChild : IView<T>;
|
||||
|
||||
public TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
|
||||
TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
|
||||
where TChild : IView<TDataContext>;
|
||||
|
||||
void RemoveChild<TDataContext>(IView<TDataContext> child);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using DeclarativeProperty;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using TerminalUI.Models;
|
||||
@@ -79,18 +80,30 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
|
||||
if (_itemsSource is IDeclarativeProperty<ObservableCollection<TItem>> observableDeclarativeProperty)
|
||||
{
|
||||
observableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender();
|
||||
_getItems = () => observableDeclarativeProperty.Value;
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else if (_itemsSource is IDeclarativeProperty<ReadOnlyObservableCollection<TItem>> readOnlyObservableDeclarativeProperty)
|
||||
{
|
||||
readOnlyObservableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender();
|
||||
_getItems = () => readOnlyObservableDeclarativeProperty.Value;
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else if (_itemsSource is IDeclarativeProperty<IEnumerable<TItem>> enumerableDeclarativeProperty)
|
||||
{
|
||||
enumerableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender();
|
||||
_getItems = () => enumerableDeclarativeProperty.Value;
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
if (_itemsSource is ObservableCollection<TItem> observableDeclarative)
|
||||
{
|
||||
((INotifyCollectionChanged) observableDeclarative).CollectionChanged +=
|
||||
(_, _) => ApplicationContext?.EventLoop.RequestRerender();
|
||||
|
||||
_getItems = () => observableDeclarative;
|
||||
}
|
||||
else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative)
|
||||
{
|
||||
((INotifyCollectionChanged) readOnlyObservableDeclarative).CollectionChanged +=
|
||||
(_, _) => ApplicationContext?.EventLoop.RequestRerender();
|
||||
|
||||
_getItems = () => readOnlyObservableDeclarative;
|
||||
}
|
||||
else if (_itemsSource is ICollection<TItem> collection)
|
||||
_getItems = () => collection;
|
||||
@@ -111,7 +124,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
}
|
||||
}
|
||||
|
||||
public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate;
|
||||
public Func<ListViewItem<TItem>, IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate;
|
||||
|
||||
public ListView()
|
||||
{
|
||||
@@ -120,7 +133,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
RerenderProperties.Add(nameof(Orientation));
|
||||
}
|
||||
|
||||
public override Size GetRequestedSize()
|
||||
protected override Size CalculateSize()
|
||||
{
|
||||
InstantiateItemViews();
|
||||
if (_listViewItems is null || _listViewItemLength == 0)
|
||||
@@ -147,19 +160,16 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DefaultRenderer(Position position, Size size)
|
||||
{
|
||||
if (Orientation == Orientation.Vertical)
|
||||
RenderVertical(position, size);
|
||||
else
|
||||
RenderHorizontal(position, size);
|
||||
}
|
||||
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
|
||||
=> Orientation == Orientation.Vertical
|
||||
? RenderVertical(renderContext, position, size)
|
||||
: RenderHorizontal(renderContext, position, size);
|
||||
|
||||
private void RenderHorizontal(Position position, Size size)
|
||||
private bool RenderHorizontal(RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
//Note: no support for same width elements
|
||||
var listViewItems = InstantiateItemViews();
|
||||
if (listViewItems.Length == 0) return;
|
||||
if (listViewItems.Length == 0) return false;
|
||||
|
||||
Span<Size> requestedSizes = stackalloc Size[_listViewItemLength];
|
||||
|
||||
@@ -215,20 +225,22 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
width = size.Width - deltaX;
|
||||
}
|
||||
|
||||
item.Render(position with {X = position.X + deltaX}, size with {Width = width});
|
||||
item.Render(renderContext, position with {X = position.X + deltaX}, size with {Width = width});
|
||||
deltaX = nextDeltaX;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RenderVertical(Position position, Size size)
|
||||
private bool RenderVertical(RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
//Note: only same height is supported
|
||||
var requestedItemSize = _requestedItemSize;
|
||||
if (requestedItemSize.Height == 0 || requestedItemSize.Width == 0)
|
||||
return;
|
||||
return false;
|
||||
|
||||
var listViewItems = InstantiateItemViews();
|
||||
if (listViewItems.Length == 0) return;
|
||||
if (listViewItems.Length == 0) return false;
|
||||
|
||||
var itemsToRender = listViewItems.Length;
|
||||
var heightNeeded = requestedItemSize.Height * listViewItems.Length;
|
||||
@@ -264,7 +276,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
for (var i = renderStartIndex; i < lastItemIndex; i++)
|
||||
{
|
||||
var item = listViewItems[i];
|
||||
item.Render(position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width});
|
||||
item.Render(renderContext, position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width});
|
||||
deltaY += requestedItemSize.Height;
|
||||
}
|
||||
|
||||
@@ -276,6 +288,8 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
driver.SetCursorPosition(position with {Y = position.Y + i});
|
||||
driver.Write(placeholder);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Span<ListViewItem<TItem>> InstantiateItemViews()
|
||||
@@ -300,11 +314,16 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
{
|
||||
var dataContext = items[i];
|
||||
var child = CreateChild<ListViewItem<TItem>, TItem>(_ => dataContext);
|
||||
child.Content = ItemTemplate(child);
|
||||
ItemTemplate(child);
|
||||
var newContent = ItemTemplate(child);
|
||||
child.Content = newContent;
|
||||
newListViewItems[i] = child;
|
||||
}
|
||||
|
||||
if (_listViewItems is not null)
|
||||
{
|
||||
ListViewItemPool.Return(_listViewItems);
|
||||
}
|
||||
|
||||
_listViewItems = newListViewItems;
|
||||
_listViewItemLength = items.Count;
|
||||
listViewItems = newListViewItems[..items.Count];
|
||||
@@ -324,5 +343,5 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
return _listViewItems;
|
||||
}
|
||||
|
||||
private static IView? DefaultItemTemplate(ListViewItem<TItem> listViewItem) => null;
|
||||
private static IView<TItem>? DefaultItemTemplate(ListViewItem<TItem> listViewItem) => null;
|
||||
}
|
||||
@@ -4,13 +4,13 @@ namespace TerminalUI.Controls;
|
||||
|
||||
public class ListViewItem<T> : ContentView<T>
|
||||
{
|
||||
public override Size GetRequestedSize()
|
||||
protected override Size CalculateSize()
|
||||
{
|
||||
if (Content is null) return new Size(0, 0);
|
||||
return Content.GetRequestedSize();
|
||||
}
|
||||
|
||||
protected override void DefaultRenderer(Position position, Size size)
|
||||
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
if (ContentRendererMethod is null)
|
||||
{
|
||||
@@ -22,6 +22,6 @@ public class ListViewItem<T> : ContentView<T>
|
||||
+ DataContext?.GetType().Name);
|
||||
}
|
||||
|
||||
ContentRendererMethod(position, size);
|
||||
return ContentRendererMethod(renderContext, position, size);
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,38 @@ namespace TerminalUI.Controls;
|
||||
|
||||
public partial class Rectangle<T> : View<T>
|
||||
{
|
||||
[Notify] private IColor? _fill;
|
||||
public override Size GetRequestedSize() => new(Width ?? 0, Height ?? 0);
|
||||
private record RenderState(
|
||||
Position Position,
|
||||
Size Size,
|
||||
IColor? Fill);
|
||||
|
||||
protected override void DefaultRenderer(Position position, Size size)
|
||||
private RenderState? _lastRenderState;
|
||||
|
||||
[Notify] private IColor? _fill;
|
||||
protected override Size CalculateSize() => new(Width ?? 0, Height ?? 0);
|
||||
|
||||
protected override bool DefaultRenderer(RenderContext renderContext, 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;
|
||||
var renderState = new RenderState(position, size, Fill);
|
||||
if (!NeedsRerender(renderState) || Fill is null) return false;
|
||||
_lastRenderState = renderState;
|
||||
|
||||
var driver = renderContext.ConsoleDriver;
|
||||
|
||||
var s = new string('█', size.Width);
|
||||
driver.SetBackgroundColor(Fill);
|
||||
driver.SetForegroundColor(Fill);
|
||||
|
||||
var height = size.Height;
|
||||
for (var i = 0; i < height; i++)
|
||||
{
|
||||
ApplicationContext?.ConsoleDriver.SetCursorPosition(position with {Y = position.Y + i});
|
||||
ApplicationContext?.ConsoleDriver.Write(s);
|
||||
driver.SetCursorPosition(position with {Y = position.Y + i});
|
||||
driver.Write(s);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool NeedsRerender(RenderState renderState)
|
||||
=> _lastRenderState is null || _lastRenderState != renderState;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public partial class StackPanel<T> : ChildContainerView<T>
|
||||
private readonly Dictionary<IView, Size> _requestedSizes = new();
|
||||
[Notify] private Orientation _orientation = Orientation.Vertical;
|
||||
|
||||
public override Size GetRequestedSize()
|
||||
protected override Size CalculateSize()
|
||||
{
|
||||
_requestedSizes.Clear();
|
||||
var width = 0;
|
||||
@@ -35,20 +35,38 @@ public partial class StackPanel<T> : ChildContainerView<T>
|
||||
return new Size(width, height);
|
||||
}
|
||||
|
||||
protected override void DefaultRenderer(Position position, Size size)
|
||||
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
var delta = 0;
|
||||
var neededRerender = false;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found");
|
||||
|
||||
var childPosition = Orientation == Orientation.Vertical
|
||||
? position with {Y = position.Y + delta}
|
||||
: position with {X = position.X + delta};
|
||||
child.Render(childPosition, childSize);
|
||||
|
||||
var endX = position.X + size.Width;
|
||||
var endY = position.Y + size.Height;
|
||||
|
||||
if (childPosition.X > endX || childPosition.Y > endY) break;
|
||||
if (childPosition.X + childSize.Width > endX)
|
||||
{
|
||||
childSize = childSize with {Width = endX - childPosition.X};
|
||||
}
|
||||
if (childPosition.Y + childSize.Height > endY)
|
||||
{
|
||||
childSize = childSize with {Height = endY - childPosition.Y};
|
||||
}
|
||||
|
||||
neededRerender = child.Render(renderContext, childPosition, childSize) || neededRerender;
|
||||
|
||||
delta += Orientation == Orientation.Vertical
|
||||
? childSize.Height
|
||||
: childSize.Width;
|
||||
}
|
||||
|
||||
return neededRerender;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using System.ComponentModel;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using TerminalUI.Color;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
using TerminalUI.Extensions;
|
||||
using TerminalUI.Models;
|
||||
|
||||
@@ -7,9 +9,16 @@ namespace TerminalUI.Controls;
|
||||
|
||||
public partial class TextBlock<T> : View<T>
|
||||
{
|
||||
private record RenderContext(Position Position, string? Text, IColor? Foreground, IColor? Background);
|
||||
private record RenderState(
|
||||
Position Position,
|
||||
Size Size,
|
||||
string? Text,
|
||||
IColor? Foreground,
|
||||
IColor? Background);
|
||||
|
||||
private RenderContext? _renderContext;
|
||||
private RenderState? _lastRenderState;
|
||||
private string[]? _textLines;
|
||||
private bool _placeholderRenderDone;
|
||||
|
||||
[Notify] private string? _text = string.Empty;
|
||||
[Notify] private IColor? _foreground;
|
||||
@@ -28,23 +37,41 @@ public partial class TextBlock<T> : View<T>
|
||||
RerenderProperties.Add(nameof(Foreground));
|
||||
RerenderProperties.Add(nameof(Background));
|
||||
RerenderProperties.Add(nameof(TextAlignment));
|
||||
|
||||
((INotifyPropertyChanged) this).PropertyChanged += (o, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Text))
|
||||
{
|
||||
_textLines = Text?.Split(Environment.NewLine);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override Size GetRequestedSize() => new(Text?.Length ?? 0, 1);
|
||||
protected override Size CalculateSize() => new(_textLines?.Max(l => l.Length) ?? 0, _textLines?.Length ?? 0);
|
||||
|
||||
protected override void DefaultRenderer(Position position, Size size)
|
||||
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
if (size.Width == 0 || size.Height == 0) return;
|
||||
if (size.Width == 0 || size.Height == 0) return false;
|
||||
|
||||
var driver = ApplicationContext!.ConsoleDriver;
|
||||
var renderContext = new RenderContext(position, Text, _foreground, _background);
|
||||
if (!NeedsRerender(renderContext)) return;
|
||||
var driver = renderContext.ConsoleDriver;
|
||||
var renderState = new RenderState(position, size, Text, _foreground, _background);
|
||||
if (!NeedsRerender(renderState)) return false;
|
||||
|
||||
_renderContext = renderContext;
|
||||
_lastRenderState = renderState;
|
||||
|
||||
if (Text is null) return;
|
||||
if (_textLines is null)
|
||||
{
|
||||
if (_placeholderRenderDone)
|
||||
{
|
||||
_placeholderRenderDone = true;
|
||||
RenderEmpty(renderContext, position, size);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_placeholderRenderDone = false;
|
||||
|
||||
driver.SetCursorPosition(position);
|
||||
driver.ResetColor();
|
||||
if (Foreground is { } foreground)
|
||||
{
|
||||
@@ -56,19 +83,31 @@ public partial class TextBlock<T> : View<T>
|
||||
driver.SetBackgroundColor(background);
|
||||
}
|
||||
|
||||
var text = TextAlignment switch
|
||||
{
|
||||
TextAlignment.Right => string.Format($"{{0,{size.Width}}}", Text),
|
||||
_ => string.Format($"{{0,{-size.Width}}}", Text)
|
||||
};
|
||||
if (text.Length > size.Width)
|
||||
{
|
||||
text = text[..size.Width];
|
||||
}
|
||||
RenderText(_textLines, driver, position, size);
|
||||
|
||||
driver.Write(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool NeedsRerender(RenderContext renderContext)
|
||||
=> _renderContext is null || _renderContext != renderContext;
|
||||
private void RenderText(string[] textLines, IConsoleDriver driver, Position position, Size size)
|
||||
{
|
||||
for (var i = 0; i < textLines.Length; i++)
|
||||
{
|
||||
var text = textLines[i];
|
||||
text = TextAlignment switch
|
||||
{
|
||||
TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text),
|
||||
_ => string.Format($"{{0,{-size.Width}}}", text)
|
||||
};
|
||||
if (text.Length > size.Width)
|
||||
{
|
||||
text = text[..size.Width];
|
||||
}
|
||||
|
||||
driver.SetCursorPosition(position with {Y = position.Y + i});
|
||||
driver.Write(text);
|
||||
}
|
||||
}
|
||||
|
||||
private bool NeedsRerender(RenderState renderState)
|
||||
=> _lastRenderState is null || _lastRenderState != renderState;
|
||||
}
|
||||
@@ -13,9 +13,13 @@ public abstract partial class View<T> : IView<T>
|
||||
[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;
|
||||
|
||||
@@ -32,17 +36,50 @@ public abstract partial class View<T> : IView<T>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<object> Extensions { get; } = new();
|
||||
public Action<Position, Size> RenderMethod { get; set; }
|
||||
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 abstract Size GetRequestedSize();
|
||||
|
||||
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()
|
||||
{
|
||||
@@ -50,7 +87,8 @@ public abstract partial class View<T> : IView<T>
|
||||
|
||||
private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is not null
|
||||
if (Attached
|
||||
&& e.PropertyName is not null
|
||||
&& (e.PropertyName == nameof(IView.DataContext)
|
||||
|| RerenderProperties.Contains(e.PropertyName)
|
||||
)
|
||||
@@ -60,10 +98,16 @@ public abstract partial class View<T> : IView<T>
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void DefaultRenderer(Position position, Size size);
|
||||
protected abstract bool DefaultRenderer(RenderContext renderContext, Position position, Size size);
|
||||
|
||||
public void Render(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(
|
||||
@@ -74,7 +118,31 @@ public abstract partial class View<T> : IView<T>
|
||||
+ DataContext?.GetType().Name);
|
||||
}
|
||||
|
||||
RenderMethod(position, size);
|
||||
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()
|
||||
@@ -93,9 +161,9 @@ public abstract partial class View<T> : IView<T>
|
||||
public virtual TChild AddChild<TChild>(TChild child) where TChild : IView<T>
|
||||
{
|
||||
child.DataContext = DataContext;
|
||||
child.ApplicationContext = ApplicationContext;
|
||||
CopyCommonPropertiesToNewChild(child);
|
||||
|
||||
var mapper = new DataContextMapper<T>(this, d => child.DataContext = d);
|
||||
var mapper = new DataContextMapper<T, T>(this, child, d => d);
|
||||
AddDisposable(mapper);
|
||||
child.AddDisposable(mapper);
|
||||
|
||||
@@ -106,15 +174,36 @@ public abstract partial class View<T> : IView<T>
|
||||
where TChild : IView<TDataContext>
|
||||
{
|
||||
child.DataContext = dataContextMapper(DataContext);
|
||||
child.ApplicationContext = ApplicationContext;
|
||||
CopyCommonPropertiesToNewChild(child);
|
||||
|
||||
var mapper = new DataContextMapper<T>(this, d => child.DataContext = dataContextMapper(d));
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user