TextBox, PropertyChangeHandler

This commit is contained in:
2023-08-11 21:51:44 +02:00
parent e989a65e81
commit 1fde0df2d6
81 changed files with 1539 additions and 390 deletions

View File

@@ -0,0 +1,157 @@
using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public partial class Border<T> : ContentView<T>
{
[Notify] private Thickness _borderThickness = 1;
[Notify] private Thickness _padding = 0;
[Notify] private char _topChar = '─';
[Notify] private char _leftChar = '│';
[Notify] private char _rightChar = '│';
[Notify] private char _bottomChar = '─';
[Notify] private char _topLeftChar = '┌';
[Notify] private char _topRightChar = '┐';
[Notify] private char _bottomLeftChar = '└';
[Notify] private char _bottomRightChar = '┘';
public Border()
{
RerenderProperties.Add(nameof(BorderThickness));
RerenderProperties.Add(nameof(Padding));
RerenderProperties.Add(nameof(TopChar));
RerenderProperties.Add(nameof(LeftChar));
RerenderProperties.Add(nameof(RightChar));
RerenderProperties.Add(nameof(BottomChar));
}
protected override Size CalculateSize()
{
var size = new Size(
_borderThickness.Left + _borderThickness.Right + _padding.Left + _padding.Right,
_borderThickness.Top + _borderThickness.Bottom + _padding.Top + _padding.Bottom
);
if (Content is null || !Content.IsVisible) return size;
var contentSize = Content.GetRequestedSize();
return new Size(contentSize.Width + size.Width, contentSize.Height + size.Height);
}
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{
if (ContentRendererMethod is null)
{
throw new NullReferenceException(
nameof(ContentRendererMethod)
+ " is null, cannot render content of "
+ Content?.GetType().Name
+ " with DataContext of "
+ DataContext?.GetType().Name);
}
var childPosition = new Position(X: position.X + _borderThickness.Left, Y: position.Y + _borderThickness.Top);
var childSize = new Size(
Width: size.Width - _borderThickness.Left - _borderThickness.Right,
Height: size.Height - _borderThickness.Top - _borderThickness.Bottom
);
if (_padding.Left > 0 || _padding.Top > 0 || _padding.Right > 0 || _padding.Bottom > 0)
{
childPosition = new Position(X: childPosition.X + _padding.Left, Y: childPosition.Y + _padding.Top);
childSize = new Size(
Width: childSize.Width - _padding.Left - _padding.Right,
Height: childSize.Height - _padding.Top - _padding.Bottom
);
}
var contentRendered = ContentRendererMethod(renderContext, childPosition, childSize);
if (contentRendered)
{
var driver = renderContext.ConsoleDriver;
driver.ResetColor();
SetColorsForDriver(renderContext);
RenderTopBorder(renderContext, position, size);
RenderBottomBorder(renderContext, position, size);
RenderLeftBorder(renderContext, position, size);
RenderRightBorder(renderContext, position, size);
RenderTopLeftCorner(renderContext, position);
RenderTopRightCorner(renderContext, position, size);
RenderBottomLeftCorner(renderContext, position, size);
RenderBottomRightCorner(renderContext, position, size);
//TODO render padding
}
return contentRendered;
}
private void RenderTopBorder(RenderContext renderContext, Position position, Size size)
{
position = position with {X = position.X + _borderThickness.Left};
size = new Size(Width: size.Width - _borderThickness.Left - _borderThickness.Right, Height: _borderThickness.Top);
RenderText(_topChar, renderContext.ConsoleDriver, position, size);
}
private void RenderBottomBorder(RenderContext renderContext, Position position, Size size)
{
position = new Position(X: position.X + _borderThickness.Left, Y: position.Y + size.Height - _borderThickness.Bottom);
size = new Size(Width: size.Width - _borderThickness.Left - _borderThickness.Right, Height: _borderThickness.Bottom);
RenderText(_bottomChar, renderContext.ConsoleDriver, position, size);
}
private void RenderLeftBorder(RenderContext renderContext, Position position, Size size)
{
position = position with {Y = position.Y + _borderThickness.Top};
size = new Size(Width: _borderThickness.Left, Height: size.Height - _borderThickness.Top - _borderThickness.Bottom);
RenderText(_leftChar, renderContext.ConsoleDriver, position, size);
}
private void RenderRightBorder(RenderContext renderContext, Position position, Size size)
{
position = new Position(X: position.X + size.Width - _borderThickness.Right, Y: position.Y + _borderThickness.Top);
size = new Size(Width: _borderThickness.Right, Height: size.Height - _borderThickness.Top - _borderThickness.Bottom);
RenderText(_rightChar, renderContext.ConsoleDriver, position, size);
}
private void RenderTopLeftCorner(RenderContext renderContext, Position position)
{
if (_borderThickness.Left == 0 || _borderThickness.Top == 0) return;
var size = new Size(Width: _borderThickness.Left, Height: _borderThickness.Top);
RenderText(_topLeftChar, renderContext.ConsoleDriver, position, size);
}
private void RenderTopRightCorner(RenderContext renderContext, Position position, Size size)
{
if (_borderThickness.Right == 0 || _borderThickness.Top == 0) return;
position = position with {X = position.X + size.Width - _borderThickness.Right};
size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Top);
RenderText(_topRightChar, renderContext.ConsoleDriver, position, size);
}
private void RenderBottomLeftCorner(RenderContext renderContext, Position position, Size size)
{
if (_borderThickness.Left == 0 || _borderThickness.Bottom == 0) return;
position = position with {Y = position.Y + size.Height - _borderThickness.Bottom};
size = new Size(Width: _borderThickness.Left, Height: _borderThickness.Bottom);
RenderText(_bottomLeftChar, renderContext.ConsoleDriver, position, size);
}
private void RenderBottomRightCorner(RenderContext renderContext, Position position, Size size)
{
if (_borderThickness.Right == 0 || _borderThickness.Bottom == 0) return;
position = new Position(
X: position.X + size.Width - _borderThickness.Right,
Y: position.Y + size.Height - _borderThickness.Bottom
);
size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Bottom);
RenderText(_bottomRightChar, renderContext.ConsoleDriver, position, size);
}
}

