New controls, main view

This commit is contained in:
2023-08-09 20:40:54 +02:00
parent d549733b71
commit 7dcca6363b
41 changed files with 668 additions and 234 deletions

View File

@@ -1,10 +1,12 @@
using TerminalUI.ConsoleDrivers;
using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers;
namespace TerminalUI;
public class ApplicationContext : IApplicationContext
{
public required IConsoleDriver ConsoleDriver { get; init; }
public ILoggerFactory? LoggerFactory { get; init; }
public IEventLoop EventLoop { get; init; }
public bool IsRunning { get; set; }

View File

@@ -6,30 +6,38 @@ using TerminalUI.Traits;
namespace TerminalUI;
public class Binding<TDataContext, TResult> : IDisposable
public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
{
private readonly Func<TDataContext, TResult> _dataContextMapper;
private readonly Func<TDataContext, TExpressionResult> _dataContextMapper;
private IView<TDataContext> _dataSourceView;
private object? _propertySource;
private PropertyInfo _targetProperty;
private readonly Func<TExpressionResult, TResult> _converter;
private readonly TResult? _fallbackValue;
private IDisposableCollection? _propertySourceDisposableCollection;
private PropertyTrackTreeItem? _propertyTrackTreeItem;
private IPropertyChangeTracker? _propertyChangeTracker;
public Binding(
IView<TDataContext> dataSourceView,
Expression<Func<TDataContext?, TResult>> dataContextExpression,
Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression,
object? propertySource,
PropertyInfo targetProperty
PropertyInfo targetProperty,
Func<TExpressionResult, TResult> converter,
TResult? fallbackValue = default
)
{
ArgumentNullException.ThrowIfNull(dataSourceView);
ArgumentNullException.ThrowIfNull(dataContextExpression);
ArgumentNullException.ThrowIfNull(targetProperty);
ArgumentNullException.ThrowIfNull(converter);
_dataSourceView = dataSourceView;
_dataContextMapper = dataContextExpression.Compile();
_propertySource = propertySource;
_targetProperty = targetProperty;
_converter = converter;
_fallbackValue = fallbackValue;
InitTrackingTree(dataContextExpression);
@@ -52,7 +60,7 @@ public class Binding<TDataContext, TResult> : IDisposable
}
}
private void InitTrackingTree(Expression<Func<TDataContext?, TResult>> dataContextExpression)
private void InitTrackingTree(Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression)
{
var properties = new List<string>();
FindReactiveProperties(dataContextExpression, properties);
@@ -175,7 +183,19 @@ public class Binding<TDataContext, TResult> : IDisposable
}
private void UpdateTargetProperty()
=> _targetProperty.SetValue(_propertySource, _dataContextMapper(_dataSourceView.DataContext));
{
TResult value;
try
{
value = _converter(_dataContextMapper(_dataSourceView.DataContext));
}
catch
{
value = _fallbackValue;
}
_targetProperty.SetValue(_propertySource, value);
}
public void Dispose()
{

View File

@@ -11,4 +11,8 @@ public record struct Color256(byte Color, ColorType Type) : IColor
ColorType.Background => $"\x1b[48;5;{Color}m",
_ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType))
};
public IColor AsForeground() => this with {Type = ColorType.Foreground};
public IColor AsBackground() => this with {Type = ColorType.Background};
}

View File

@@ -11,4 +11,7 @@ public record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor
ColorType.Background => $"\x1b[48;2;{R};{G};{B};m",
_ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType))
};
public IColor AsForeground() => this with {Type = ColorType.Foreground};
public IColor AsBackground() => this with {Type = ColorType.Background};
}

View File

@@ -3,4 +3,7 @@
public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor
{
public string ToConsoleColor() => throw new NotImplementedException();
public IColor AsForeground() => this with {Type = ColorType.Foreground};
public IColor AsBackground() => this with {Type = ColorType.Background};
}

View File

@@ -4,4 +4,6 @@ public interface IColor
{
ColorType Type { get; }
string ToConsoleColor();
IColor AsForeground();
IColor AsBackground();
}

View File

@@ -44,6 +44,6 @@ public class DotnetDriver : IConsoleDriver
Console.BackgroundColor = consoleColor.Color;
}
public Size GetBufferSize() => new(Console.BufferWidth, Console.BufferHeight);
public Size GetWindowSize() => new(Console.WindowWidth, Console.WindowHeight);
public void Clear() => Console.Clear();
}

View File

