Console DialogService
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace ObservableComputations;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
[ObservableComputationsCall]
|
||||
public static Filtering<TSourceItem> Filtering<TSourceItem>(
|
||||
this ReadOnlyObservableCollection<TSourceItem> source,
|
||||
Expression<Func<TSourceItem, bool>> predicateExpression,
|
||||
int initialCapacity = 0)
|
||||
=> new(source, predicateExpression, initialCapacity);
|
||||
|
||||
[ObservableComputationsCall]
|
||||
public static Selecting<TSourceItem, TResultItem> Selecting<TSourceItem, TResultItem>(
|
||||
this ReadOnlyObservableCollection<TSourceItem> source,
|
||||
|
||||
@@ -22,9 +22,10 @@ public class DotnetDriver : IConsoleDriver
|
||||
return new(x, y);
|
||||
}
|
||||
|
||||
public void Write(string text) => Console.Write(text);
|
||||
public void Write(string text) => Console.Out.Write(text);
|
||||
public void Write(ReadOnlySpan<char> text) => Console.Out.Write(text);
|
||||
|
||||
public void Write(char text) => Console.Write(text);
|
||||
public void Write(char text) => Console.Out.Write(text);
|
||||
|
||||
public virtual void Dispose() => Console.Clear();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public interface IConsoleDriver
|
||||
void ResetColor();
|
||||
Position GetCursorPosition();
|
||||
void Write(string text);
|
||||
void Write(ReadOnlySpan<char> text);
|
||||
void Write(char text);
|
||||
bool CanRead();
|
||||
ConsoleKeyInfo ReadKey();
|
||||
|
||||
@@ -4,7 +4,7 @@ using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public partial class Border<T> : ContentView<T>, IDisplayView
|
||||
public sealed partial class Border<T> : ContentView<Border<T>, T>, IDisplayView
|
||||
{
|
||||
[Notify] private Thickness _borderThickness = 1;
|
||||
[Notify] private Thickness _padding = 0;
|
||||
|
||||
@@ -4,7 +4,9 @@ using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
|
||||
public abstract class ChildContainerView<TConcrete, T>
|
||||
: View<TConcrete, T>, IChildContainer<T>
|
||||
where TConcrete : View<TConcrete, T>
|
||||
{
|
||||
private readonly ObservableCollection<IView> _children = new();
|
||||
public ReadOnlyObservableCollection<IView> Children { get; }
|
||||
|
||||
@@ -4,7 +4,9 @@ using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public abstract partial class ContentView<T> : View<T>, IContentRenderer<T>
|
||||
public abstract partial class ContentView<TConcrete, T>
|
||||
: View<TConcrete, T>, IContentRenderer<T>
|
||||
where TConcrete : View<TConcrete, T>
|
||||
{
|
||||
private bool _placeholderRenderDone;
|
||||
[Notify] private RenderMethod _contentRendererMethod;
|
||||
|
||||
@@ -7,7 +7,7 @@ using TerminalUI.ViewExtensions;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public class Grid<T> : ChildContainerView<T>, IVisibilityChangeHandler
|
||||
public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeHandler
|
||||
{
|
||||
private readonly List<IView> _forceRerenderChildren = new();
|
||||
private readonly object _forceRerenderChildrenLock = new();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.ComponentModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using GeneralInputKey;
|
||||
using TerminalUI.Color;
|
||||
using TerminalUI.Models;
|
||||
using TerminalUI.Traits;
|
||||
@@ -28,10 +30,12 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection
|
||||
List<object> Extensions { get; }
|
||||
RenderMethod RenderMethod { get; set; }
|
||||
IView? VisualParent { get; set; }
|
||||
ReadOnlyObservableCollection<IView> VisualChildren { get; }
|
||||
event Action<IView> Disposed;
|
||||
|
||||
Size GetRequestedSize();
|
||||
bool Render(in RenderContext renderContext, Position position, Size size);
|
||||
void HandleKeyInput(GeneralKeyEventArgs keyEventArgs);
|
||||
}
|
||||
|
||||
public interface IView<T> : IView
|
||||
|
||||
194
src/Library/TerminalUI/Controls/ItemsControl.cs
Normal file
194
src/Library/TerminalUI/Controls/ItemsControl.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using ObservableComputations;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using TerminalUI.Models;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public sealed partial class ItemsControl<TDataContext, TItem>
|
||||
: View<ItemsControl<TDataContext, TItem>, TDataContext>, IVisibilityChangeHandler
|
||||
{
|
||||
private readonly List<IView> _forceRerenderChildren = new();
|
||||
private readonly object _forceRerenderChildrenLock = new();
|
||||
private readonly List<IDisposable> _itemsDisposables = new();
|
||||
private readonly Dictionary<IView, Size> _requestedSizes = new();
|
||||
private IList<IView<TItem>> _children = new List<IView<TItem>>();
|
||||
private object? _itemsSource;
|
||||
[Notify] private Orientation _orientation = Orientation.Vertical;
|
||||
|
||||
public Func<IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate;
|
||||
|
||||
public IReadOnlyList<IView<TItem>> Children => _children.AsReadOnly();
|
||||
|
||||
public object? ItemsSource
|
||||
{
|
||||
get => _itemsSource;
|
||||
set
|
||||
{
|
||||
if (_itemsSource == value) return;
|
||||
_itemsSource = value;
|
||||
|
||||
foreach (var disposable in _itemsDisposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_itemsDisposables.Clear();
|
||||
|
||||
if (_itemsSource is ObservableCollection<TItem> observableDeclarative)
|
||||
{
|
||||
_children = observableDeclarative
|
||||
.Selecting(i => CreateItem(i))
|
||||
.OfTypeComputing<IView<TItem>>();
|
||||
}
|
||||
else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative)
|
||||
{
|
||||
_children = readOnlyObservableDeclarative
|
||||
.Selecting(i => CreateItem(i))
|
||||
.OfTypeComputing<IView<TItem>>();
|
||||
}
|
||||
else if (_itemsSource is ICollection<TItem> collection)
|
||||
_children = collection.Select(CreateItem).OfType<IView<TItem>>().ToList();
|
||||
else if (_itemsSource is TItem[] array)
|
||||
_children = array.Select(CreateItem).OfType<IView<TItem>>().ToList();
|
||||
else if (_itemsSource is IEnumerable<TItem> enumerable)
|
||||
_children = enumerable.Select(CreateItem).OfType<IView<TItem>>().ToList();
|
||||
else if (value is null)
|
||||
{
|
||||
_children = new List<IView<TItem>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Children));
|
||||
}
|
||||
}
|
||||
|
||||
public ItemsControl()
|
||||
{
|
||||
RerenderProperties.Add(nameof(ItemsSource));
|
||||
RerenderProperties.Add(nameof(Orientation));
|
||||
}
|
||||
|
||||
|
||||
protected override Size CalculateSize()
|
||||
{
|
||||
_requestedSizes.Clear();
|
||||
double width = 0;
|
||||
double height = 0;
|
||||
|
||||
foreach (var child in _children)
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
var childSize = child.GetRequestedSize();
|
||||
_requestedSizes.Add(child, childSize);
|
||||
|
||||
if (Orientation == Orientation.Vertical)
|
||||
{
|
||||
width = Math.Max(width, childSize.Width);
|
||||
height += childSize.Height;
|
||||
}
|
||||
else
|
||||
{
|
||||
width += childSize.Width;
|
||||
height = Math.Max(height, childSize.Height);
|
||||
}
|
||||
}
|
||||
|
||||
return new Size((int) width, (int) height);
|
||||
}
|
||||
|
||||
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
var neededRerender = false;
|
||||
IReadOnlyList<IView> forceRerenderChildren;
|
||||
lock (_forceRerenderChildrenLock)
|
||||
{
|
||||
forceRerenderChildren = _forceRerenderChildren.ToList();
|
||||
_forceRerenderChildren.Clear();
|
||||
}
|
||||
|
||||
var delta = 0;
|
||||
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
|
||||
? position with {Y = position.Y + delta}
|
||||
: position with {X = position.X + delta};
|
||||
|
||||
childSize = Orientation == Orientation.Vertical
|
||||
? childSize with {Width = size.Width}
|
||||
: childSize with {Height = size.Height};
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
if (forceRerenderChildren.Contains(child))
|
||||
{
|
||||
var rerenderContext = new RenderContext(
|
||||
renderContext.ConsoleDriver,
|
||||
true,
|
||||
renderContext.Foreground,
|
||||
renderContext.Background,
|
||||
renderContext.Statistics
|
||||
);
|
||||
neededRerender = child.Render(rerenderContext, childPosition, childSize) || neededRerender;
|
||||
}
|
||||
else
|
||||
{
|
||||
neededRerender = child.Render(renderContext, childPosition, childSize) || neededRerender;
|
||||
}
|
||||
|
||||
delta += Orientation == Orientation.Vertical
|
||||
? childSize.Height
|
||||
: childSize.Width;
|
||||
}
|
||||
|
||||
return neededRerender;
|
||||
}
|
||||
|
||||
private IView<TItem>? CreateItem(TItem dataContext)
|
||||
{
|
||||
var newItem = ItemTemplate();
|
||||
AddChild(newItem, _ => dataContext);
|
||||
return newItem;
|
||||
}
|
||||
|
||||
private static IView<TItem>? DefaultItemTemplate() => null;
|
||||
|
||||
public void ChildVisibilityChanged(IView child)
|
||||
{
|
||||
var viewToForceRerender = child;
|
||||
while (viewToForceRerender.VisualParent != null && viewToForceRerender.VisualParent != this)
|
||||
{
|
||||
viewToForceRerender = viewToForceRerender.VisualParent;
|
||||
}
|
||||
|
||||
if (viewToForceRerender.VisualParent != this) return;
|
||||
|
||||
lock (_forceRerenderChildrenLock)
|
||||
{
|
||||
_forceRerenderChildren.Add(viewToForceRerender);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using TerminalUI.Models;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
public sealed partial class ListView<TDataContext, TItem> : View<ListView<TDataContext, TItem>, TDataContext>
|
||||
{
|
||||
private static readonly ArrayPool<ListViewItem<TItem, TDataContext>> ListViewItemPool = ArrayPool<ListViewItem<TItem, TDataContext>>.Shared;
|
||||
|
||||
@@ -187,7 +187,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
}
|
||||
|
||||
var renderStartIndex = _renderStartIndex;
|
||||
|
||||
|
||||
//TODO: This MUST be calculated and used
|
||||
var lastItemIndex = _listViewItemLength;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using TerminalUI.Models;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public partial class ListViewItem<T, TParentDataContext> : ContentView<T>
|
||||
public sealed partial class ListViewItem<T, TParentDataContext> : ContentView<ListViewItem<T, TParentDataContext>, T>
|
||||
{
|
||||
public ListView<TParentDataContext, T> Parent { get; }
|
||||
[Notify] private bool _isSelected;
|
||||
@@ -11,7 +11,7 @@ public partial class ListViewItem<T, TParentDataContext> : ContentView<T>
|
||||
public ListViewItem(ListView<TParentDataContext, T> parent)
|
||||
{
|
||||
Parent = parent;
|
||||
|
||||
|
||||
RerenderProperties.Add(nameof(IsSelected));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public partial class Rectangle<T> : View<T>, IDisplayView
|
||||
public sealed partial class Rectangle<T> : View<Rectangle<T>, T>, IDisplayView
|
||||
{
|
||||
private record RenderState(
|
||||
Position Position,
|
||||
|
||||
@@ -4,7 +4,7 @@ using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public partial class StackPanel<T> : ChildContainerView<T>, IVisibilityChangeHandler
|
||||
public sealed partial class StackPanel<T> : ChildContainerView<StackPanel<T>, T>, IVisibilityChangeHandler
|
||||
{
|
||||
private readonly List<IView> _forceRerenderChildren = new();
|
||||
private readonly object _forceRerenderChildrenLock = new();
|
||||
@@ -41,7 +41,6 @@ public partial class StackPanel<T> : ChildContainerView<T>, IVisibilityChangeHan
|
||||
|
||||
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
var delta = 0;
|
||||
var neededRerender = false;
|
||||
IReadOnlyList<IView> forceRerenderChildren;
|
||||
lock (_forceRerenderChildrenLock)
|
||||
@@ -50,17 +49,21 @@ public partial class StackPanel<T> : ChildContainerView<T>, IVisibilityChangeHan
|
||||
_forceRerenderChildren.Clear();
|
||||
}
|
||||
|
||||
var delta = 0;
|
||||
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
|
||||
? position with {Y = position.Y + delta}
|
||||
: position with {X = position.X + delta};
|
||||
|
||||
childSize = Orientation == Orientation.Vertical
|
||||
? childSize with {Width = size.Width}
|
||||
: childSize with {Height = size.Height};
|
||||
|
||||
var endX = position.X + size.Width;
|
||||
var endY = position.Y + size.Height;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ using TerminalUI.Traits;
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
[DebuggerDisplay("Text = {Text}")]
|
||||
public partial class TextBlock<T> : View<T>, IDisplayView
|
||||
public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
|
||||
{
|
||||
private record RenderState(
|
||||
Position Position,
|
||||
@@ -59,7 +59,7 @@ public partial class TextBlock<T> : View<T>, IDisplayView
|
||||
Text,
|
||||
foreground,
|
||||
background);
|
||||
|
||||
|
||||
if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false;
|
||||
|
||||
_lastRenderState = renderState;
|
||||
|
||||
@@ -10,7 +10,7 @@ using TerminalUI.Traits;
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
[DebuggerDisplay("Text = {Text}")]
|
||||
public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
public sealed partial class TextBox<T> : View<TextBox<T>, T>, IFocusable, IDisplayView
|
||||
{
|
||||
private record RenderState(
|
||||
string? Text,
|
||||
@@ -20,7 +20,6 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
IColor? BackgroundColor
|
||||
);
|
||||
|
||||
private readonly List<Action<TextBox<T>, GeneralKeyEventArgs>> _keyHandlers = new();
|
||||
private readonly List<Action<TextBox<T>, string>> _textHandlers = new();
|
||||
|
||||
private RenderState? _lastRenderState;
|
||||
@@ -31,6 +30,7 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
private Position _relativeCursorPosition = new(0, 0);
|
||||
|
||||
[Notify] private bool _multiLine;
|
||||
[Notify] private char? _passwordChar;
|
||||
public bool SetKeyHandledIfKnown { get; set; }
|
||||
|
||||
public string Text
|
||||
@@ -55,6 +55,7 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
_textLines = _text.Split(Environment.NewLine).ToList();
|
||||
RerenderProperties.Add(nameof(Text));
|
||||
RerenderProperties.Add(nameof(MultiLine));
|
||||
RerenderProperties.Add(nameof(PasswordChar));
|
||||
|
||||
((INotifyPropertyChanged) this).PropertyChanged += OnPropertyChangedEventHandler;
|
||||
}
|
||||
@@ -115,12 +116,41 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
}
|
||||
|
||||
RenderEmpty(renderContext, position, size);
|
||||
RenderText(_textLines, driver, position, size);
|
||||
|
||||
if (PasswordChar is { } passwordChar && !char.IsControl(passwordChar))
|
||||
{
|
||||
for (var i = 0; i < _textLines.Count; i++)
|
||||
{
|
||||
var pos = position with {Y = position.Y + i};
|
||||
RenderPasswordTextLine(_textLines[i], passwordChar, driver, pos, size);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderText(_textLines, driver, position, size);
|
||||
}
|
||||
|
||||
_cursorPosition = position + _relativeCursorPosition;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RenderPasswordTextLine(
|
||||
string sourceText,
|
||||
char passwordChar,
|
||||
IConsoleDriver driver,
|
||||
Position position,
|
||||
Size size)
|
||||
{
|
||||
Span<char> text = stackalloc char[sourceText.Length];
|
||||
for (var j = 0; j < text.Length; j++)
|
||||
{
|
||||
text[j] = passwordChar;
|
||||
}
|
||||
|
||||
RenderText(text, driver, position, size);
|
||||
}
|
||||
|
||||
private bool NeedsRerender(RenderState renderState)
|
||||
=> _lastRenderState is null || _lastRenderState != renderState;
|
||||
|
||||
@@ -136,13 +166,22 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
consoleDriver.SetCursorPosition(_cursorPosition.Value);
|
||||
}
|
||||
|
||||
public void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
|
||||
public override void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
|
||||
{
|
||||
HandleKeyInputInternal(keyEventArgs);
|
||||
if (keyEventArgs.Handled)
|
||||
{
|
||||
ApplicationContext?.RenderEngine.RequestRerender(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
var view = VisualParent;
|
||||
while (view != null && !keyEventArgs.Handled)
|
||||
{
|
||||
view.HandleKeyInput(keyEventArgs);
|
||||
view = view.VisualParent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleKeyInputInternal(GeneralKeyEventArgs keyEventArgs)
|
||||
@@ -158,11 +197,14 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
if (!known && HandleNavigation(keyEventArgs, out known))
|
||||
return;
|
||||
|
||||
if (!known && ProcessKeyHandlers(keyEventArgs))
|
||||
return;
|
||||
if (!known)
|
||||
{
|
||||
ProcessKeyHandlers(keyEventArgs);
|
||||
if (keyEventArgs.Handled) return;
|
||||
}
|
||||
|
||||
if (!known
|
||||
&& keyEventArgs.KeyChar != '\0'
|
||||
&& !char.IsControl(keyEventArgs.KeyChar)
|
||||
&& keyEventArgs.KeyChar.ToString() is {Length: 1} keyString)
|
||||
{
|
||||
var y = _relativeCursorPosition.Y;
|
||||
@@ -172,6 +214,12 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
|
||||
keyEventArgs.Handled = true;
|
||||
UpdateTextField();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!known)
|
||||
{
|
||||
ProcessParentKeyHandlers(keyEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,29 +342,6 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ProcessKeyHandlers(GeneralKeyEventArgs keyEventArgs)
|
||||
{
|
||||
foreach (var keyHandler in _keyHandlers)
|
||||
{
|
||||
keyHandler(this, keyEventArgs);
|
||||
if (keyEventArgs.Handled) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_keyHandlers.Clear();
|
||||
}
|
||||
|
||||
public TextBox<T> WithKeyHandler(Action<TextBox<T>, GeneralKeyEventArgs> keyHandler)
|
||||
{
|
||||
_keyHandlers.Add(keyHandler);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TextBox<T> WithTextHandler(Action<TextBox<T>, string> textChanged)
|
||||
{
|
||||
_textHandlers.Add(textChanged);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GeneralInputKey;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using TerminalUI.Color;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
@@ -12,9 +13,10 @@ namespace TerminalUI.Controls;
|
||||
|
||||
public delegate string TextTransformer(string text, Position position, Size size);
|
||||
|
||||
public abstract partial class View<T> : IView<T>
|
||||
public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : View<TConcrete, T>
|
||||
{
|
||||
private readonly List<IDisposable> _disposables = new();
|
||||
private readonly ReadOnlyObservableCollection<IView> _readOnlyVisualChildren;
|
||||
[Notify] private T? _dataContext;
|
||||
[Notify] private int? _minWidth;
|
||||
[Notify] private int? _maxWidth;
|
||||
@@ -33,7 +35,9 @@ public abstract partial class View<T> : IView<T>
|
||||
[Notify] private bool _attached;
|
||||
[Notify] private IView? _visualParent;
|
||||
|
||||
protected List<Action<TConcrete, GeneralKeyEventArgs>> KeyHandlers { get; } = new();
|
||||
protected ObservableCollection<IView> VisualChildren { get; } = new();
|
||||
ReadOnlyObservableCollection<IView> IView.VisualChildren => _readOnlyVisualChildren;
|
||||
|
||||
public List<object> Extensions { get; } = new();
|
||||
public RenderMethod RenderMethod { get; set; }
|
||||
@@ -43,6 +47,7 @@ public abstract partial class View<T> : IView<T>
|
||||
protected View()
|
||||
{
|
||||
RenderMethod = DefaultRenderer;
|
||||
_readOnlyVisualChildren = new ReadOnlyObservableCollection<IView>(VisualChildren);
|
||||
|
||||
RerenderProperties.Add(nameof(Width));
|
||||
RerenderProperties.Add(nameof(MinWidth));
|
||||
@@ -245,6 +250,27 @@ public abstract partial class View<T> : IView<T>
|
||||
}
|
||||
}
|
||||
|
||||
protected void RenderText(
|
||||
in ReadOnlySpan<char> text,
|
||||
IConsoleDriver driver,
|
||||
Position position,
|
||||
Size size)
|
||||
{
|
||||
for (var i = 0; i < size.Height; i++)
|
||||
{
|
||||
var currentPosition = position with {Y = position.Y + i};
|
||||
var finalText = text;
|
||||
|
||||
if (finalText.Length > size.Width)
|
||||
{
|
||||
finalText = finalText[..size.Width];
|
||||
}
|
||||
|
||||
driver.SetCursorPosition(currentPosition);
|
||||
driver.Write(finalText);
|
||||
}
|
||||
}
|
||||
|
||||
protected void RenderText(
|
||||
char content,
|
||||
IConsoleDriver driver,
|
||||
@@ -367,7 +393,39 @@ public abstract partial class View<T> : IView<T>
|
||||
arrayPool.Return(disposables, true);
|
||||
|
||||
_disposables.Clear();
|
||||
KeyHandlers.Clear();
|
||||
|
||||
Disposed?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
|
||||
{
|
||||
ProcessKeyHandlers(keyEventArgs);
|
||||
ProcessParentKeyHandlers(keyEventArgs);
|
||||
}
|
||||
|
||||
|
||||
protected void ProcessKeyHandlers(GeneralKeyEventArgs keyEventArgs)
|
||||
{
|
||||
foreach (var keyHandler in KeyHandlers)
|
||||
{
|
||||
keyHandler((TConcrete)this, keyEventArgs);
|
||||
if (keyEventArgs.Handled) return;
|
||||
}
|
||||
}
|
||||
|
||||
protected void ProcessParentKeyHandlers(GeneralKeyEventArgs keyEventArgs)
|
||||
{
|
||||
if (VisualParent is { } parent)
|
||||
{
|
||||
parent.HandleKeyInput(keyEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
public TConcrete WithKeyHandler(Action<TConcrete, GeneralKeyEventArgs> keyHandler)
|
||||
{
|
||||
KeyHandlers.Add(keyHandler);
|
||||
return (TConcrete)this;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,20 @@ public class FocusManager : IFocusManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_focused is not null && !_focused.IsVisible)
|
||||
if (_focused is not null)
|
||||
{
|
||||
_focused = null;
|
||||
var visible = _focused.IsVisible;
|
||||
var parent = _focused.VisualParent;
|
||||
while (parent != null)
|
||||
{
|
||||
visible &= parent.IsVisible;
|
||||
parent = parent.VisualParent;
|
||||
}
|
||||
|
||||
if (!visible)
|
||||
{
|
||||
_focused = null;
|
||||
}
|
||||
}
|
||||
|
||||
return _focused;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\DeclarativeProperty\DeclarativeProperty.csproj" />
|
||||
<ProjectReference Include="..\ObservableComputations.Extensions\ObservableComputations.Extensions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user