View File

@@ -1,11 +1,13 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace TerminalUI.Controls;
public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
{
private readonly ObservableCollection<IView> _children = new();
private readonly Dictionary<IView, bool> _visibilities = new();
public ReadOnlyObservableCollection<IView> Children { get; }
public ChildInitializer<T> ChildInitializer { get; }
@@ -28,8 +30,8 @@ public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
ApplicationContext?.EventLoop.RequestRerender();
}
};
((INotifyPropertyChanged)this).PropertyChanged += (o, args) =>
((INotifyPropertyChanged) this).PropertyChanged += (o, args) =>
{
if (args.PropertyName == nameof(ApplicationContext))
{
@@ -41,14 +43,18 @@ public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
};
}
protected override void AttachChildren()
protected void SaveVisibilities()
{
foreach (var child in Children)
_visibilities.Clear();
foreach (var child in _children)
{
child.Attached = true;
_visibilities[child] = child.IsVisible;
}
}
protected bool? GetLastVisibility(IView view)
=> _visibilities.TryGetValue(view, out var visibility) ? visibility : null;
public override TChild AddChild<TChild>(TChild child)
{
child = base.AddChild(child);

View File

@@ -40,18 +40,9 @@ public abstract partial class ContentView<T> : View<T>, IContentRenderer<T>
RerenderProperties.Add(nameof(ContentRendererMethod));
}
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 (Content is null || !Content.IsVisible)
{
if (_placeholderRenderDone) return false;
_placeholderRenderDone = true;

View File

@@ -1,5 +1,4 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using TerminalUI.Extensions;
using TerminalUI.Models;
@@ -13,9 +12,9 @@ public class Grid<T> : ChildContainerView<T>
private List<ColumnDefinition> _columnDefinitions = new() {ColumnDefinition.Star(1)};
private ILogger<Grid<T>>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger<Grid<T>>();
private delegate void WithSizes(RenderContext renderContext, Span<int> widths, Span<int> heights);
private delegate void WithSizes(RenderContext renderContext, ReadOnlySpan<int> widths, ReadOnlySpan<int> heights);
private delegate TResult WithSizes<TResult>(RenderContext renderContext, Span<int> widths, Span<int> heights);
private delegate TResult WithSizes<TResult>(RenderContext renderContext, ReadOnlySpan<int> widths, ReadOnlySpan<int> heights);
private const int ToBeCalculated = -1;
@@ -132,14 +131,14 @@ public class Grid<T> : ChildContainerView<T>
var width = 0;
var height = 0;
for (var i = 0; i < columnWidths.Length; i++)
foreach (var t in columnWidths)
{
width += columnWidths[i];
width += t;
}
for (var i = 0; i < rowHeights.Length; i++)
foreach (var t in rowHeights)
{
height += rowHeights[i];
height += t;
}
return new Size(width, height);
@@ -151,49 +150,122 @@ public class Grid<T> : ChildContainerView<T>
new Option<Size>(size, true),
(context, columnWidths, rowHeights) =>
{
foreach (var child in Children)
context = new RenderContext(
context.ConsoleDriver,
context.ForceRerender,
Foreground ?? context.Foreground,
Background ?? context.Background
);
var viewsByPosition = GroupViewsByPosition(columnWidths.Length, rowHeights.Length);
for (var column = 0; column < columnWidths.Length; column++)
{
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++)
for (var row = 0; row < rowHeights.Length; row++)
{
left += columnWidths[i];
RenderViewsByPosition(
context,
position,
columnWidths,
rowHeights,
viewsByPosition,
column,
row
);
}
for (var i = 0; i < y; i++)
{
top += rowHeights[i];
}
child.Render(context, new Position(left, top), new Size(width, height));
}
return true;
/*var viewsByPosition = GroupViewsByPosition(columnWidths, rowHeights);
CleanUnusedArea(viewsByPosition, columnWidths, rowHeights);*/
});
/*private void CleanUnusedArea(Dictionary<(int, int),List<IView>> viewsByPosition, Span<int> columnWidths, Span<int> rowHeights)
private void RenderViewsByPosition(
RenderContext context,
Position gridPosition,
ReadOnlySpan<int> columnWidths,
ReadOnlySpan<int> rowHeights,
IReadOnlyDictionary<(int, int), List<IView>> viewsByPosition,
int column,
int row)
{
for (var x = 0; x < columnWidths.Length; x++)
if (!viewsByPosition.TryGetValue((column, row), out var children)) return;
var anyChangedVisibility = false;
foreach (var child in children)
{
for (var y = 0; y < rowHeights.Length; y++)
var lastVisibility = GetLastVisibility(child);
if (lastVisibility is { } b && b != child.IsVisible)
{
if (!viewsByPosition.TryGetValue((x, y), out var list)) continue;
anyChangedVisibility = true;
break;
}
}
}*/
/*private Dictionary<(int, int), List<IView>> GroupViewsByPosition(int columns, int rows)
var width = columnWidths[column];
var height = rowHeights[row];
var renderSize = new Size(width, height);
var renderPosition = GetRenderPosition(
gridPosition,
columnWidths,
rowHeights,
column,
row
);
var needsRerender = anyChangedVisibility;
if (needsRerender)
{
context = new RenderContext(
context.ConsoleDriver,
true,
context.Foreground,
context.Background
);
RenderEmpty(context, renderPosition, renderSize);
}
//This implies that children further back in the list will be rendered on top of children placed before in the list.
foreach (var child in children.Where(child => child.IsVisible))
{
var rendered = child.Render(context, renderPosition, renderSize);
if (rendered && !needsRerender)
{
needsRerender = true;
context = new RenderContext(
context.ConsoleDriver,
true,
context.Foreground,
context.Background
);
}
}
static Position GetRenderPosition(
Position gridPosition,
ReadOnlySpan<int> columnWidths,
ReadOnlySpan<int> rowHeights,
int column,
int row
)
{
var left = gridPosition.X;
var top = gridPosition.Y;
for (var i = 0; i < column; i++)
{
left += columnWidths[i];
}
for (var i = 0; i < row; i++)
{
top += rowHeights[i];
}
return new Position(left, top);
}
}
private Dictionary<(int, int), List<IView>> GroupViewsByPosition(int columns, int rows)
{
Dictionary<ValueTuple<int, int>, List<IView>> viewsByPosition = new();
foreach (var child in Children)
@@ -210,7 +282,7 @@ public class Grid<T> : ChildContainerView<T>
}
return viewsByPosition;
}*/
}
private ValueTuple<int, int> GetViewColumnAndRow(IView view, int columns, int rows)
{
@@ -237,7 +309,7 @@ public class Grid<T> : ChildContainerView<T>
{
WithCalculatedSize(renderContext, size, Helper);
object? Helper(RenderContext renderContext1, Span<int> widths, Span<int> heights)
object? Helper(RenderContext renderContext1, ReadOnlySpan<int> widths, ReadOnlySpan<int> heights)
{
actionWithSizes(renderContext1, widths, heights);
return null;

View File

@@ -1,4 +1,5 @@
using System.ComponentModel;
using TerminalUI.Color;
using TerminalUI.Models;
using TerminalUI.Traits;
@@ -17,9 +18,12 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection
int? MaxHeight { get; set; }
int? Height { get; set; }
int ActualHeight { get; }
Margin Margin { get; set; }
Thickness Margin { get; set; }
bool IsVisible { get; set; }
bool Attached { get; set; }
string? Name { get; set; }
IColor? Foreground { get; set; }
IColor? Background { get; set; }
IApplicationContext? ApplicationContext { get; set; }
List<object> Extensions { get; }
RenderMethod RenderMethod { get; set; }

View File

@@ -9,12 +9,12 @@ namespace TerminalUI.Controls;
public partial class ListView<TDataContext, TItem> : View<TDataContext>
{
private static readonly ArrayPool<ListViewItem<TItem>> ListViewItemPool = ArrayPool<ListViewItem<TItem>>.Shared;
private static readonly ArrayPool<ListViewItem<TItem, TDataContext>> ListViewItemPool = ArrayPool<ListViewItem<TItem, TDataContext>>.Shared;
private readonly List<IDisposable> _itemsDisposables = new();
private Func<IEnumerable<TItem>?>? _getItems;
private object? _itemsSource;
private ListViewItem<TItem>[]? _listViewItems;
private ListViewItem<TItem, TDataContext>[]? _listViewItems;
private int _listViewItemLength;
private int _selectedIndex = 0;
private int _renderStartIndex = 0;
@@ -30,6 +30,14 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
if (_selectedIndex != value)
{
_selectedIndex = value;
if (_listViewItems is not null)
{
for (var i = 0; i < _listViewItemLength; i++)
{
_listViewItems[i].IsSelected = i == value;
}
}
OnPropertyChanged();
OnPropertyChanged(nameof(SelectedItem));
}
@@ -124,7 +132,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
}
}
public Func<ListViewItem<TItem>, IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate;
public Func<ListViewItem<TItem, TDataContext>, IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate;
public ListView()
{
@@ -292,7 +300,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
return true;
}
private Span<ListViewItem<TItem>> InstantiateItemViews()
private ReadOnlySpan<ListViewItem<TItem, TDataContext>> InstantiateItemViews()
{
var items = _getItems?.Invoke()?.ToList();
if (items is null)
@@ -305,7 +313,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
return _listViewItems;
}
Span<ListViewItem<TItem>> listViewItems;
ReadOnlySpan<ListViewItem<TItem, TDataContext>> listViewItems;
if (_listViewItems is null || _listViewItemLength != items.Count)
{
@@ -313,7 +321,8 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
for (var i = 0; i < items.Count; i++)
{
var dataContext = items[i];
var child = CreateChild<ListViewItem<TItem>, TItem>(_ => dataContext);
var child = new ListViewItem<TItem, TDataContext>(this);
AddChild(child, _ => dataContext);
var newContent = ItemTemplate(child);
child.Content = newContent;
newListViewItems[i] = child;
@@ -336,12 +345,12 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
return listViewItems;
}
private Span<ListViewItem<TItem>> InstantiateEmptyItemViews()
private ReadOnlySpan<ListViewItem<TItem, TDataContext>> InstantiateEmptyItemViews()
{
_listViewItems = ListViewItemPool.Rent(0);
_listViewItemLength = 0;
return _listViewItems;
}
private static IView<TItem>? DefaultItemTemplate(ListViewItem<TItem> listViewItem) => null;
private static IView<TItem>? DefaultItemTemplate(ListViewItem<TItem, TDataContext> listViewItem) => null;
}

View File

@@ -1,12 +1,23 @@
using TerminalUI.Models;
using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public class ListViewItem<T> : ContentView<T>
public partial class ListViewItem<T, TParentDataContext> : ContentView<T>
{
public ListView<TParentDataContext, T> Parent { get; }
[Notify] private bool _isSelected;
public ListViewItem(ListView<TParentDataContext, T> parent)
{
Parent = parent;
RerenderProperties.Add(nameof(IsSelected));
}
protected override Size CalculateSize()
{
if (Content is null) return new Size(0, 0);
if (Content is null || !Content.IsVisible) return new Size(0, 0);
return Content.GetRequestedSize();
}

View File

@@ -19,7 +19,7 @@ public partial class Rectangle<T> : View<T>
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{
var renderState = new RenderState(position, size, Fill);
if (!NeedsRerender(renderState) || Fill is null) return false;
if ((!renderContext.ForceRerender && !NeedsRerender(renderState)) || Fill is null) return false;
_lastRenderState = renderState;
var driver = renderContext.ConsoleDriver;
@@ -34,7 +34,7 @@ public partial class Rectangle<T> : View<T>
driver.SetCursorPosition(position with {Y = position.Y + i});
driver.Write(s);
}
return true;
}

View File

@@ -17,6 +17,8 @@ public partial class StackPanel<T> : ChildContainerView<T>
foreach (var child in Children)
{
if (!child.IsVisible) continue;
var childSize = child.GetRequestedSize();
_requestedSizes.Add(child, childSize);
@@ -41,6 +43,8 @@ public partial class StackPanel<T> : ChildContainerView<T>
var neededRerender = false;
foreach (var child in Children)
{
if (!child.IsVisible) continue;
if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found");
var childPosition = Orientation == Orientation.Vertical
@@ -49,12 +53,13 @@ public partial class StackPanel<T> : ChildContainerView<T>
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};

View File

@@ -1,12 +1,13 @@
using System.ComponentModel;
using System.Diagnostics;
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Extensions;
using TerminalUI.Models;
namespace TerminalUI.Controls;
[DebuggerDisplay("Text = {Text}")]
public partial class TextBlock<T> : View<T>
{
private record RenderState(
@@ -21,8 +22,6 @@ public partial class TextBlock<T> : View<T>
private bool _placeholderRenderDone;
[Notify] private string? _text = string.Empty;
[Notify] private IColor? _foreground;
[Notify] private IColor? _background;
[Notify] private TextAlignment _textAlignment = TextAlignment.Left;
public TextBlock()
@@ -34,8 +33,6 @@ public partial class TextBlock<T> : View<T>
);
RerenderProperties.Add(nameof(Text));
RerenderProperties.Add(nameof(Foreground));
RerenderProperties.Add(nameof(Background));
RerenderProperties.Add(nameof(TextAlignment));
((INotifyPropertyChanged) this).PropertyChanged += (o, e) =>
@@ -53,9 +50,16 @@ public partial class TextBlock<T> : View<T>
{
if (size.Width == 0 || size.Height == 0) return false;
var driver = renderContext.ConsoleDriver;
var renderState = new RenderState(position, size, Text, _foreground, _background);
if (!NeedsRerender(renderState)) return false;
var foreground = Foreground ?? renderContext.Foreground;
var background = Background ?? renderContext.Background;
var renderState = new RenderState(
position,
size,
Text,
foreground,
background);
if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false;
_lastRenderState = renderState;
@@ -72,41 +76,29 @@ public partial class TextBlock<T> : View<T>
_placeholderRenderDone = false;
var driver = renderContext.ConsoleDriver;
driver.ResetColor();
if (Foreground is { } foreground)
if (foreground is not null)
{
driver.SetForegroundColor(foreground);
}
if (Background is { } background)
if (background is not null)
{
driver.SetBackgroundColor(background);
}
RenderText(_textLines, driver, position, size);
RenderText(_textLines, driver, position, size, TransformText);
return true;
}
private void RenderText(string[] textLines, IConsoleDriver driver, Position position, Size size)
{
for (var i = 0; i < textLines.Length; i++)
private string TransformText(string text, Position position, Size size)
=> TextAlignment switch
{
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);
}
}
TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text),
_ => string.Format($"{{0,{-size.Width}}}", text)
};
private bool NeedsRerender(RenderState renderState)
=> _lastRenderState is null || _lastRenderState != renderState;

View File

@@ -0,0 +1,296 @@
using GeneralInputKey;
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Models;
using TerminalUI.Traits;
namespace TerminalUI.Controls;
public partial class TextBox<T> : View<T>, IFocusable
{
private record RenderState(
string? Text,
Position Position,
Size Size,
IColor? ForegroundColor,
IColor? BackgroundColor
);
private readonly List<Action<GeneralKeyEventArgs>> _keyHandlers = new();
private RenderState? _lastRenderState;
private string _text = string.Empty;
private List<string> _textLines;
private Position? _cursorPosition;
private Position _relativeCursorPosition = new(0, 0);
[Notify] private bool _multiLine;
public bool SetKeyHandledIfKnown { get; set; }
public string Text
{
get => _text;
set
{
ArgumentNullException.ThrowIfNull(value);
if (value == _text) return;
_text = MultiLine ? value : value.Split(Environment.NewLine)[0];
_textLines = _text.Split(Environment.NewLine).ToList();
UpdateRelativeCursorPosition();
OnPropertyChanged();
}
}
public TextBox()
{
_textLines = _text.Split(Environment.NewLine).ToList();
RerenderProperties.Add(nameof(Text));
RerenderProperties.Add(nameof(MultiLine));
}
private void UpdateTextField()
{
_text = string.Join(Environment.NewLine, _textLines);
UpdateRelativeCursorPosition();
OnPropertyChanged(nameof(Text));
}
private void UpdateRelativeCursorPosition()
{
if (_relativeCursorPosition.Y > _textLines.Count - 1)
_relativeCursorPosition = _relativeCursorPosition with {Y = _textLines.Count - 1};
if (_relativeCursorPosition.X > _textLines[_relativeCursorPosition.Y].Length)
_relativeCursorPosition = _relativeCursorPosition with {X = _textLines[_relativeCursorPosition.Y].Length};
}
protected override Size CalculateSize() => new(Width ?? 10, Height ?? 1);
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{
var foreground = Foreground ?? renderContext.Foreground;
var background = Background ?? renderContext.Background;
var renderStatus = new RenderState(
Text,
position,
size,
foreground,
background);
if (!renderContext.ForceRerender && !NeedsRerender(renderStatus)) return false;
_lastRenderState = renderStatus;
var driver = renderContext.ConsoleDriver;
driver.ResetColor();
if (foreground is not null)
{
driver.SetForegroundColor(foreground);
}
if (background is not null)
{
driver.SetBackgroundColor(background);
}
RenderEmpty(renderContext, position, size);
RenderText(_textLines, driver, position, size);
_cursorPosition = position + _relativeCursorPosition;
return true;
}
private bool NeedsRerender(RenderState renderState)
=> _lastRenderState is null || _lastRenderState != renderState;
public void Focus()
=> ApplicationContext?.FocusManager.SetFocus(this);
public void UnFocus()
=> ApplicationContext?.FocusManager.UnFocus(this);
public void SetCursorPosition(IConsoleDriver consoleDriver)
{
if (_cursorPosition is null) return;
consoleDriver.SetCursorPosition(_cursorPosition.Value);
}
public void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
{
HandleKeyInputInternal(keyEventArgs);
if (keyEventArgs.Handled)
{
ApplicationContext?.EventLoop.RequestRerender();
}
}
private void HandleKeyInputInternal(GeneralKeyEventArgs keyEventArgs)
{
if (keyEventArgs.Handled) return;
if (HandleBackspace(keyEventArgs, out var known))
return;
if (!known && HandleDelete(keyEventArgs, out known))
return;
if (!known && HandleNavigation(keyEventArgs, out known))
return;
if (!known && ProcessKeyHandlers(keyEventArgs))
return;
if (!known
&& keyEventArgs.KeyChar != '\0'
&& keyEventArgs.KeyChar.ToString() is {Length: 1} keyString)
{
var y = _relativeCursorPosition.Y;
var x = _relativeCursorPosition.X;
_textLines[y] = _textLines[y][..x] + keyString + _textLines[y][x..];
_relativeCursorPosition = _relativeCursorPosition with {X = x + 1};
keyEventArgs.Handled = true;
UpdateTextField();
}
}
private bool HandleBackspace(GeneralKeyEventArgs keyEventArgs, out bool known)
{
if (keyEventArgs.Key != Keys.Backspace)
{
known = false;
return false;
}
known = true;
if (_relativeCursorPosition is {X: 0, Y: 0})
{
return keyEventArgs.Handled = SetKeyHandledIfKnown;
}
if (_relativeCursorPosition.X == 0)
{
var y = _relativeCursorPosition.Y;
_textLines[y - 1] += _textLines[y];
_textLines.RemoveAt(y);
_relativeCursorPosition = new Position(Y: y - 1, X: _textLines[y - 1].Length);
}
else
{
var y = _relativeCursorPosition.Y;
var x = _relativeCursorPosition.X;
_textLines[y] = _textLines[y].Remove(x - 1, 1);
_relativeCursorPosition = _relativeCursorPosition with {X = x - 1};
}
UpdateTextField();
return keyEventArgs.Handled = true;
}
private bool HandleDelete(GeneralKeyEventArgs keyEventArgs, out bool known)
{
if (keyEventArgs.Key != Keys.Delete)
{
known = false;
return false;
}
known = true;
if (_relativeCursorPosition.Y == _textLines.Count - 1
&& _relativeCursorPosition.X == _textLines[_relativeCursorPosition.Y].Length)
{
return keyEventArgs.Handled = SetKeyHandledIfKnown;
}
if (_relativeCursorPosition.X == _textLines[_relativeCursorPosition.Y].Length)
{
var y = _relativeCursorPosition.Y;
_textLines[y] += _textLines[y + 1];
_textLines.RemoveAt(y + 1);
}
else
{
var y = _relativeCursorPosition.Y;
var x = _relativeCursorPosition.X;
_textLines[y] = _textLines[y].Remove(x, 1);
}
UpdateTextField();
return keyEventArgs.Handled = true;
}
private bool HandleNavigation(GeneralKeyEventArgs keyEventArgs, out bool known)
{
if (keyEventArgs.Key == Keys.Left)
{
known = true;
keyEventArgs.Handled = SetKeyHandledIfKnown;
if (_relativeCursorPosition is {X: 0, Y: 0})
{
return keyEventArgs.Handled;
}
if (_relativeCursorPosition.X == 0)
{
var y = _relativeCursorPosition.Y - 1;
_relativeCursorPosition = new Position(_textLines[y].Length, y);
}
else
{
_relativeCursorPosition = _relativeCursorPosition with {X = _relativeCursorPosition.X - 1};
}
return keyEventArgs.Handled = true;
}
else if (keyEventArgs.Key == Keys.Right)
{
known = true;
keyEventArgs.Handled = SetKeyHandledIfKnown;
if (_relativeCursorPosition.Y == _textLines.Count - 1
&& _relativeCursorPosition.X == _textLines[_relativeCursorPosition.Y].Length)
{
return keyEventArgs.Handled;
}
if (_relativeCursorPosition.X == _textLines[_relativeCursorPosition.Y].Length)
{
_relativeCursorPosition = new Position(0, _relativeCursorPosition.Y + 1);
}
else
{
_relativeCursorPosition = _relativeCursorPosition with {X = _relativeCursorPosition.X + 1};
}
return keyEventArgs.Handled = true;
}
known = false;
return false;
}
private bool ProcessKeyHandlers(GeneralKeyEventArgs keyEventArgs)
{
foreach (var keyHandler in _keyHandlers)
{
keyHandler(keyEventArgs);
if (keyEventArgs.Handled) return true;
}
return false;
}
public TextBox<T> WithKeyHandler(Action<GeneralKeyEventArgs> keyHandler)
{
_keyHandlers.Add(keyHandler);
return this;
}
}

View File

@@ -1,11 +1,16 @@
using System.Buffers;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public delegate string TextTransformer(string text, Position position, Size size);
public abstract partial class View<T> : IView<T>
{
private readonly List<IDisposable> _disposables = new();
@@ -18,24 +23,15 @@ public abstract partial class View<T> : IView<T>
[Notify] private int? _maxHeight;
[Notify] private int? _height;
[Notify] private int _actualHeight;
[Notify] private Margin _margin = new Margin(0, 0, 0, 0);
[Notify] private bool _isVisible = true;
[Notify] private Thickness _margin = 0;
[Notify] private IColor? _foreground;
[Notify] private IColor? _background;
[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();
}
}
}
[Notify] private bool _attached;
protected ObservableCollection<IView> VisualChildren { get; } = new();
public List<object> Extensions { get; } = new();
public RenderMethod RenderMethod { get; set; }
@@ -46,11 +42,16 @@ public abstract partial class View<T> : IView<T>
{
RenderMethod = DefaultRenderer;
RerenderProperties.Add(nameof(Width));
RerenderProperties.Add(nameof(MinWidth));
RerenderProperties.Add(nameof(MaxWidth));
RerenderProperties.Add(nameof(Height));
RerenderProperties.Add(nameof(MinHeight));
RerenderProperties.Add(nameof(MaxHeight));
RerenderProperties.Add(nameof(IsVisible));
RerenderProperties.Add(nameof(Margin));
RerenderProperties.Add(nameof(Foreground));
RerenderProperties.Add(nameof(Background));
((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged;
}
@@ -80,11 +81,6 @@ public abstract partial class View<T> : IView<T>
protected abstract Size CalculateSize();
protected virtual void AttachChildren()
{
}
private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (Attached
@@ -96,6 +92,21 @@ public abstract partial class View<T> : IView<T>
{
ApplicationContext?.EventLoop.RequestRerender();
}
if (e.PropertyName == nameof(Attached))
{
foreach (var visualChild in VisualChildren)
{
visualChild.Attached = Attached;
}
}
else if(e.PropertyName == nameof(ApplicationContext))
{
foreach (var visualChild in VisualChildren)
{
visualChild.ApplicationContext = ApplicationContext;
}
}
}
protected abstract bool DefaultRenderer(RenderContext renderContext, Position position, Size size);
@@ -105,6 +116,8 @@ public abstract partial class View<T> : IView<T>
if (!Attached)
throw new InvalidOperationException("Cannot render unattached view");
if (!IsVisible) return false;
ActualWidth = size.Width;
ActualHeight = size.Height;
@@ -137,6 +150,8 @@ public abstract partial class View<T> : IView<T>
protected void RenderEmpty(RenderContext renderContext, Position position, Size size)
{
var driver = renderContext.ConsoleDriver;
driver.ResetColor();
var placeHolder = new string(ApplicationContext!.EmptyCharacter, size.Width);
for (var i = 0; i < size.Height; i++)
{
@@ -145,6 +160,94 @@ public abstract partial class View<T> : IView<T>
}
}
protected void RenderText(
IList<string> textLines,
IConsoleDriver driver,
Position position,
Size size,
TextTransformer? textTransformer = null)
{
for (var i = 0; i < textLines.Count; i++)
{
var currentPosition = position with {Y = position.Y + i};
var text = textLines[i];
if (textTransformer is not null)
{
text = textTransformer(text, currentPosition, size);
}
if (text.Length > size.Width)
{
text = text[..size.Width];
}
driver.SetCursorPosition(currentPosition);
driver.Write(text);
}
}
protected void RenderText(
string text,
IConsoleDriver driver,
Position position,
Size size,
TextTransformer? textTransformer = null)
{
for (var i = 0; i < size.Height; i++)
{
var currentPosition = position with {Y = position.Y + i};
var finalText = text;
if (textTransformer is not null)
{
finalText = textTransformer(finalText, currentPosition, size);
}
if (finalText.Length > size.Width)
{
finalText = finalText[..size.Width];
}
driver.SetCursorPosition(currentPosition);
driver.Write(finalText);
}
}
protected void RenderText(
char content,
IConsoleDriver driver,
Position position,
Size size)
{
var contentString = new string(content, size.Width);
for (var i = 0; i < size.Height; i++)
{
var currentPosition = position with {Y = position.Y + i};
driver.SetCursorPosition(currentPosition);
driver.Write(contentString);
}
}
protected void SetColorsForDriver(RenderContext renderContext)
{
var driver = renderContext.ConsoleDriver;
var foreground = Foreground ?? renderContext.Foreground;
var background = Background ?? renderContext.Background;
if (foreground is not null)
{
driver.SetForegroundColor(foreground);
}
if (background is not null)
{
driver.SetBackgroundColor(background);
}
}
public TChild CreateChild<TChild>() where TChild : IView<T>, new()
{
var child = new TChild();
@@ -162,6 +265,7 @@ public abstract partial class View<T> : IView<T>
{
child.DataContext = DataContext;
CopyCommonPropertiesToNewChild(child);
VisualChildren.Add(child);
var mapper = new DataContextMapper<T, T>(this, child, d => d);
AddDisposable(mapper);
@@ -175,6 +279,7 @@ public abstract partial class View<T> : IView<T>
{
child.DataContext = dataContextMapper(DataContext);
CopyCommonPropertiesToNewChild(child);
VisualChildren.Add(child);
var mapper = new DataContextMapper<T, TDataContext>(this, child, dataContextMapper);
AddDisposable(mapper);