TextBox, PropertyChangeHandler
This commit is contained in:
9
src/Library/GeneralInputKey/GeneralInputKey.csproj
Normal file
9
src/Library/GeneralInputKey/GeneralInputKey.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
28
src/Library/GeneralInputKey/GeneralKeyEventArgs.cs
Normal file
28
src/Library/GeneralInputKey/GeneralKeyEventArgs.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace GeneralInputKey;
|
||||
|
||||
public class GeneralKeyEventArgs
|
||||
{
|
||||
private readonly Action<bool>? _handledChanged;
|
||||
private bool _handled;
|
||||
public required Keys Key { get; init; }
|
||||
public required char KeyChar { get; init; }
|
||||
public required SpecialKeysStatus SpecialKeysStatus { get; init; }
|
||||
|
||||
public bool Handled
|
||||
{
|
||||
get => _handled;
|
||||
set
|
||||
{
|
||||
if (_handled != value)
|
||||
{
|
||||
_handled = value;
|
||||
_handledChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public GeneralKeyEventArgs(Action<bool>? handledChanged = null)
|
||||
{
|
||||
_handledChanged = handledChanged;
|
||||
}
|
||||
}
|
||||
71
src/Library/GeneralInputKey/Keys.cs
Normal file
71
src/Library/GeneralInputKey/Keys.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GeneralInputKey;
|
||||
|
||||
public enum Keys
|
||||
{
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
E,
|
||||
F,
|
||||
G,
|
||||
H,
|
||||
I,
|
||||
J,
|
||||
K,
|
||||
L,
|
||||
M,
|
||||
N,
|
||||
O,
|
||||
P,
|
||||
Q,
|
||||
R,
|
||||
S,
|
||||
T,
|
||||
U,
|
||||
V,
|
||||
W,
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Enter,
|
||||
Escape,
|
||||
Backspace,
|
||||
Delete,
|
||||
Space,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Comma,
|
||||
Question,
|
||||
Tab,
|
||||
LWin,
|
||||
RWin,
|
||||
[Description("0")] Num0,
|
||||
[Description("1")] Num1,
|
||||
[Description("2")] Num2,
|
||||
[Description("3")] Num3,
|
||||
[Description("4")] Num4,
|
||||
[Description("5")] Num5,
|
||||
[Description("6")] Num6,
|
||||
[Description("7")] Num7,
|
||||
[Description("8")] Num8,
|
||||
[Description("9")] Num9
|
||||
}
|
||||
6
src/Library/GeneralInputKey/SpecialKeysStatus.cs
Normal file
6
src/Library/GeneralInputKey/SpecialKeysStatus.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace GeneralInputKey;
|
||||
|
||||
public record struct SpecialKeysStatus(bool IsAltPressed, bool IsShiftPressed, bool IsCtrlPressed)
|
||||
{
|
||||
public static SpecialKeysStatus Default { get; } = new(false, false, false);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using FileTime.App.Core.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
|
||||
namespace TerminalUI;
|
||||
@@ -6,6 +7,7 @@ namespace TerminalUI;
|
||||
public class ApplicationContext : IApplicationContext
|
||||
{
|
||||
public required IConsoleDriver ConsoleDriver { get; init; }
|
||||
public required IFocusManager FocusManager { get; init; }
|
||||
public ILoggerFactory? LoggerFactory { get; init; }
|
||||
public IEventLoop EventLoop { get; init; }
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
@@ -6,7 +6,7 @@ using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
|
||||
public sealed class Binding<TDataContext, TExpressionResult, TResult> : PropertyTrackerBase<TDataContext, TExpressionResult>
|
||||
{
|
||||
private readonly Func<TDataContext, TExpressionResult> _dataContextMapper;
|
||||
private IView<TDataContext> _dataSourceView;
|
||||
@@ -15,8 +15,6 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
|
||||
private readonly Func<TExpressionResult, TResult> _converter;
|
||||
private readonly TResult? _fallbackValue;
|
||||
private IDisposableCollection? _propertySourceDisposableCollection;
|
||||
private PropertyTrackTreeItem? _propertyTrackTreeItem;
|
||||
private IPropertyChangeTracker? _propertyChangeTracker;
|
||||
|
||||
public Binding(
|
||||
IView<TDataContext> dataSourceView,
|
||||
@@ -25,7 +23,7 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
|
||||
PropertyInfo targetProperty,
|
||||
Func<TExpressionResult, TResult> converter,
|
||||
TResult? fallbackValue = default
|
||||
)
|
||||
) : base(() => dataSourceView.DataContext, dataSourceExpression)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dataSourceView);
|
||||
ArgumentNullException.ThrowIfNull(dataSourceExpression);
|
||||
@@ -39,8 +37,6 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
|
||||
_converter = converter;
|
||||
_fallbackValue = fallbackValue;
|
||||
|
||||
InitTrackingTree(dataSourceExpression);
|
||||
|
||||
UpdateTrackers();
|
||||
|
||||
dataSourceView.PropertyChanged += View_PropertyChanged;
|
||||
@@ -60,106 +56,6 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void InitTrackingTree(Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression)
|
||||
{
|
||||
var properties = new List<string>();
|
||||
FindReactiveProperties(dataContextExpression, properties);
|
||||
|
||||
if (properties.Count > 0)
|
||||
{
|
||||
var rootItem = new PropertyTrackTreeItem();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var pathParts = property.Split('.');
|
||||
var currentItem = rootItem;
|
||||
for (var i = 0; i < pathParts.Length; i++)
|
||||
{
|
||||
if (!currentItem.Children.TryGetValue(pathParts[i], out var child))
|
||||
{
|
||||
child = new PropertyTrackTreeItem();
|
||||
currentItem.Children.Add(pathParts[i], child);
|
||||
}
|
||||
|
||||
currentItem = child;
|
||||
}
|
||||
}
|
||||
|
||||
_propertyTrackTreeItem = rootItem;
|
||||
}
|
||||
}
|
||||
|
||||
private string? FindReactiveProperties(Expression? expression, List<string> properties)
|
||||
{
|
||||
if (expression is null) return "";
|
||||
|
||||
if (expression is LambdaExpression lambdaExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(lambdaExpression.Body, properties));
|
||||
}
|
||||
else if (expression is ConditionalExpression conditionalExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.Test, properties));
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.IfTrue, properties));
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.IfFalse, properties));
|
||||
}
|
||||
else if (expression is MemberExpression memberExpression)
|
||||
{
|
||||
if (memberExpression.Expression is not null)
|
||||
{
|
||||
FindReactiveProperties(memberExpression.Expression, properties);
|
||||
|
||||
if (FindReactiveProperties(memberExpression.Expression, properties) is { } path
|
||||
&& memberExpression.Member is PropertyInfo dataContextPropertyInfo)
|
||||
{
|
||||
path += "." + memberExpression.Member.Name;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (expression is MethodCallExpression methodCallExpression)
|
||||
{
|
||||
if (methodCallExpression.Object is
|
||||
{
|
||||
NodeType:
|
||||
not ExpressionType.Parameter
|
||||
and not ExpressionType.Constant
|
||||
} methodObject)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(methodObject, properties));
|
||||
}
|
||||
|
||||
foreach (var argument in methodCallExpression.Arguments)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(argument, properties));
|
||||
}
|
||||
}
|
||||
else if (expression is BinaryExpression binaryExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties));
|
||||
SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties));
|
||||
}
|
||||
else if (expression is UnaryExpression unaryExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(unaryExpression.Operand, properties));
|
||||
}
|
||||
else if (expression is ParameterExpression parameterExpression)
|
||||
{
|
||||
if (parameterExpression.Type == typeof(TDataContext))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
void SavePropertyPath(string? path)
|
||||
{
|
||||
if (path is null) return;
|
||||
path = path.TrimStart('.');
|
||||
properties.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return;
|
||||
@@ -168,22 +64,7 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
|
||||
UpdateTargetProperty();
|
||||
}
|
||||
|
||||
private void UpdateTrackers()
|
||||
{
|
||||
if (_propertyChangeTracker is not null)
|
||||
{
|
||||
_propertyChangeTracker.Dispose();
|
||||
}
|
||||
|
||||
if (_propertyTrackTreeItem is not null)
|
||||
{
|
||||
_propertyChangeTracker = PropertyChangeHelper.TraverseDataContext(
|
||||
_propertyTrackTreeItem,
|
||||
_dataSourceView.DataContext,
|
||||
UpdateTargetProperty
|
||||
);
|
||||
}
|
||||
}
|
||||
protected override void Update(string propertyPath) => UpdateTargetProperty();
|
||||
|
||||
private void UpdateTargetProperty()
|
||||
{
|
||||
@@ -200,8 +81,9 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
|
||||
_targetProperty.SetValue(_propertySource, value);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_propertySourceDisposableCollection?.RemoveDisposable(this);
|
||||
_dataSourceView.RemoveDisposable(this);
|
||||
_dataSourceView.PropertyChanged -= View_PropertyChanged;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace TerminalUI.Color;
|
||||
|
||||
public record struct Color256(byte Color, ColorType Type) : IColor
|
||||
public readonly record struct Color256(byte Color, ColorType Type) : IColor
|
||||
{
|
||||
public string ToConsoleColor()
|
||||
=> Type switch
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace TerminalUI.Color;
|
||||
|
||||
public record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor
|
||||
public readonly record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor
|
||||
{
|
||||
public string ToConsoleColor()
|
||||
=> Type switch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace TerminalUI.Color;
|
||||
|
||||
public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor
|
||||
public readonly record struct ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor
|
||||
{
|
||||
public string ToConsoleColor() => throw new NotImplementedException();
|
||||
public IColor AsForeground() => this with {Type = ColorType.Foreground};
|
||||
|
||||
157
src/Library/TerminalUI/Controls/Border.cs
Normal file
157
src/Library/TerminalUI/Controls/Border.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
296
src/Library/TerminalUI/Controls/TextBox.cs
Normal file
296
src/Library/TerminalUI/Controls/TextBox.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -9,6 +9,7 @@ public class EventLoop : IEventLoop
|
||||
private readonly object _lock = new();
|
||||
private readonly List<IView> _viewsToRender = new();
|
||||
private bool _rerenderRequested;
|
||||
private bool _lastCursorVisible;
|
||||
|
||||
public EventLoop(IApplicationContext applicationContext)
|
||||
{
|
||||
@@ -44,14 +45,35 @@ public class EventLoop : IEventLoop
|
||||
viewsToRender = _viewsToRender.ToList();
|
||||
}
|
||||
|
||||
var size = _applicationContext.ConsoleDriver.GetWindowSize();
|
||||
var renderContext = new RenderContext(_applicationContext.ConsoleDriver);
|
||||
var driver = _applicationContext.ConsoleDriver;
|
||||
var size = driver.GetWindowSize();
|
||||
var renderContext = new RenderContext(
|
||||
driver,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
);
|
||||
foreach (var view in viewsToRender)
|
||||
{
|
||||
view.Attached = true;
|
||||
view.GetRequestedSize();
|
||||
view.Render(renderContext, new Position(0, 0), size);
|
||||
}
|
||||
|
||||
if (_applicationContext.FocusManager.Focused is { } focused)
|
||||
{
|
||||
focused.SetCursorPosition(driver);
|
||||
if (!_lastCursorVisible)
|
||||
{
|
||||
driver.SetCursorVisible(true);
|
||||
_lastCursorVisible = true;
|
||||
}
|
||||
}
|
||||
else if (_lastCursorVisible)
|
||||
{
|
||||
driver.SetCursorVisible(false);
|
||||
_lastCursorVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddViewToRender(IView view)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using TerminalUI.Controls;
|
||||
using System.Linq.Expressions;
|
||||
using TerminalUI.Controls;
|
||||
|
||||
namespace TerminalUI.Extensions;
|
||||
|
||||
@@ -6,7 +7,7 @@ public static class ViewExtensions
|
||||
{
|
||||
public static T? GetExtension<T>(this IView view)
|
||||
=> (T?) view.Extensions.FirstOrDefault(e => e is T);
|
||||
|
||||
|
||||
public static IView<TDataContext> WithExtension<TDataContext>(this IView<TDataContext> view, object extension)
|
||||
{
|
||||
view.Extensions.Add(extension);
|
||||
@@ -23,4 +24,19 @@ public static class ViewExtensions
|
||||
action(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
public static TItem WithPropertyChangedHandler<TItem, TExpressionResult>(
|
||||
this TItem dataSource,
|
||||
Expression<Func<TItem, TExpressionResult>> dataSourceExpression,
|
||||
Action<string, bool, TExpressionResult> handler)
|
||||
{
|
||||
new PropertyChangedHandler<TItem, TExpressionResult>
|
||||
(
|
||||
dataSource,
|
||||
dataSourceExpression,
|
||||
handler
|
||||
);
|
||||
|
||||
return dataSource;
|
||||
}
|
||||
}
|
||||
31
src/Library/TerminalUI/FocusManager.cs
Normal file
31
src/Library/TerminalUI/FocusManager.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public class FocusManager : IFocusManager
|
||||
{
|
||||
private IFocusable? _focused;
|
||||
|
||||
public IFocusable? Focused
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_focused is not null && !_focused.IsVisible)
|
||||
{
|
||||
_focused = null;
|
||||
}
|
||||
|
||||
return _focused;
|
||||
}
|
||||
|
||||
private set => _focused = value;
|
||||
}
|
||||
|
||||
public void SetFocus(IFocusable focusable) => Focused = focusable;
|
||||
|
||||
public void UnFocus(IFocusable focusable)
|
||||
{
|
||||
if (Focused == focusable)
|
||||
Focused = null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using FileTime.App.Core.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
|
||||
namespace TerminalUI;
|
||||
@@ -10,4 +11,5 @@ public interface IApplicationContext
|
||||
IConsoleDriver ConsoleDriver { get; init; }
|
||||
ILoggerFactory? LoggerFactory { get; init; }
|
||||
char EmptyCharacter { get; init; }
|
||||
IFocusManager FocusManager { get; init; }
|
||||
}
|
||||
10
src/Library/TerminalUI/IFocusManager.cs
Normal file
10
src/Library/TerminalUI/IFocusManager.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public interface IFocusManager
|
||||
{
|
||||
void SetFocus(IFocusable focusable);
|
||||
void UnFocus(IFocusable focusable);
|
||||
IFocusable? Focused { get; }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace TerminalUI.Models;
|
||||
|
||||
public record Margin(int Left, int Top, int Right, int Bottom)
|
||||
{
|
||||
public static implicit operator Margin(int value) => new(value, value, value, value);
|
||||
public static implicit operator Margin((int Left, int Top, int Right, int Bottom) value) => new(value.Left, value.Top, value.Right, value.Bottom);
|
||||
public static implicit operator Margin(string s)
|
||||
{
|
||||
var parts = s.Split(' ');
|
||||
return parts.Length switch
|
||||
{
|
||||
1 => new Margin(int.Parse(parts[0])),
|
||||
2 => new Margin(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[0]), int.Parse(parts[1])),
|
||||
4 => new Margin(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2]), int.Parse(parts[3])),
|
||||
_ => throw new ArgumentException("Invalid margin format", nameof(s))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
namespace TerminalUI.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
public record struct Position(int X, int Y);
|
||||
namespace TerminalUI.Models;
|
||||
|
||||
[DebuggerDisplay("X = {X}, Y = {Y}")]
|
||||
public readonly record struct Position(int X, int Y)
|
||||
{
|
||||
public static Position operator +(Position left, Position right) => new(left.X + right.X, left.Y + right.Y);
|
||||
public static Position operator -(Position left, Position right) => new(left.X - right.X, left.Y - right.Y);
|
||||
}
|
||||
@@ -1,18 +1,32 @@
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
using System.Diagnostics;
|
||||
using TerminalUI.Color;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
|
||||
namespace TerminalUI.Models;
|
||||
|
||||
[DebuggerDisplay("RenderId = {RenderId}, ForceRerender = {ForceRerender}, Driver = {ConsoleDriver.GetType().Name}")]
|
||||
public readonly ref struct RenderContext
|
||||
{
|
||||
private static int _renderId = 0;
|
||||
private static int _renderId;
|
||||
public readonly int RenderId;
|
||||
public readonly IConsoleDriver ConsoleDriver;
|
||||
public readonly bool ForceRerender;
|
||||
public readonly IColor? Foreground;
|
||||
public readonly IColor? Background;
|
||||
|
||||
public RenderContext(IConsoleDriver consoleDriver)
|
||||
public RenderContext(
|
||||
IConsoleDriver consoleDriver,
|
||||
bool forceRerender,
|
||||
IColor? foreground,
|
||||
IColor? background)
|
||||
{
|
||||
ConsoleDriver = consoleDriver;
|
||||
RenderId = _renderId++;
|
||||
|
||||
ConsoleDriver = consoleDriver;
|
||||
ForceRerender = forceRerender;
|
||||
Foreground = foreground;
|
||||
Background = background;
|
||||
}
|
||||
|
||||
public static RenderContext Empty => new(null!);
|
||||
public static RenderContext Empty => new(null!, false, null, null);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
namespace TerminalUI.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace TerminalUI.Models;
|
||||
|
||||
[DebuggerDisplay("Width = {Width}, Height = {Height}")]
|
||||
public readonly record struct Size(int Width, int Height);
|
||||
21
src/Library/TerminalUI/Models/Thickness.cs
Normal file
21
src/Library/TerminalUI/Models/Thickness.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace TerminalUI.Models;
|
||||
|
||||
[DebuggerDisplay("Left = {Left}, Top = {Top}, Right = {Right}, Bottom = {Bottom}")]
|
||||
public record Thickness(int Left, int Top, int Right, int Bottom)
|
||||
{
|
||||
public static implicit operator Thickness(int value) => new(value, value, value, value);
|
||||
public static implicit operator Thickness((int Left, int Top, int Right, int Bottom) value) => new(value.Left, value.Top, value.Right, value.Bottom);
|
||||
public static implicit operator Thickness(string s)
|
||||
{
|
||||
var parts = s.Split(' ');
|
||||
return parts.Length switch
|
||||
{
|
||||
1 => new Thickness(int.Parse(parts[0])),
|
||||
2 => new Thickness(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[0]), int.Parse(parts[1])),
|
||||
4 => new Thickness(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2]), int.Parse(parts[3])),
|
||||
_ => throw new ArgumentException("Invalid margin format", nameof(s))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,25 @@
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
internal interface IPropertyChangeTracker : IDisposable
|
||||
public interface IPropertyChangeTracker : IDisposable
|
||||
{
|
||||
string Name { get; }
|
||||
string Path { get; }
|
||||
Dictionary<string, IPropertyChangeTracker> Children { get; }
|
||||
}
|
||||
|
||||
internal abstract class PropertyChangeTrackerBase : IPropertyChangeTracker
|
||||
public abstract class PropertyChangeTrackerBase : IPropertyChangeTracker
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Path { get; }
|
||||
public Dictionary<string, IPropertyChangeTracker> Children { get; } = new();
|
||||
|
||||
protected PropertyChangeTrackerBase(string name, string path)
|
||||
{
|
||||
Name = name;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
foreach (var propertyChangeTracker in Children.Values)
|
||||
@@ -20,18 +30,20 @@ internal abstract class PropertyChangeTrackerBase : IPropertyChangeTracker
|
||||
}
|
||||
}
|
||||
|
||||
internal class PropertyChangeTracker : PropertyChangeTrackerBase
|
||||
public class PropertyChangeTracker : PropertyChangeTrackerBase
|
||||
{
|
||||
private readonly PropertyTrackTreeItem _propertyTrackTreeItem;
|
||||
private readonly INotifyPropertyChanged _target;
|
||||
private readonly IEnumerable<string> _propertiesToListen;
|
||||
private readonly Action _updateBinding;
|
||||
private readonly Action<string> _updateBinding;
|
||||
|
||||
public PropertyChangeTracker(
|
||||
string name,
|
||||
string path,
|
||||
PropertyTrackTreeItem propertyTrackTreeItem,
|
||||
INotifyPropertyChanged target,
|
||||
IEnumerable<string> propertiesToListen,
|
||||
Action updateBinding)
|
||||
Action<string> updateBinding) : base(name, path)
|
||||
{
|
||||
_propertyTrackTreeItem = propertyTrackTreeItem;
|
||||
_target = target;
|
||||
@@ -48,10 +60,10 @@ internal class PropertyChangeTracker : PropertyChangeTrackerBase
|
||||
return;
|
||||
}
|
||||
|
||||
_updateBinding();
|
||||
Children.Remove(propertyName);
|
||||
|
||||
var newChild = PropertyChangeHelper.TraverseDataContext(
|
||||
|
||||
var newChild = PropertyChangeHelper.CreatePropertyTracker(
|
||||
Path,
|
||||
_propertyTrackTreeItem.Children[propertyName],
|
||||
_target.GetType().GetProperty(propertyName)?.GetValue(_target),
|
||||
_updateBinding
|
||||
@@ -61,6 +73,8 @@ internal class PropertyChangeTracker : PropertyChangeTrackerBase
|
||||
{
|
||||
Children.Add(propertyName, newChild);
|
||||
}
|
||||
|
||||
_updateBinding(propertyName);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
@@ -71,32 +85,54 @@ internal class PropertyChangeTracker : PropertyChangeTrackerBase
|
||||
}
|
||||
}
|
||||
|
||||
internal class NonSubscriberPropertyChangeTracker : PropertyChangeTrackerBase
|
||||
public class NonSubscriberPropertyChangeTracker : PropertyChangeTrackerBase
|
||||
{
|
||||
public NonSubscriberPropertyChangeTracker(string name, string path) : base(name, path)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal class PropertyTrackTreeItem
|
||||
public class PropertyTrackTreeItem
|
||||
{
|
||||
public string Name { get; }
|
||||
public Dictionary<string, PropertyTrackTreeItem> Children { get; } = new();
|
||||
|
||||
public PropertyTrackTreeItem(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class PropertyChangeHelper
|
||||
public static class PropertyChangeHelper
|
||||
{
|
||||
internal static IPropertyChangeTracker? TraverseDataContext(
|
||||
internal static IPropertyChangeTracker? CreatePropertyTracker(
|
||||
string? path,
|
||||
PropertyTrackTreeItem propertyTrackTreeItem,
|
||||
object? obj,
|
||||
Action updateBinding
|
||||
Action<string> updateBinding
|
||||
)
|
||||
{
|
||||
if (obj is null) return null;
|
||||
|
||||
path = path is null ? propertyTrackTreeItem.Name : path + "." + propertyTrackTreeItem.Name;
|
||||
|
||||
IPropertyChangeTracker tracker = obj is INotifyPropertyChanged notifyPropertyChanged
|
||||
? new PropertyChangeTracker(propertyTrackTreeItem, notifyPropertyChanged, propertyTrackTreeItem.Children.Keys, updateBinding)
|
||||
: new NonSubscriberPropertyChangeTracker();
|
||||
? new PropertyChangeTracker(
|
||||
propertyTrackTreeItem.Name,
|
||||
path,
|
||||
propertyTrackTreeItem,
|
||||
notifyPropertyChanged,
|
||||
propertyTrackTreeItem.Children.Keys,
|
||||
updateBinding
|
||||
)
|
||||
: new NonSubscriberPropertyChangeTracker(
|
||||
propertyTrackTreeItem.Name,
|
||||
path);
|
||||
|
||||
foreach (var (propertyName, trackerTreeItem) in propertyTrackTreeItem.Children)
|
||||
{
|
||||
var childTracker = TraverseDataContext(
|
||||
var childTracker = CreatePropertyTracker(
|
||||
path,
|
||||
trackerTreeItem,
|
||||
obj.GetType().GetProperty(propertyName)?.GetValue(obj),
|
||||
updateBinding
|
||||
|
||||
45
src/Library/TerminalUI/PropertyChangedHandler.cs
Normal file
45
src/Library/TerminalUI/PropertyChangedHandler.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public sealed class PropertyChangedHandler<TItem, TExpressionResult> : PropertyTrackerBase<TItem, TExpressionResult>, IDisposable
|
||||
{
|
||||
private readonly TItem _dataSource;
|
||||
private readonly Action<string, bool, TExpressionResult?> _handler;
|
||||
private readonly PropertyTrackTreeItem? _propertyTrackTreeItem;
|
||||
private readonly Func<TItem, TExpressionResult> _propertyValueGenerator;
|
||||
|
||||
public PropertyChangedHandler(
|
||||
TItem dataSource,
|
||||
Expression<Func<TItem, TExpressionResult>> dataSourceExpression,
|
||||
Action<string, bool, TExpressionResult?> handler
|
||||
) : base(() => dataSource, dataSourceExpression)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_handler = handler;
|
||||
ArgumentNullException.ThrowIfNull(dataSource);
|
||||
ArgumentNullException.ThrowIfNull(dataSourceExpression);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
|
||||
_propertyTrackTreeItem = CreateTrackingTree(dataSourceExpression);
|
||||
_propertyValueGenerator = dataSourceExpression.Compile();
|
||||
UpdateTrackers();
|
||||
}
|
||||
|
||||
protected override void Update(string propertyPath)
|
||||
{
|
||||
TExpressionResult? value = default;
|
||||
var parsed = false;
|
||||
|
||||
try
|
||||
{
|
||||
value = _propertyValueGenerator(_dataSource);
|
||||
parsed = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_handler(propertyPath, parsed, value);
|
||||
}
|
||||
}
|
||||
145
src/Library/TerminalUI/PropertyTrackerBase.cs
Normal file
145
src/Library/TerminalUI/PropertyTrackerBase.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public abstract class PropertyTrackerBase<TSource, TExpressionResult> : IDisposable
|
||||
{
|
||||
private readonly Func<TSource?> _source;
|
||||
protected PropertyTrackTreeItem? PropertyTrackTreeItem { get; }
|
||||
protected IPropertyChangeTracker? PropertyChangeTracker { get; private set; }
|
||||
|
||||
protected PropertyTrackerBase(
|
||||
Func<TSource?> source,
|
||||
Expression<Func<TSource?, TExpressionResult>> dataSourceExpression)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dataSourceExpression);
|
||||
|
||||
_source = source;
|
||||
PropertyTrackTreeItem = CreateTrackingTree(dataSourceExpression);
|
||||
}
|
||||
|
||||
protected PropertyTrackTreeItem? CreateTrackingTree(Expression<Func<TSource?, TExpressionResult>> dataContextExpression)
|
||||
{
|
||||
var properties = new List<string>();
|
||||
FindReactiveProperties(dataContextExpression, properties);
|
||||
|
||||
if (properties.Count > 0)
|
||||
{
|
||||
var rootItem = new PropertyTrackTreeItem(null!);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var pathParts = property.Split('.');
|
||||
var currentItem = rootItem;
|
||||
for (var i = 0; i < pathParts.Length; i++)
|
||||
{
|
||||
if (!currentItem.Children.TryGetValue(pathParts[i], out var child))
|
||||
{
|
||||
child = new PropertyTrackTreeItem(pathParts[i]);
|
||||
currentItem.Children.Add(pathParts[i], child);
|
||||
}
|
||||
|
||||
currentItem = child;
|
||||
}
|
||||
}
|
||||
|
||||
return rootItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? FindReactiveProperties(Expression? expression, List<string> properties)
|
||||
{
|
||||
if (expression is null) return "";
|
||||
|
||||
if (expression is LambdaExpression lambdaExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(lambdaExpression.Body, properties));
|
||||
}
|
||||
else if (expression is ConditionalExpression conditionalExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.Test, properties));
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.IfTrue, properties));
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.IfFalse, properties));
|
||||
}
|
||||
else if (expression is MemberExpression memberExpression)
|
||||
{
|
||||
if (memberExpression.Expression is not null)
|
||||
{
|
||||
FindReactiveProperties(memberExpression.Expression, properties);
|
||||
|
||||
if (FindReactiveProperties(memberExpression.Expression, properties) is { } path
|
||||
&& memberExpression.Member is PropertyInfo dataContextPropertyInfo)
|
||||
{
|
||||
path += "." + memberExpression.Member.Name;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (expression is MethodCallExpression methodCallExpression)
|
||||
{
|
||||
if (methodCallExpression.Object is
|
||||
{
|
||||
NodeType:
|
||||
not ExpressionType.Parameter
|
||||
and not ExpressionType.Constant
|
||||
} methodObject)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(methodObject, properties));
|
||||
}
|
||||
|
||||
foreach (var argument in methodCallExpression.Arguments)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(argument, properties));
|
||||
}
|
||||
}
|
||||
else if (expression is BinaryExpression binaryExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties));
|
||||
SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties));
|
||||
}
|
||||
else if (expression is UnaryExpression unaryExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(unaryExpression.Operand, properties));
|
||||
}
|
||||
else if (expression is ParameterExpression parameterExpression)
|
||||
{
|
||||
if (parameterExpression.Type == typeof(TSource))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
void SavePropertyPath(string? path)
|
||||
{
|
||||
if (path is null) return;
|
||||
path = path.TrimStart('.');
|
||||
properties.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateTrackers()
|
||||
{
|
||||
if (PropertyChangeTracker is not null)
|
||||
{
|
||||
PropertyChangeTracker.Dispose();
|
||||
}
|
||||
|
||||
if (PropertyTrackTreeItem is not null)
|
||||
{
|
||||
PropertyChangeTracker = PropertyChangeHelper.CreatePropertyTracker(
|
||||
null,
|
||||
PropertyTrackTreeItem,
|
||||
_source(),
|
||||
Update
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void Update(string propertyPath);
|
||||
|
||||
public virtual void Dispose() => PropertyChangeTracker?.Dispose();
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\DeclarativeProperty\DeclarativeProperty.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -18,4 +19,8 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="KeyHandling\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
13
src/Library/TerminalUI/Traits/IFocusable.cs
Normal file
13
src/Library/TerminalUI/Traits/IFocusable.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using GeneralInputKey;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
using TerminalUI.Controls;
|
||||
|
||||
namespace TerminalUI.Traits;
|
||||
|
||||
public interface IFocusable : IView
|
||||
{
|
||||
void Focus();
|
||||
void UnFocus();
|
||||
void SetCursorPosition(IConsoleDriver consoleDriver);
|
||||
void HandleKeyInput(GeneralKeyEventArgs keyEventArgs);
|
||||
}
|
||||
Reference in New Issue
Block a user