@@ -17,6 +17,6 @@ public interface IConsoleDriver
void SetCursorVisible(bool cursorVisible);
void SetForegroundColor(IColor foreground);
void SetBackgroundColor(IColor background);
Size GetBufferSize();
Size GetWindowSize();
void Clear();
}

View File

@@ -0,0 +1,58 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace TerminalUI.Controls;
public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
{
private readonly ObservableCollection<IView> _children = new();
public ReadOnlyObservableCollection<IView> Children { get; }
public ChildInitializer<T> ChildInitializer { get; }
protected ChildContainerView()
{
ChildInitializer = new ChildInitializer<T>(this);
Children = new ReadOnlyObservableCollection<IView>(_children);
_children.CollectionChanged += (o, args) =>
{
if (Attached)
{
if (args.NewItems?.OfType<IView>() is { } newItems)
{
foreach (var newItem in newItems)
{
newItem.Attached = true;
}
}
ApplicationContext?.EventLoop.RequestRerender();
}
};
((INotifyPropertyChanged)this).PropertyChanged += (o, args) =>
{
if (args.PropertyName == nameof(ApplicationContext))
{
foreach (var child in Children)
{
child.ApplicationContext = ApplicationContext;
}
}
};
}
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 ChildInitializer<T> : IEnumerable<IView>
{
private readonly IChildContainer<T> _childContainer;
public ChildInitializer(IChildContainer<T> childContainer)
{
_childContainer = childContainer;
}
public void Add(IView<T> item) => _childContainer.AddChild(item);
public void Add<TDataContext>(ChildWithDataContextMapper<T, TDataContext> item)
=> _childContainer.AddChild(item.Child, item.DataContextMapper);
public IEnumerator<IView> GetEnumerator() => _childContainer.Children.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View File

@@ -5,14 +5,15 @@ using TerminalUI.ViewExtensions;
namespace TerminalUI.Controls;
public class Grid<T> : View<T>
public class Grid<T> : ChildContainerView<T>
{
private delegate void WithSizes(Span<int> widths, Span<int> heights);
private delegate TResult WithSizes<TResult>(Span<int> widths, Span<int> heights);
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 ObservableCollection<RowDefinition> RowDefinitions { get; } = new() {RowDefinition.Star(1)};
public ObservableCollection<ColumnDefinition> ColumnDefinitions { get; } = new() {ColumnDefinition.Star(1)};
public object? ColumnDefinitionsObject
{
@@ -62,35 +63,76 @@ public class Grid<T> : View<T>
}
}
public Grid()
{
ChildInitializer = new GridChildInitializer<T>(this);
Children = new ReadOnlyObservableCollection<IView>(_children);
_children.CollectionChanged += (o, e) =>
public override Size GetRequestedSize()
=> WithCalculatedSize((columnWidths, rowHeights) =>
{
if (Attached)
var width = 0;
var height = 0;
for (var i = 0; i < columnWidths.Length; i++)
{
if (e.NewItems?.OfType<IView>() is { } newItems)
{
foreach (var newItem in newItems)
{
newItem.Attached = true;
}
}
ApplicationContext?.EventLoop.RequestRerender();
width += columnWidths[i];
}
};
}
public override Size GetRequestedSize() => throw new NotImplementedException();
for (var i = 0; i < rowHeights.Length; i++)
{
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;
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(position.X + left, position.Y + top), new Size(width, height));
}
}, new Option<Size>(size, true));
private void WithCalculatedSize(WithSizes actionWithSizes, Option<Size> size)
{
//TODO: Optimize it, dont calculate all of these only if there is Auto value(s)
WithCalculatedSize(Helper, size);
object? Helper(Span<int> widths, Span<int> heights)
{
actionWithSizes(widths, heights);
return null;
}
}
private TResult WithCalculatedSize<TResult>(WithSizes<TResult> actionWithSizes, Option<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];
var rows = RowDefinitions.Count;
if (columns < 1) columns = 1;
if (rows < 1) rows = 1;
Span<int> allWidth = stackalloc int[columns * rows];
Span<int> allHeight = stackalloc int[columns * rows];
foreach (var child in Children)
{
@@ -104,38 +146,47 @@ public class Grid<T> : View<T>
}
Span<int> columnWidths = stackalloc int[columns];
Span<int> rowHeights = stackalloc int[RowDefinitions.Count];
Span<int> rowHeights = stackalloc int[rows];
var usedWidth = 0;
var widthStars = 0;
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)
else if (size.IsSome && ColumnDefinitions[i].Type == GridUnitType.Star)
{
widthStars += ColumnDefinitions[i].Value;
columnWidths[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < RowDefinitions.Count; j++)
for (var j = 0; j < rows; j++)
{
max = Math.Max(max, allWidth.GetFromMatrix(i, j, columns));
}
columnWidths[i] = max;
}
if (columnWidths[i] != ToBeCalculated)
usedWidth += columnWidths[i];
}
var usedHeight = 0;
var heightStars = 0;
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)
else if (size.IsSome && RowDefinitions[i].Type == GridUnitType.Star)
{
heightStars += RowDefinitions[i].Value;
rowHeights[i] = ToBeCalculated;
}
else
@@ -148,33 +199,39 @@ public class Grid<T> : View<T>
rowHeights[i] = max;
}
if (rowHeights[i] != ToBeCalculated)
usedHeight += rowHeights[i];
}
foreach (var child in Children)
if (size.IsSome)
{
var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
var widthLeft = size.Value.Width - usedWidth;
var heightLeft = size.Value.Height - usedHeight;
var width = columnWidths[x];
var height = rowHeights[y];
var widthPerStart = (int) Math.Floor((double) widthLeft / widthStars);
var heightPerStart = (int) Math.Floor((double) heightLeft / heightStars);
var left = 0;
var top = 0;
for (var i = 0; i < x; i++)
for (var i = 0; i < columnWidths.Length; i++)
{
left += columnWidths[i];
var column = ColumnDefinitions[i];
if (column.Type == GridUnitType.Star)
{
columnWidths[i] = widthPerStart * column.Value;
}
}
for (var i = 0; i < y; i++)
for (var i = 0; i < rowHeights.Length; i++)
{
top += rowHeights[i];
var row = RowDefinitions[i];
if (row.Type == GridUnitType.Star)
{
rowHeights[i] = heightPerStart * row.Value;
}
}
child.Render(new Position(left, top), new Size(width, height));
}
return actionWithSizes(columnWidths, rowHeights);
}
public void SetRowDefinitions(string value)
@@ -190,7 +247,7 @@ public class Grid<T> : View<T>
}
else if (v.EndsWith("*"))
{
var starValue = int.Parse(v[0..^1]);
var starValue = v.Length == 1 ? 1 : int.Parse(v[..^1]);
RowDefinitions.Add(RowDefinition.Star(starValue));
}
else if (int.TryParse(v, out var pixelValue))
@@ -217,7 +274,7 @@ public class Grid<T> : View<T>
}
else if (v.EndsWith("*"))
{
var starValue = int.Parse(v[0..^1]);
var starValue = v.Length == 1 ? 1 : int.Parse(v[..^1]);
ColumnDefinitions.Add(ColumnDefinition.Star(starValue));
}
else if (int.TryParse(v, out var pixelValue))
@@ -230,19 +287,4 @@ public class Grid<T> : View<T>
}
}
}
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

