diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs
index e6866b8142..78bbd836be 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -219,13 +219,16 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
- PseudoClasses.Add(":pressed");
- e.Device.Capture(this);
- e.Handled = true;
-
- if (ClickMode == ClickMode.Press)
+ if (e.MouseButton == MouseButton.Left)
{
- RaiseClickEvent();
+ PseudoClasses.Add(":pressed");
+ e.Device.Capture(this);
+ e.Handled = true;
+
+ if (ClickMode == ClickMode.Press)
+ {
+ RaiseClickEvent();
+ }
}
}
@@ -234,13 +237,16 @@ namespace Avalonia.Controls
{
base.OnPointerReleased(e);
- e.Device.Capture(null);
- PseudoClasses.Remove(":pressed");
- e.Handled = true;
-
- if (ClickMode == ClickMode.Release && new Rect(Bounds.Size).Contains(e.GetPosition(this)))
+ if (e.MouseButton == MouseButton.Left)
{
- RaiseClickEvent();
+ e.Device.Capture(null);
+ PseudoClasses.Remove(":pressed");
+ e.Handled = true;
+
+ if (ClickMode == ClickMode.Release && new Rect(Bounds.Size).Contains(e.GetPosition(this)))
+ {
+ RaiseClickEvent();
+ }
}
}
diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs
index b966d09b1f..5d1b9a1462 100644
--- a/src/Avalonia.Controls/TreeView.cs
+++ b/src/Avalonia.Controls/TreeView.cs
@@ -16,7 +16,7 @@ namespace Avalonia.Controls
///
/// Displays a hierachical tree of data.
///
- public class TreeView : ItemsControl
+ public class TreeView : ItemsControl, ICustomKeyboardNavigation
{
///
/// Defines the property.
@@ -90,6 +90,26 @@ namespace Avalonia.Controls
}
}
+ (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction)
+ {
+ if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
+ {
+ if (!this.IsVisualAncestorOf(element))
+ {
+ IControl result = _selectedItem != null ?
+ ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) :
+ ItemContainerGenerator.ContainerFromIndex(0);
+ return (true, result);
+ }
+ else
+ {
+ return (true, null);
+ }
+ }
+
+ return (false, null);
+ }
+
///
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj
index e9e74e24fe..0411cf77a5 100644
--- a/src/Avalonia.Input/Avalonia.Input.csproj
+++ b/src/Avalonia.Input/Avalonia.Input.csproj
@@ -37,5 +37,8 @@
Properties\SharedAssemblyInfo.cs
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs
index e5cc5a8557..102da6efc4 100644
--- a/src/Avalonia.Input/FocusManager.cs
+++ b/src/Avalonia.Input/FocusManager.cs
@@ -176,9 +176,10 @@ namespace Avalonia.Input
/// The event args.
private void OnPreviewPointerPressed(object sender, RoutedEventArgs e)
{
- if (sender == e.Source)
+ var ev = (PointerPressedEventArgs)e;
+
+ if (sender == e.Source && ev.MouseButton == MouseButton.Left)
{
- var ev = (PointerPressedEventArgs)e;
var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement);
if (element == null || !CanFocus(element))
diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Input/ICustomKeyboardNavigation.cs
new file mode 100644
index 0000000000..de5f98e04b
--- /dev/null
+++ b/src/Avalonia.Input/ICustomKeyboardNavigation.cs
@@ -0,0 +1,15 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Input
+{
+ ///
+ /// Designates a control as handling its own keyboard navigation.
+ ///
+ public interface ICustomKeyboardNavigation
+ {
+ (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction);
+ }
+}
diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs
index 57da49fa03..bf2b61d08b 100644
--- a/src/Avalonia.Input/KeyboardNavigationHandler.cs
+++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs
@@ -2,7 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Linq;
using Avalonia.Input.Navigation;
+using Avalonia.VisualTree;
namespace Avalonia.Input
{
@@ -52,6 +54,31 @@ namespace Avalonia.Input
{
Contract.Requires(element != null);
+ var customHandler = element.GetSelfAndVisualAncestors()
+ .OfType()
+ .FirstOrDefault();
+
+ if (customHandler != null)
+ {
+ var (handled, next) = customHandler.GetNext(element, direction);
+
+ if (handled)
+ {
+ if (next != null)
+ {
+ return next;
+ }
+ else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
+ {
+ return TabNavigation.GetNextInTabOrder((IInputElement)customHandler, direction, true);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
return TabNavigation.GetNextInTabOrder(element, direction);
diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs
index a88ed1e8aa..75cb3a39e8 100644
--- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs
+++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Input.Navigation
{
case KeyboardNavigationMode.Continue:
return GetNextInContainer(element, container, direction) ??
- GetFirstInNextContainer(element, direction);
+ GetFirstInNextContainer(element, element, direction);
case KeyboardNavigationMode.Cycle:
return GetNextInContainer(element, container, direction) ??
GetFocusableDescendant(container, direction);
@@ -173,10 +173,12 @@ namespace Avalonia.Input.Navigation
///
/// Gets the first item that should be focused in the next container.
///
+ /// The element being navigated away from.
/// The container.
/// The direction of the search.
/// The first element, or null if there are no more elements.
private static IInputElement GetFirstInNextContainer(
+ IInputElement element,
IInputElement container,
NavigationDirection direction)
{
@@ -200,6 +202,16 @@ namespace Avalonia.Input.Navigation
if (sibling != null)
{
+ if (sibling is ICustomKeyboardNavigation custom)
+ {
+ var (handled, customNext) = custom.GetNext(element, direction);
+
+ if (handled)
+ {
+ return customNext;
+ }
+ }
+
if (sibling.CanFocus())
{
next = sibling;
@@ -214,7 +226,7 @@ namespace Avalonia.Input.Navigation
if (next == null)
{
- next = GetFirstInNextContainer(parent, direction);
+ next = GetFirstInNextContainer(element, parent, direction);
}
}
else
diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs
index 6ba7ab1a0c..6e077e887f 100644
--- a/src/Avalonia.Input/Navigation/TabNavigation.cs
+++ b/src/Avalonia.Input/Navigation/TabNavigation.cs
@@ -18,13 +18,17 @@ namespace Avalonia.Input.Navigation
///
/// The element.
/// The tab direction. Must be Next or Previous.
+ ///
+ /// If true will not descend into to find next control.
+ ///
///
/// The next element in the specified direction, or null if
/// was the last in the requested direction.
///
public static IInputElement GetNextInTabOrder(
IInputElement element,
- NavigationDirection direction)
+ NavigationDirection direction,
+ bool outsideElement = false)
{
Contract.Requires(element != null);
Contract.Requires(
@@ -40,20 +44,20 @@ namespace Avalonia.Input.Navigation
switch (mode)
{
case KeyboardNavigationMode.Continue:
- return GetNextInContainer(element, container, direction) ??
- GetFirstInNextContainer(element, direction);
+ return GetNextInContainer(element, container, direction, outsideElement) ??
+ GetFirstInNextContainer(element, element, direction);
case KeyboardNavigationMode.Cycle:
- return GetNextInContainer(element, container, direction) ??
+ return GetNextInContainer(element, container, direction, outsideElement) ??
GetFocusableDescendant(container, direction);
case KeyboardNavigationMode.Contained:
- return GetNextInContainer(element, container, direction);
+ return GetNextInContainer(element, container, direction, outsideElement);
default:
- return GetFirstInNextContainer(container, direction);
+ return GetFirstInNextContainer(element, container, direction);
}
}
else
{
- return GetFocusableDescendants(element).FirstOrDefault();
+ return GetFocusableDescendants(element, direction).FirstOrDefault();
}
}
@@ -66,16 +70,17 @@ namespace Avalonia.Input.Navigation
private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction)
{
return direction == NavigationDirection.Next ?
- GetFocusableDescendants(container).FirstOrDefault() :
- GetFocusableDescendants(container).LastOrDefault();
+ GetFocusableDescendants(container, direction).FirstOrDefault() :
+ GetFocusableDescendants(container, direction).LastOrDefault();
}
///
/// Gets the focusable descendants of the specified element.
///
/// The element.
+ /// The tab direction. Must be Next or Previous.
/// The element's focusable descendants.
- private static IEnumerable GetFocusableDescendants(IInputElement element)
+ private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction)
{
var mode = KeyboardNavigation.GetTabNavigation((InputElement)element);
@@ -103,16 +108,25 @@ namespace Avalonia.Input.Navigation
foreach (var child in children)
{
- if (child.CanFocus())
+ var customNext = GetCustomNext(child, direction);
+
+ if (customNext.handled)
{
- yield return child;
+ yield return customNext.next;
}
-
- if (child.CanFocusDescendants())
+ else
{
- foreach (var descendant in GetFocusableDescendants(child))
+ if (child.CanFocus())
{
- yield return descendant;
+ yield return child;
+ }
+
+ if (child.CanFocusDescendants())
+ {
+ foreach (var descendant in GetFocusableDescendants(child, direction))
+ {
+ yield return descendant;
+ }
}
}
}
@@ -124,15 +138,19 @@ namespace Avalonia.Input.Navigation
/// The starting element/
/// The container.
/// The direction.
+ ///
+ /// If true will not descend into to find next control.
+ ///
/// The next element, or null if the element is the last.
private static IInputElement GetNextInContainer(
IInputElement element,
IInputElement container,
- NavigationDirection direction)
+ NavigationDirection direction,
+ bool outsideElement)
{
- if (direction == NavigationDirection.Next)
+ if (direction == NavigationDirection.Next && !outsideElement)
{
- var descendant = GetFocusableDescendants(element).FirstOrDefault();
+ var descendant = GetFocusableDescendants(element, direction).FirstOrDefault();
if (descendant != null)
{
@@ -167,7 +185,7 @@ namespace Avalonia.Input.Navigation
if (element != null && direction == NavigationDirection.Previous)
{
- var descendant = GetFocusableDescendants(element).LastOrDefault();
+ var descendant = GetFocusableDescendants(element, direction).LastOrDefault();
if (descendant != null)
{
@@ -184,10 +202,12 @@ namespace Avalonia.Input.Navigation
///
/// Gets the first item that should be focused in the next container.
///
+ /// The element being navigated away from.
/// The container.
/// The direction of the search.
/// The first element, or null if there are no more elements.
private static IInputElement GetFirstInNextContainer(
+ IInputElement element,
IInputElement container,
NavigationDirection direction)
{
@@ -210,6 +230,13 @@ namespace Avalonia.Input.Navigation
if (sibling != null)
{
+ var customNext = GetCustomNext(sibling, direction);
+
+ if (customNext.handled)
+ {
+ return customNext.next;
+ }
+
if (sibling.CanFocus())
{
next = sibling;
@@ -217,24 +244,34 @@ namespace Avalonia.Input.Navigation
else
{
next = direction == NavigationDirection.Next ?
- GetFocusableDescendants(sibling).FirstOrDefault() :
- GetFocusableDescendants(sibling).LastOrDefault();
+ GetFocusableDescendants(sibling, direction).FirstOrDefault() :
+ GetFocusableDescendants(sibling, direction).LastOrDefault();
}
}
if (next == null)
{
- next = GetFirstInNextContainer(parent, direction);
+ next = GetFirstInNextContainer(element, parent, direction);
}
}
else
{
next = direction == NavigationDirection.Next ?
- GetFocusableDescendants(container).FirstOrDefault() :
- GetFocusableDescendants(container).LastOrDefault();
+ GetFocusableDescendants(container, direction).FirstOrDefault() :
+ GetFocusableDescendants(container, direction).LastOrDefault();
}
return next;
}
+
+ private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction)
+ {
+ if (element is ICustomKeyboardNavigation custom)
+ {
+ return custom.GetNext(element, direction);
+ }
+
+ return (false, null);
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
index 52d36a33fa..44ef7192ff 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
@@ -315,6 +315,50 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1));
}
+ [Fact]
+ public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node()
+ {
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var focus = FocusManager.Instance;
+ var navigation = AvaloniaLocator.Current.GetService();
+ var data = CreateTestTreeData();
+
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = data,
+ DataTemplates = CreateNodeDataTemplate(),
+ };
+
+ var button = new Button();
+
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children = { target, button },
+ }
+ };
+
+ ApplyTemplates(target);
+
+ var item = data[0].Children[0];
+ var node = target.ItemContainerGenerator.Index.ContainerFromItem(item);
+ Assert.NotNull(node);
+
+ target.SelectedItem = item;
+ node.Focus();
+ Assert.Same(node, focus.Current);
+
+ navigation.Move(focus.Current, NavigationDirection.Next);
+ Assert.Same(button, focus.Current);
+
+ navigation.Move(focus.Current, NavigationDirection.Next);
+ Assert.Same(node, focus.Current);
+ }
+ }
+
private void ApplyTemplates(TreeView tree)
{
tree.ApplyTemplate();
diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs
new file mode 100644
index 0000000000..a090dcd18d
--- /dev/null
+++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs
@@ -0,0 +1,214 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Controls;
+using Xunit;
+
+namespace Avalonia.Input.UnitTests
+{
+ public class KeyboardNavigationTests_Custom
+ {
+ [Fact]
+ public void Tab_Should_Custom_Navigate_Within_Children()
+ {
+ Button current;
+ Button next;
+ var target = new CustomNavigatingStackPanel
+ {
+ Children =
+ {
+ (current = new Button { Content = "Button 1" }),
+ new Button { Content = "Button 2" },
+ (next = new Button { Content = "Button 3" }),
+ },
+ NextControl = next,
+ };
+
+ var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
+
+ Assert.Same(next, result);
+ }
+
+ [Fact]
+ public void Right_Should_Custom_Navigate_Within_Children()
+ {
+ Button current;
+ Button next;
+ var target = new CustomNavigatingStackPanel
+ {
+ Children =
+ {
+ (current = new Button { Content = "Button 1" }),
+ new Button { Content = "Button 2" },
+ (next = new Button { Content = "Button 3" }),
+ },
+ NextControl = next,
+ };
+
+ var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right);
+
+ Assert.Same(next, result);
+ }
+
+ [Fact]
+ public void Tab_Should_Custom_Navigate_From_Outside()
+ {
+ Button current;
+ Button next;
+ var target = new CustomNavigatingStackPanel
+ {
+ Children =
+ {
+ new Button { Content = "Button 1" },
+ new Button { Content = "Button 2" },
+ (next = new Button { Content = "Button 3" }),
+ },
+ NextControl = next,
+ };
+
+ var root = new StackPanel
+ {
+ Children =
+ {
+ (current = new Button { Content = "Outside" }),
+ target,
+ }
+ };
+
+ var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
+
+ Assert.Same(next, result);
+ }
+
+ [Fact]
+ public void Tab_Should_Custom_Navigate_From_Outside_When_Wrapping()
+ {
+ Button current;
+ Button next;
+ var target = new CustomNavigatingStackPanel
+ {
+ Children =
+ {
+ new Button { Content = "Button 1" },
+ new Button { Content = "Button 2" },
+ (next = new Button { Content = "Button 3" }),
+ },
+ NextControl = next,
+ };
+
+ var root = new StackPanel
+ {
+ Children =
+ {
+ target,
+ (current = new Button { Content = "Outside" }),
+ }
+ };
+
+ var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
+
+ Assert.Same(next, result);
+ }
+
+ [Fact]
+ public void ShiftTab_Should_Custom_Navigate_From_Outside()
+ {
+ Button current;
+ Button next;
+ var target = new CustomNavigatingStackPanel
+ {
+ Children =
+ {
+ new Button { Content = "Button 1" },
+ new Button { Content = "Button 2" },
+ (next = new Button { Content = "Button 3" }),
+ },
+ NextControl = next,
+ };
+
+ var root = new StackPanel
+ {
+ Children =
+ {
+ (current = new Button { Content = "Outside" }),
+ target,
+ }
+ };
+
+ var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
+
+ Assert.Same(next, result);
+ }
+
+ [Fact]
+ public void Right_Should_Custom_Navigate_From_Outside()
+ {
+ Button current;
+ Button next;
+ var target = new CustomNavigatingStackPanel
+ {
+ Children =
+ {
+ new Button { Content = "Button 1" },
+ new Button { Content = "Button 2" },
+ (next = new Button { Content = "Button 3" }),
+ },
+ NextControl = next,
+ };
+
+ var root = new StackPanel
+ {
+ Children =
+ {
+ (current = new Button { Content = "Outside" }),
+ target,
+ },
+ [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
+ };
+
+ var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right);
+
+ Assert.Same(next, result);
+ }
+
+ [Fact]
+ public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next()
+ {
+ Button current;
+ Button next;
+ var target = new CustomNavigatingStackPanel
+ {
+ Children =
+ {
+ new Button { Content = "Button 1" },
+ (current = new Button { Content = "Button 2" }),
+ new Button { Content = "Button 3" },
+ },
+ };
+
+ var root = new StackPanel
+ {
+ Children =
+ {
+ target,
+ (next = new Button { Content = "Outside" }),
+ }
+ };
+
+ var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
+
+ Assert.Same(next, result);
+ }
+
+ private class CustomNavigatingStackPanel : StackPanel, ICustomKeyboardNavigation
+ {
+ public bool CustomNavigates { get; set; } = true;
+ public IInputElement NextControl { get; set; }
+
+ public (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction)
+ {
+ return (CustomNavigates, NextControl);
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs
index 0cd8d4295b..f66adec1eb 100644
--- a/tests/Avalonia.UnitTests/TestServices.cs
+++ b/tests/Avalonia.UnitTests/TestServices.cs
@@ -50,6 +50,7 @@ namespace Avalonia.UnitTests
public static readonly TestServices RealFocus = new TestServices(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
+ keyboardNavigation: new KeyboardNavigationHandler(),
inputManager: new InputManager());
public static readonly TestServices RealLayoutManager = new TestServices(
@@ -63,6 +64,7 @@ namespace Avalonia.UnitTests
IFocusManager focusManager = null,
IInputManager inputManager = null,
Func keyboardDevice = null,
+ IKeyboardNavigationHandler keyboardNavigation = null,
ILayoutManager layoutManager = null,
IRuntimePlatform platform = null,
Func renderer = null,
@@ -79,6 +81,7 @@ namespace Avalonia.UnitTests
FocusManager = focusManager;
InputManager = inputManager;
KeyboardDevice = keyboardDevice;
+ KeyboardNavigation = keyboardNavigation;
LayoutManager = layoutManager;
Platform = platform;
Renderer = renderer;
@@ -96,6 +99,7 @@ namespace Avalonia.UnitTests
public IInputManager InputManager { get; }
public IFocusManager FocusManager { get; }
public Func KeyboardDevice { get; }
+ public IKeyboardNavigationHandler KeyboardNavigation { get; }
public ILayoutManager LayoutManager { get; }
public IRuntimePlatform Platform { get; }
public Func Renderer { get; }
@@ -113,6 +117,7 @@ namespace Avalonia.UnitTests
IFocusManager focusManager = null,
IInputManager inputManager = null,
Func keyboardDevice = null,
+ IKeyboardNavigationHandler keyboardNavigation = null,
ILayoutManager layoutManager = null,
IRuntimePlatform platform = null,
Func renderer = null,
@@ -131,6 +136,7 @@ namespace Avalonia.UnitTests
focusManager: focusManager ?? FocusManager,
inputManager: inputManager ?? InputManager,
keyboardDevice: keyboardDevice ?? KeyboardDevice,
+ keyboardNavigation: keyboardNavigation ?? KeyboardNavigation,
layoutManager: layoutManager ?? LayoutManager,
platform: platform ?? Platform,
renderer: renderer ?? Renderer,
diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs
index c5d533486b..28577e9670 100644
--- a/tests/Avalonia.UnitTests/UnitTestApplication.cs
+++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs
@@ -49,6 +49,7 @@ namespace Avalonia.UnitTests
.BindToSelf(this)
.Bind().ToConstant(Services.InputManager)
.Bind().ToConstant(Services.KeyboardDevice?.Invoke())
+ .Bind().ToConstant(Services.KeyboardNavigation)
.Bind().ToConstant(Services.LayoutManager)
.Bind().ToConstant(Services.Platform)
.Bind().ToConstant(new RendererFactory(Services.Renderer))