Focus next/previous input element with Tab
This commit is contained in:
@@ -301,13 +301,15 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
|
||||
var x = positionExtension?.Column ?? 0;
|
||||
var y = positionExtension?.Row ?? 0;
|
||||
|
||||
if (x > columns)
|
||||
Debug.Assert(x < columns, "Child requests column outside of grid");
|
||||
if (x >= columns)
|
||||
{
|
||||
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y);
|
||||
x = 0;
|
||||
}
|
||||
|
||||
if (y > rows)
|
||||
Debug.Assert(y < rows, "Child requests row outside of grid");
|
||||
if (y >= rows)
|
||||
{
|
||||
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y);
|
||||
y = 0;
|
||||
@@ -339,15 +341,27 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
|
||||
Span<int> allWidth = stackalloc int[columns * rows];
|
||||
Span<int> allHeight = stackalloc int[columns * rows];
|
||||
|
||||
//Store the largest width and height for a cell
|
||||
foreach (var child in Children)
|
||||
{
|
||||
var childSize = child.GetRequestedSize();
|
||||
var (x, y) = GetViewColumnAndRow(child, columns, rows);
|
||||
|
||||
allWidth.SetToMatrix(childSize.Width, x, y, columns);
|
||||
allHeight.SetToMatrix(childSize.Height, x, y, columns);
|
||||
var currentWidth = allWidth.GetFromMatrix(x, y, columns);
|
||||
var currentHeight = allHeight.GetFromMatrix(x, y, columns);
|
||||
|
||||
if (currentWidth < childSize.Width)
|
||||
{
|
||||
allWidth.SetToMatrix(childSize.Width, x, y, columns);
|
||||
}
|
||||
|
||||
if (currentHeight < childSize.Height)
|
||||
{
|
||||
allHeight.SetToMatrix(childSize.Height, x, y, columns);
|
||||
}
|
||||
}
|
||||
|
||||
//Calculate the width and height for each column and row
|
||||
Span<int> columnWidths = stackalloc int[columns];
|
||||
Span<int> rowHeights = stackalloc int[rows];
|
||||
|
||||
@@ -407,6 +421,7 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
|
||||
usedHeight += rowHeights[i];
|
||||
}
|
||||
|
||||
//Calculate the width and height for each column and row with star value if size of the current grid is given
|
||||
if (size.IsSome)
|
||||
{
|
||||
var widthLeft = size.Value.Width - usedWidth;
|
||||
|
||||
@@ -31,6 +31,7 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection
|
||||
RenderMethod RenderMethod { get; set; }
|
||||
IView? VisualParent { get; set; }
|
||||
ReadOnlyObservableCollection<IView> VisualChildren { get; }
|
||||
bool IsFocusBoundary { get; set; }
|
||||
event Action<IView> Disposed;
|
||||
|
||||
Size GetRequestedSize();
|
||||
|
||||
@@ -38,15 +38,21 @@ public sealed partial class ItemsControl<TDataContext, TItem>
|
||||
|
||||
if (_itemsSource is ObservableCollection<TItem> observableDeclarative)
|
||||
{
|
||||
var consumer = new OcConsumer();
|
||||
_children = observableDeclarative
|
||||
.Selecting(i => CreateItem(i))
|
||||
.OfTypeComputing<IView<TItem>>();
|
||||
.OfTypeComputing<IView<TItem>>()
|
||||
.For(consumer);
|
||||
_itemsDisposables.Add(consumer);
|
||||
}
|
||||
else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative)
|
||||
{
|
||||
var consumer = new OcConsumer();
|
||||
_children = readOnlyObservableDeclarative
|
||||
.Selecting(i => CreateItem(i))
|
||||
.OfTypeComputing<IView<TItem>>();
|
||||
.OfTypeComputing<IView<TItem>>()
|
||||
.For(consumer);
|
||||
_itemsDisposables.Add(consumer);
|
||||
}
|
||||
else if (_itemsSource is ICollection<TItem> collection)
|
||||
_children = collection.Select(CreateItem).OfType<IView<TItem>>().ToList();
|
||||
|
||||
@@ -34,6 +34,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
|
||||
[Notify] private IApplicationContext? _applicationContext;
|
||||
[Notify] private bool _attached;
|
||||
[Notify] private IView? _visualParent;
|
||||
[Notify] private bool _isFocusBoundary;
|
||||
|
||||
protected List<Action<TConcrete, GeneralKeyEventArgs>> KeyHandlers { get; } = new();
|
||||
protected ObservableCollection<IView> VisualChildren { get; } = new();
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using TerminalUI.Traits;
|
||||
using System.Diagnostics.Contracts;
|
||||
using GeneralInputKey;
|
||||
using TerminalUI.Controls;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public class FocusManager : IFocusManager
|
||||
{
|
||||
private readonly IRenderEngine _renderEngine;
|
||||
private IFocusable? _focused;
|
||||
|
||||
public IFocusable? Focused
|
||||
@@ -32,6 +36,11 @@ public class FocusManager : IFocusManager
|
||||
private set => _focused = value;
|
||||
}
|
||||
|
||||
public FocusManager(IRenderEngine renderEngine)
|
||||
{
|
||||
_renderEngine = renderEngine;
|
||||
}
|
||||
|
||||
public void SetFocus(IFocusable focusable) => Focused = focusable;
|
||||
|
||||
public void UnFocus(IFocusable focusable)
|
||||
@@ -39,4 +48,95 @@ public class FocusManager : IFocusManager
|
||||
if (Focused == focusable)
|
||||
Focused = null;
|
||||
}
|
||||
|
||||
public void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
|
||||
{
|
||||
if (keyEventArgs.Handled || Focused is null) return;
|
||||
|
||||
if (keyEventArgs.Key == Keys.Tab && keyEventArgs.SpecialKeysStatus.IsShiftPressed)
|
||||
{
|
||||
FocusElement(
|
||||
(views, from) => views.TakeWhile(x => x != from).Reverse(),
|
||||
c => c.Reverse()
|
||||
);
|
||||
keyEventArgs.Handled = true;
|
||||
}
|
||||
else if (keyEventArgs.Key == Keys.Tab)
|
||||
{
|
||||
FocusElement(
|
||||
(views, from) => views.SkipWhile(x => x != from).Skip(1),
|
||||
c => c
|
||||
);
|
||||
keyEventArgs.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void FocusElement(
|
||||
Func<IEnumerable<IView>, IView, IEnumerable<IView>> fromChildSelector,
|
||||
Func<IEnumerable<IView>, IEnumerable<IView>> childSelector
|
||||
)
|
||||
{
|
||||
if (Focused is null) return;
|
||||
|
||||
var element = FindElement(Focused,
|
||||
Focused,
|
||||
fromChildSelector
|
||||
);
|
||||
|
||||
if (element is null)
|
||||
{
|
||||
var topParent = FindLastFocusParent(Focused);
|
||||
element = FindElement(
|
||||
topParent,
|
||||
Focused,
|
||||
fromChildSelector,
|
||||
childSelector
|
||||
);
|
||||
}
|
||||
|
||||
if (element is null) return;
|
||||
|
||||
_renderEngine.RequestRerender(element);
|
||||
_renderEngine.RequestRerender(Focused);
|
||||
Focused = element;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
private static IFocusable? FindElement(IView view, IView original,
|
||||
Func<IEnumerable<IView>, IView, IEnumerable<IView>> fromChildSelector,
|
||||
Func<IEnumerable<IView>, IEnumerable<IView>>? childSelector = null,
|
||||
IView? from = null)
|
||||
{
|
||||
if (!view.IsVisible) return null;
|
||||
childSelector ??= views => views;
|
||||
|
||||
if (view != original && view is IFocusable focusable)
|
||||
return focusable;
|
||||
|
||||
var visualChildren = from != null && view.VisualChildren.Contains(from)
|
||||
? fromChildSelector(view.VisualChildren, from)
|
||||
: childSelector(view.VisualChildren);
|
||||
|
||||
foreach (var viewVisualChild in visualChildren)
|
||||
{
|
||||
var result = FindElement(viewVisualChild, original, fromChildSelector, childSelector);
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (view.VisualParent is null || view.IsFocusBoundary)
|
||||
return null;
|
||||
|
||||
return FindElement(view.VisualParent, original, fromChildSelector, childSelector, view);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
private static IView FindLastFocusParent(IView view)
|
||||
{
|
||||
if (view.IsFocusBoundary || view.VisualParent is null) return view;
|
||||
return FindLastFocusParent(view.VisualParent);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using TerminalUI.Traits;
|
||||
using GeneralInputKey;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
@@ -7,4 +8,5 @@ public interface IFocusManager
|
||||
void SetFocus(IFocusable focusable);
|
||||
void UnFocus(IFocusable focusable);
|
||||
IFocusable? Focused { get; }
|
||||
void HandleKeyInput(GeneralKeyEventArgs keyEventArgs);
|
||||
}
|
||||
Reference in New Issue
Block a user