@@ -1,24 +0,0 @@
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,8 @@
using System.Collections.ObjectModel;
namespace TerminalUI.Controls;
public interface IChildContainer<T> : IView<T>
{
ReadOnlyObservableCollection<IView> Children { get; }
}

View File

@@ -39,8 +39,8 @@ 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>;
public TChild AddChild<TChild>(TChild child) where TChild : IView<T>;
TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
public TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>;
}

View File

@@ -1,12 +1,12 @@
using System.Buffers;
using System.Collections.ObjectModel;
using System.Security.Cryptography.X509Certificates;
using DeclarativeProperty;
using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public class ListView<TDataContext, TItem> : View<TDataContext>
public partial class ListView<TDataContext, TItem> : View<TDataContext>
{
private static readonly ArrayPool<ListViewItem<TItem>> ListViewItemPool = ArrayPool<ListViewItem<TItem>>.Shared;
@@ -18,6 +18,8 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
private int _selectedIndex = 0;
private int _renderStartIndex = 0;
private Size _requestedItemSize = new(0, 0);
[Notify] private int _listPadding = 0;
[Notify] private Orientation _orientation = Orientation.Vertical;
public int SelectedIndex
{
@@ -28,7 +30,34 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
{
_selectedIndex = value;
OnPropertyChanged();
ApplicationContext?.EventLoop.RequestRerender();
OnPropertyChanged(nameof(SelectedItem));
}
}
}
public TItem? SelectedItem
{
get => _listViewItems is null ? default : _listViewItems[_selectedIndex].DataContext;
set
{
if (_listViewItems is null || value is null) return;
var newSelectedIndex = -1;
for (var i = 0; i < _listViewItemLength; i++)
{
var dataContext = _listViewItems[i].DataContext;
if (dataContext is null) continue;
if (dataContext.Equals(value))
{
newSelectedIndex = i;
break;
}
}
if (newSelectedIndex != -1)
{
SelectedIndex = newSelectedIndex;
}
}
}
@@ -76,60 +105,87 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
_listViewItems = null;
}
_renderStartIndex = 0;
SelectedIndex = 0;
OnPropertyChanged();
}
}
public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate;
public ListView()
{
RerenderProperties.Add(nameof(ItemsSource));
RerenderProperties.Add(nameof(SelectedIndex));
RerenderProperties.Add(nameof(Orientation));
}
public override Size GetRequestedSize()
{
if (_listViewItems is null || _listViewItems.Length == 0)
InstantiateItemViews();
if (_listViewItems is null || _listViewItemLength == 0)
return new Size(0, 0);
var itemSize = _listViewItems[0].GetRequestedSize();
_requestedItemSize = itemSize;
return itemSize with {Height = itemSize.Height * _listViewItems.Length};
return itemSize with {Height = itemSize.Height * _listViewItemLength};
}
protected override void DefaultRenderer(Position position, Size size)
{
var requestedItemSize = _requestedItemSize;
if (requestedItemSize.Height == 0 || requestedItemSize.Width == 0)
return;
var listViewItems = InstantiateItemViews();
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)
if (heightNeeded > size.Height)
{
var maxItemsToRender = (int) Math.Floor((double) size.Height / requestedItemSize.Height);
if (SelectedIndex < renderStartIndex)
itemsToRender = maxItemsToRender;
if (SelectedIndex - ListPadding < renderStartIndex)
{
renderStartIndex = SelectedIndex - 1;
renderStartIndex = SelectedIndex - ListPadding;
}
else if (SelectedIndex > renderStartIndex + maxItemsToRender)
else if (SelectedIndex + ListPadding >= renderStartIndex + maxItemsToRender)
{
renderStartIndex = SelectedIndex - maxItemsToRender + 1;
renderStartIndex = SelectedIndex + ListPadding - maxItemsToRender + 1;
}
if(renderStartIndex < 0)
if (renderStartIndex + itemsToRender > listViewItems.Length)
renderStartIndex = listViewItems.Length - itemsToRender;
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 lastItemIndex = renderStartIndex + itemsToRender;
if (lastItemIndex > listViewItems.Length)
lastItemIndex = listViewItems.Length;
for (var i = renderStartIndex; i < lastItemIndex; i++)
{
var item = listViewItems[i];
item.Render(position with {Y = position.Y + deltaY}, requestedItemSize);
item.Render(position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width});
deltaY += requestedItemSize.Height;
}
var driver = ApplicationContext!.ConsoleDriver;
var placeholder = new string(' ', size.Width);
driver.ResetColor();
for (var i = deltaY; i < size.Height; i++)
{
driver.SetCursorPosition(position with {Y = position.Y + i});
driver.Write(placeholder);
}
}
private Span<ListViewItem<TItem>> InstantiateItemViews()

