Add project files.

This commit is contained in:
Ankitkumar Satapara
2026-04-17 22:31:58 +05:30
commit 21aaef6776
473 changed files with 50152 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Interactivity
{
internal readonly struct DirectionalFocusNavigator<TElement>
where TElement : UIElement, IKeyboardFocusTarget<TElement>
{
private readonly IEnumerable<IKeyboardFocusTarget<TElement>> _availableTargets;
public DirectionalFocusNavigator(IEnumerable<IKeyboardFocusTarget<TElement>> availableTargets)
{
_availableTargets = availableTargets;
}
public readonly IKeyboardFocusTarget<TElement>? FindNextFocusTarget(IKeyboardFocusTarget<TElement> currentContainer, TraversalRequest request)
{
var currentContainerBounds = currentContainer.Bounds;
IEnumerable<IKeyboardFocusTarget<TElement>> candidates = request.FocusNavigationDirection switch
{
FocusNavigationDirection.Left => _availableTargets.Where(c => c.Bounds.Left < currentContainerBounds.Left),
FocusNavigationDirection.Right => _availableTargets.Where(c => c.Bounds.Left > currentContainerBounds.Left),
FocusNavigationDirection.Up => _availableTargets.Where(c => c.Bounds.Top < currentContainerBounds.Top),
FocusNavigationDirection.Down => _availableTargets.Where(c => c.Bounds.Top > currentContainerBounds.Top),
FocusNavigationDirection.Previous => FindCandidatesLinearly(currentContainer, request),
FocusNavigationDirection.Next => FindCandidatesLinearly(currentContainer, request),
FocusNavigationDirection.First => FindCandidatesLinearly(currentContainer, request),
FocusNavigationDirection.Last => FindCandidatesLinearly(currentContainer, request),
_ => Array.Empty<IKeyboardFocusTarget<TElement>>()
};
// Wrap focus if no candidates found in the current direction
if (!candidates.Any())
{
candidates = request.FocusNavigationDirection switch
{
FocusNavigationDirection.Left => _availableTargets.OrderByDescending(c => c.Bounds.Left).Take(1),
FocusNavigationDirection.Right => _availableTargets.OrderBy(c => c.Bounds.Left).Take(1),
FocusNavigationDirection.Up => _availableTargets.OrderByDescending(c => c.Bounds.Top).Take(1),
FocusNavigationDirection.Down => _availableTargets.OrderBy(c => c.Bounds.Top).Take(1),
_ => Array.Empty<IKeyboardFocusTarget<TElement>>()
};
request.Wrapped = true;
}
IKeyboardFocusTarget<TElement>? best = null;
double minDistanceSquared = double.MaxValue;
foreach (var candidate in candidates)
{
double distanceSquared = (candidate.Bounds.TopLeft - currentContainerBounds.TopLeft).LengthSquared;
if (distanceSquared < minDistanceSquared)
{
minDistanceSquared = distanceSquared;
best = candidate;
}
}
return best;
}
private IKeyboardFocusTarget<TElement>[] FindCandidatesLinearly(IKeyboardFocusTarget<TElement> currentContainer, TraversalRequest request)
{
var nextTarget = new LinearFocusNavigator<TElement>(_availableTargets).FindNextFocusTarget(currentContainer, request);
return nextTarget is null ? Array.Empty<IKeyboardFocusTarget<TElement>>() : new[] { nextTarget };
}
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Interactivity
{
/// <summary>
/// Represents a unique identifier for a keyboard navigation layer.
/// </summary>
public class KeyboardNavigationLayerId
{
public static readonly KeyboardNavigationLayerId Nodes = new KeyboardNavigationLayerId();
public static readonly KeyboardNavigationLayerId Connections = new KeyboardNavigationLayerId();
public static readonly KeyboardNavigationLayerId Decorators = new KeyboardNavigationLayerId();
}
/// <summary>
/// Represents a group of keyboard navigation layers that can be activated and navigated through.
/// </summary>
public interface IKeyboardNavigationLayerGroup : IReadOnlyCollection<IKeyboardNavigationLayer>
{
/// <summary>
/// The current active keyboard navigation layer in the group, if any.
/// </summary>
IKeyboardNavigationLayer? ActiveNavigationLayer { get; }
/// <summary>
/// Event that is raised when the active keyboard navigation layer changes.
/// </summary>
event Action<KeyboardNavigationLayerId>? ActiveNavigationLayerChanged;
/// <summary>
/// Activates the next keyboard navigation layer in the group, allowing focus to be restored to the last focused element in that layer.
/// </summary>
/// <returns>Returns true if the navigation layer was activated, false otherwise.</returns>
bool ActivateNextNavigationLayer();
/// <summary>
/// Activates the previous keyboard navigation layer in the group, allowing focus to be restored to the last focused element in that layer.
/// </summary>
/// <returns>Returns true if the navigation layer was activated, false otherwise.</returns>
bool ActivatePreviousNavigationLayer();
/// <summary>
/// Registers a new keyboard navigation layer to the group, allowing it to handle focus movement and restoration.
/// </summary>
/// <param name="layer">The navigation layer.</param>
/// <returns></returns>
bool RegisterNavigationLayer(IKeyboardNavigationLayer layer);
/// <summary>
/// Removes the specified keyboard navigation layer from the group.
/// </summary>
/// <param name="layerId">The navigation layer id.</param>
/// <returns>Returns true if the layer was removed, false otherwise.</returns>
bool RemoveNavigationLayer(KeyboardNavigationLayerId layerId);
/// <summary>
/// Activates the specified keyboard navigation layer, making it the active layer for focus management.
/// </summary>
/// <param name="layerId">The navigation layer id to activate.</param>
/// <returns>Returns true if the navigation layer was activated, false otherwise.</returns>
bool ActivateNavigationLayer(KeyboardNavigationLayerId layerId);
}
/// <summary>
/// Represents a layer of keyboard navigation that can handle focus movement and restoration.
/// </summary>
public interface IKeyboardNavigationLayer
{
/// <summary>
/// Gets the unique identifier for this keyboard navigation layer.
/// </summary>
KeyboardNavigationLayerId Id { get; }
/// <summary>
/// Gets the last focused element within this layer, if any.
/// </summary>
IKeyboardFocusTarget<UIElement>? LastFocusedElement { get; }
/// <summary>
/// Attempts to move focus within this layer based on the provided traversal request.
/// </summary>
/// <param name="request">The traversal request.</param>
/// <returns>Returns true if the focus was moved, false otherwise.</returns>
bool TryMoveFocus(TraversalRequest request);
/// <summary>
/// Attempts to restore focus to the last focused element within this layer.
/// </summary>
/// <returns>Returns true if the focus was restored, false otherwise.</returns>
bool TryRestoreFocus();
/// <summary>
/// Called when the layer is activated, allowing for any necessary setup or focus management.
/// </summary>
void OnActivated();
/// <summary>
/// Called when the layer is deactivated, allowing for any necessary cleanup or focus management.
/// </summary>
void OnDeactivated();
}
/// <summary>
/// Represents a target for keyboard focus within a specific layer, providing bounds and the associated UI element.
/// </summary>
/// <typeparam name="TElement">The associated UI element.</typeparam>
public interface IKeyboardFocusTarget<out TElement>
where TElement : UIElement
{
/// <summary>
/// Gets the bounds of the focus target within the layer.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Gets the associated UI element for this focus target.
/// </summary>
TElement Element { get; }
}
}

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Interactivity
{
internal readonly struct LinearFocusNavigator<TElement>
where TElement : UIElement, IKeyboardFocusTarget<TElement>
{
private enum LinearNavigationDirection
{
First,
Last,
Forward,
Backward
}
private readonly IEnumerable<IKeyboardFocusTarget<TElement>> _availableTargets;
public LinearFocusNavigator(IEnumerable<IKeyboardFocusTarget<TElement>> availableTargets)
{
_availableTargets = availableTargets;
}
public readonly IKeyboardFocusTarget<TElement>? FindNextFocusTarget(IKeyboardFocusTarget<TElement> currentContainer, TraversalRequest request)
{
var direction = IsBackward(request.FocusNavigationDirection) ? LinearNavigationDirection.Backward
: IsForward(request.FocusNavigationDirection) ? LinearNavigationDirection.Forward
: request.FocusNavigationDirection == FocusNavigationDirection.First ? LinearNavigationDirection.First : LinearNavigationDirection.Last;
var availableTargets = _availableTargets as List<IKeyboardFocusTarget<TElement>> ?? _availableTargets.ToList();
int currentIndex = availableTargets.IndexOf(currentContainer);
IKeyboardFocusTarget<TElement>? candidate = direction switch
{
LinearNavigationDirection.Forward when currentIndex >= 0 && currentIndex + 1 < availableTargets.Count => availableTargets[currentIndex + 1],
LinearNavigationDirection.Backward when currentIndex > 0 => availableTargets[currentIndex - 1],
LinearNavigationDirection.First when availableTargets.Count > 0 => availableTargets[0],
LinearNavigationDirection.Last when availableTargets.Count > 0 => availableTargets[availableTargets.Count - 1],
_ => null
};
// Wrap focus if no candidates found in the current direction
if (candidate is null)
{
candidate = direction switch
{
LinearNavigationDirection.Forward when availableTargets.Count > 0 => availableTargets[0],
LinearNavigationDirection.Backward when availableTargets.Count > 0 => availableTargets[availableTargets.Count - 1],
_ => null
};
request.Wrapped = candidate != null;
}
return candidate;
}
private static bool IsForward(FocusNavigationDirection dir)
{
return dir == FocusNavigationDirection.Right || dir == FocusNavigationDirection.Up || dir == FocusNavigationDirection.Next;
}
private static bool IsBackward(FocusNavigationDirection dir)
{
return dir == FocusNavigationDirection.Left || dir == FocusNavigationDirection.Down || dir == FocusNavigationDirection.Previous;
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Windows.Input;
using System.Windows;
namespace Nodify.Interactivity
{
internal class StatefulFocusNavigator<TElement>
where TElement : UIElement, IKeyboardFocusTarget<TElement>
{
public delegate bool FindNextFocusTargetDelegate(TElement? currentElement, TraversalRequest request, out TElement? elementToFocus);
private readonly WeakReference<TElement?> _previousFocusedElement = new WeakReference<TElement?>(null);
private readonly WeakReference<TElement?> _lastFocusedElement = new WeakReference<TElement?>(null);
private FocusNavigationDirection? _previousFocusNavigationDirection;
private readonly Action<IKeyboardFocusTarget<TElement>> _onFocus;
public TElement? LastFocusedElement => _lastFocusedElement.TryGetTarget(out var target) ? target : null;
public StatefulFocusNavigator(Action<IKeyboardFocusTarget<TElement>> onFocus)
{
_onFocus = onFocus;
}
public bool TryMoveFocus(TraversalRequest request, FindNextFocusTargetDelegate findNext)
{
var currentTarget = Keyboard.FocusedElement as TElement;
// If the request is in the opposite direction of the last focus navigation, try to restore the previous focused container
if (_previousFocusedElement.TryGetTarget(out var prevTarget)
&& _previousFocusNavigationDirection.HasValue
&& request.FocusNavigationDirection.IsOppositeOf(_previousFocusNavigationDirection.Value)
&& prevTarget!.Focus())
{
_previousFocusNavigationDirection = request.FocusNavigationDirection;
_previousFocusedElement.SetTarget(currentTarget);
_lastFocusedElement.SetTarget(prevTarget);
_onFocus(prevTarget);
return true;
}
else if (findNext(currentTarget, request, out var nextTarget) && nextTarget!.Element.Focus())
{
_previousFocusNavigationDirection = request.FocusNavigationDirection;
_previousFocusedElement.SetTarget(currentTarget);
_lastFocusedElement.SetTarget(nextTarget);
_onFocus(nextTarget);
return true;
}
return false;
}
public bool TryRestoreFocus()
{
if (_lastFocusedElement.TryGetTarget(out var lastTarget))
{
if (lastTarget!.IsKeyboardFocused)
{
return true;
}
if (lastTarget.Focus())
{
_onFocus.Invoke(lastTarget);
return true;
}
}
return false;
}
}
}