View File

@@ -0,0 +1,54 @@
using System.Collections.ObjectModel;
using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public partial class StackPanel<T> : ChildContainerView<T>
{
private readonly Dictionary<IView, Size> _requestedSizes = new();
[Notify] private Orientation _orientation = Orientation.Vertical;
public override Size GetRequestedSize()
{
_requestedSizes.Clear();
var width = 0;
var height = 0;
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
_requestedSizes.Add(child, childSize);
if (Orientation == Orientation.Vertical)
{
width = Math.Max(width, childSize.Width);
height += childSize.Height;
}
else
{
width += childSize.Width;
height = Math.Max(height, childSize.Height);
}
}
return new Size(width, height);
}
protected override void DefaultRenderer(Position position, Size size)
{
var delta = 0;
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);
delta += Orientation == Orientation.Vertical
? childSize.Height
: childSize.Width;
}
}
}

View File

@@ -14,6 +14,7 @@ public partial class TextBlock<T> : View<T>
[Notify] private string? _text = string.Empty;
[Notify] private IColor? _foreground;
[Notify] private IColor? _background;
[Notify] private TextAlignment _textAlignment = TextAlignment.Left;
public TextBlock()
{
@@ -26,12 +27,15 @@ public partial class TextBlock<T> : View<T>
RerenderProperties.Add(nameof(Text));
RerenderProperties.Add(nameof(Foreground));
RerenderProperties.Add(nameof(Background));
RerenderProperties.Add(nameof(TextAlignment));
}
public override Size GetRequestedSize() => new(Text?.Length ?? 0, 1);
protected override void DefaultRenderer(Position position, Size size)
{
if (size.Width == 0 || size.Height == 0) return;
var driver = ApplicationContext!.ConsoleDriver;
var renderContext = new RenderContext(position, Text, _foreground, _background);
if (!NeedsRerender(renderContext)) return;
@@ -52,7 +56,17 @@ public partial class TextBlock<T> : View<T>
driver.SetBackgroundColor(background);
}
driver.Write(Text);
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];
}
driver.Write(text);
}
private bool NeedsRerender(RenderContext renderContext)

View File

@@ -16,6 +16,7 @@ public abstract partial class View<T> : IView<T>
[Notify] private int? _minHeight;
[Notify] private int? _maxHeight;
[Notify] private int? _height;
[Notify] private IApplicationContext? _applicationContext;
private bool _attached;
public bool Attached
@@ -33,7 +34,6 @@ public abstract partial class View<T> : IView<T>
}
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();

View File

@@ -41,14 +41,14 @@ public class EventLoop : IEventLoop
{
if (!_rerenderRequested) return;
_rerenderRequested = false;
viewsToRender = _viewsToRender.ToList();
}
var size =_applicationContext.ConsoleDriver.GetBufferSize();
var size = _applicationContext.ConsoleDriver.GetWindowSize();
foreach (var view in viewsToRender)
{
view.Attached = true;
view.GetRequestedSize();
view.Render(new Position(0, 0), size);
}
}

View File

@@ -6,20 +6,44 @@ namespace TerminalUI.Extensions;
public static class Binding
{
public static Binding<TDataContext, TResult> Bind<TView, TDataContext, TResult>(
public static Binding<TDataContext, TResult, TResult> Bind<TView, TDataContext, TResult>(
this TView targetView,
IView<TDataContext> dataSourceView,
Expression<Func<TDataContext?, TResult>> dataContextExpression,
Expression<Func<TView, TResult>> propertyExpression)
Expression<Func<TView, TResult>> propertyExpression,
TResult? fallbackValue = default)
{
if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo})
throw new AggregateException(nameof(propertyExpression) + " must be a property expression");
return new Binding<TDataContext, TResult>(
dataSourceView,
dataContextExpression,
targetView,
propertyInfo
return new Binding<TDataContext, TResult, TResult>(
dataSourceView,
dataContextExpression,
targetView,
propertyInfo,
value => value,
fallbackValue
);
}
public static Binding<TDataContext, TExpressionResult, TResult> Bind<TView, TDataContext, TExpressionResult, TResult>(
this TView targetView,
IView<TDataContext> dataSourceView,
Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression,
Expression<Func<TView, TResult>> propertyExpression,
Func<TExpressionResult, TResult> converter,
TResult? fallbackValue = default)
{
if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo})
throw new AggregateException(nameof(propertyExpression) + " must be a property expression");
return new Binding<TDataContext, TExpressionResult, TResult>(
dataSourceView,
dataContextExpression,
targetView,
propertyInfo,
converter,
fallbackValue
);
}
}

View File

@@ -11,4 +11,10 @@ public static class ViewExtensions
this IView<TTargetDataContext> view,
Func<TSourceDataContext?, TTargetDataContext?> dataContextMapper)
=> new(view, dataContextMapper);
public static TView Setup<TView>(this TView view, Action<TView> action)
{
action(view);
return view;
}
}

View File

@@ -1,4 +1,5 @@
using TerminalUI.ConsoleDrivers;
using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers;
namespace TerminalUI;
@@ -7,4 +8,5 @@ public interface IApplicationContext
IEventLoop EventLoop { get; init; }
bool IsRunning { get; set; }
IConsoleDriver ConsoleDriver { get; init; }
ILoggerFactory? LoggerFactory { get; init; }
}

View File

@@ -0,0 +1,13 @@
namespace TerminalUI.Models;
public readonly ref struct Option<T>
{
public readonly T Value;
public readonly bool IsSome;
public Option(T value, bool isSome)
{
Value = value;
IsSome = isSome;
}
}

View File

@@ -0,0 +1,7 @@
namespace TerminalUI.Models;
public enum Orientation
{
Horizontal,
Vertical
}

View File

@@ -1,3 +1,3 @@
namespace TerminalUI.Models;
public record Size(int Width, int Height);
public record struct Size(int Width, int Height);

View File

@@ -0,0 +1,8 @@
namespace TerminalUI.Models;
public enum TextAlignment
{
Left,
Center,
Right
}

View File

@@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,3 +1,3 @@
namespace TerminalUI.ViewExtensions;
public record GridPositionExtension(int Row, int Column);
public record GridPositionExtension(int Column, int Row);