From 536daf0b4c7b9e3bf0a24b93058c8157cbaf1d9b Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 16 Mar 2026 10:19:41 +0100 Subject: [PATCH] Open FocusManager API (#20854) * Open FocusManager API * Merge some FocusManager overloads * Update API suppressions * Properly reset reused XYFocusOptions instances * Clarify Focus documentation * Improve FocusManager documentation * Update API suppressions --- api/Avalonia.nupkg.xml | 132 ++++++++++++++ .../Input/FindNextElementOptions.cs | 37 ++++ src/Avalonia.Base/Input/FocusManager.cs | 169 ++++++++---------- src/Avalonia.Base/Input/IFocusManager.cs | 63 ++++++- src/Avalonia.Base/Input/InputElement.cs | 4 +- .../Input/Navigation/XYFocusOptions.cs | 29 ++- .../PresentationSource/PresentationSource.cs | 4 +- .../Input/InputElement_Focus.cs | 2 +- .../Input/KeyboardDeviceTests.cs | 4 +- tests/Avalonia.UnitTests/TestRoot.cs | 2 +- 10 files changed, 337 insertions(+), 109 deletions(-) diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index e160bda11e..dd20d0f39e 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1117,12 +1117,48 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.ClearFocus + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType) baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.IFocusManager.ClearFocus + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler @@ -2611,12 +2647,48 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.ClearFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType) baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Input.IFocusManager.ClearFocus + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler @@ -4027,6 +4099,36 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Input.IFocusManager.FindFirstFocusableElement + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindLastFocusableElement + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType}) @@ -4315,6 +4417,36 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Input.IFocusManager.FindFirstFocusableElement + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindLastFocusableElement + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType}) diff --git a/src/Avalonia.Base/Input/FindNextElementOptions.cs b/src/Avalonia.Base/Input/FindNextElementOptions.cs index e6062daf9b..72d83ec419 100644 --- a/src/Avalonia.Base/Input/FindNextElementOptions.cs +++ b/src/Avalonia.Base/Input/FindNextElementOptions.cs @@ -6,12 +6,49 @@ using System.Threading.Tasks; namespace Avalonia.Input { + /// + /// Provides options to customize the behavior when identifying the next element to focus + /// during a navigation operation. + /// public sealed class FindNextElementOptions { + /// + /// Gets or sets the root within which the search for the next + /// focusable element will be conducted. + /// + /// + /// This property defines the boundary for focus navigation operations. It determines the root element + /// in the visual tree under which the focusable item search is performed. If not specified, the search + /// will default to the current scope. + /// public InputElement? SearchRoot { get; init; } + + /// + /// Gets or sets the rectangular region within the visual hierarchy that will be excluded + /// from consideration during focus navigation. + /// public Rect ExclusionRect { get; init; } + + /// + /// Gets or sets a rectangular region that serves as a hint for focus navigation. + /// This property specifies a rectangle, relative to the coordinate system of the search root, + /// which can be used as a preferred or prioritized target when navigating focus. + /// It can be null if no specific hint region is provided. + /// public Rect? FocusHintRectangle { get; init; } + + /// + /// Specifies an optional override for the navigation strategy used in XY focus navigation. + /// This property allows customizing the focus movement behavior when navigating between UI elements. + /// public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; } + + /// + /// Specifies whether occlusivity (overlapping of elements or obstructions) + /// should be ignored during focus navigation. When set to true, + /// the navigation logic disregards obstructions that may block a potential + /// focus target, allowing elements behind such obstructions to be considered. + /// public bool IgnoreOcclusivity { get; init; } } } diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index 15b8fea77d..dc62171f48 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -4,7 +4,6 @@ using System.Linq; using Avalonia.Input.Navigation; using Avalonia.Interactivity; using Avalonia.Metadata; -using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Input @@ -12,7 +11,6 @@ namespace Avalonia.Input /// /// Manages focus for the application. /// - [PrivateApi] public class FocusManager : IFocusManager { /// @@ -42,58 +40,51 @@ namespace Avalonia.Input RoutingStrategies.Tunnel); } + [PrivateApi] public FocusManager() { - _contentRoot = null; } - public FocusManager(IInputElement contentRoot) - { - _contentRoot = contentRoot; - } - - internal void SetContentRoot(IInputElement? contentRoot) + /// + /// Gets or sets the content root for the focus management system. + /// + [PrivateApi] + public IInputElement? ContentRoot { - _contentRoot = contentRoot; + get => _contentRoot; + set => _contentRoot = value; } private IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; - private XYFocus _xyFocus = new(); - private XYFocusOptions _xYFocusOptions = new XYFocusOptions(); + private readonly XYFocus _xyFocus = new(); private IInputElement? _contentRoot; + private XYFocusOptions? _reusableFocusOptions; - /// - /// Gets the currently focused . - /// + /// public IInputElement? GetFocusedElement() => Current; - /// - /// Focuses a control. - /// - /// The control to focus. - /// The method by which focus was changed. - /// Any key modifiers active at the time of focus. + /// public bool Focus( - IInputElement? control, + IInputElement? element, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { if (KeyboardDevice.Instance is not { } keyboardDevice) return false; - if (control is not null) + if (element is not null) { - if (!CanFocus(control)) + if (!CanFocus(element)) return false; - if (GetFocusScope(control) is StyledElement scope) + if (GetFocusScope(element) is StyledElement scope) { - scope.SetValue(FocusedElementProperty, control); + scope.SetValue(FocusedElementProperty, element); _focusRoot = GetFocusRoot(scope); } - keyboardDevice.SetFocusedElement(control, method, keyModifiers); + keyboardDevice.SetFocusedElement(element, method, keyModifiers); return true; } else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore && @@ -110,12 +101,7 @@ namespace Avalonia.Input } } - public void ClearFocus() - { - Focus(null); - } - - public void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent) + internal void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent) { if (oldParent is IInputElement parentElement && GetFocusScope(parentElement) is StyledElement scope && @@ -129,6 +115,7 @@ namespace Avalonia.Input Focus(null); } + [PrivateApi] public IInputElement? GetFocusedElement(IFocusScope scope) { return (scope as StyledElement)?.GetValue(FocusedElementProperty); @@ -138,6 +125,7 @@ namespace Avalonia.Input /// Notifies the focus manager of a change in focus scope. /// /// The new focus scope. + [PrivateApi] public void SetFocusScope(IFocusScope scope) { if (GetFocusedElement(scope) is { } focused) @@ -153,12 +141,14 @@ namespace Avalonia.Input } } + [PrivateApi] public void RemoveFocusRoot(IFocusScope scope) { if (scope == _focusRoot) - ClearFocus(); + Focus(null); } + [PrivateApi] public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope; /// @@ -176,25 +166,15 @@ namespace Avalonia.Input ?? (FocusManager?)AvaloniaLocator.Current.GetService(); } - /// - /// Attempts to change focus from the element with focus to the next focusable element in the specified direction. - /// - /// The direction to traverse (in tab order). - /// true if focus moved; otherwise, false. - public bool TryMoveFocus(NavigationDirection direction) + /// + public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null) { - return FindAndSetNextFocus(direction, _xYFocusOptions); - } + ValidateDirection(direction); - /// - /// Attempts to change focus from the element with focus to the next focusable element in the specified direction, using the specified navigation options. - /// - /// The direction to traverse (in tab order). - /// The options to help identify the next element to receive focus with keyboard/controller/remote navigation. - /// true if focus moved; otherwise, false. - public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions options) - { - return FindAndSetNextFocus(direction, ValidateAndCreateFocusOptions(direction, options)); + var focusOptions = ToFocusOptions(options, true); + var result = FindAndSetNextFocus(direction, focusOptions); + _reusableFocusOptions = focusOptions; + return result; } /// @@ -295,10 +275,7 @@ namespace Avalonia.Input return true; } - /// - /// Retrieves the first element that can receive focus. - /// - /// The first focusable element. + /// public IInputElement? FindFirstFocusableElement() { var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement; @@ -317,10 +294,7 @@ namespace Avalonia.Input return GetFirstFocusableElement(searchScope); } - /// - /// Retrieves the last element that can receive focus. - /// - /// The last focusable element. + /// public IInputElement? FindLastFocusableElement() { var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement; @@ -339,52 +313,59 @@ namespace Avalonia.Input return GetFocusManager(searchScope)?.GetLastFocusableElement(searchScope); } - /// - /// Retrieves the element that should receive focus based on the specified navigation direction. - /// - /// - /// - public IInputElement? FindNextElement(NavigationDirection direction) + /// + public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null) { - var xyOption = new XYFocusOptions() - { - UpdateManifold = false - }; + ValidateDirection(direction); - return FindNextFocus(direction, xyOption); + var focusOptions = ToFocusOptions(options, false); + var result = FindNextFocus(direction, focusOptions); + _reusableFocusOptions = focusOptions; + return result; } - /// - /// Retrieves the element that should receive focus based on the specified navigation direction (cannot be used with tab navigation). - /// - /// The direction that focus moves from element to element within the app UI. - /// The options to help identify the next element to receive focus with the provided navigation. - /// The next element to receive focus. - public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions options) + private static void ValidateDirection(NavigationDirection direction) { - return FindNextFocus(direction, ValidateAndCreateFocusOptions(direction, options)); + if (direction is not ( + NavigationDirection.Next or + NavigationDirection.Previous or + NavigationDirection.Up or + NavigationDirection.Down or + NavigationDirection.Left or + NavigationDirection.Right)) + { + throw new ArgumentOutOfRangeException( + nameof(direction), + direction, + $"Only {nameof(NavigationDirection.Next)}, {nameof(NavigationDirection.Previous)}, " + + $"{nameof(NavigationDirection.Up)}, {nameof(NavigationDirection.Down)}," + + $" {nameof(NavigationDirection.Left)} and {nameof(NavigationDirection.Right)} directions are supported"); + } } - private static XYFocusOptions ValidateAndCreateFocusOptions(NavigationDirection direction, FindNextElementOptions options) + private XYFocusOptions ToFocusOptions(FindNextElementOptions? options, bool updateManifold) { - if (direction is not NavigationDirection.Up - and not NavigationDirection.Down - and not NavigationDirection.Left - and not NavigationDirection.Right) + // XYFocus only uses the options and never modifies them; we can cache and reset them between calls. + var focusOptions = _reusableFocusOptions; + _reusableFocusOptions = null; + + if (focusOptions is null) + focusOptions = new XYFocusOptions(); + else + focusOptions.Reset(); + + if (options is not null) { - throw new ArgumentOutOfRangeException(nameof(direction), - $"{direction} is not supported with FindNextElementOptions. Only Up, Down, Left and right are supported"); + focusOptions.SearchRoot = options.SearchRoot; + focusOptions.ExclusionRect = options.ExclusionRect; + focusOptions.FocusHintRectangle = options.FocusHintRectangle; + focusOptions.NavigationStrategyOverride = options.NavigationStrategyOverride; + focusOptions.IgnoreOcclusivity = options.IgnoreOcclusivity; } - return new XYFocusOptions - { - UpdateManifold = false, - SearchRoot = options.SearchRoot, - ExclusionRect = options.ExclusionRect, - FocusHintRectangle = options.FocusHintRectangle, - NavigationStrategyOverride = options.NavigationStrategyOverride, - IgnoreOcclusivity = options.IgnoreOcclusivity - }; + focusOptions.UpdateManifold = updateManifold; + + return focusOptions; } internal IInputElement? FindNextFocus(NavigationDirection direction, XYFocusOptions focusOptions, bool updateManifolds = true) diff --git a/src/Avalonia.Base/Input/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs index 5691172f3f..9bd1fb4239 100644 --- a/src/Avalonia.Base/Input/IFocusManager.cs +++ b/src/Avalonia.Base/Input/IFocusManager.cs @@ -14,9 +14,66 @@ namespace Avalonia.Input IInputElement? GetFocusedElement(); /// - /// Clears currently focused element. + /// Focuses a control. /// - [Unstable("This API might be removed in 11.x minor updates. Please consider focusing another element instead of removing focus at all for better UX.")] - void ClearFocus(); + /// The control to focus. + /// The method by which focus was changed. + /// Any key modifiers active at the time of focus. + /// true if the focus moved to a control; otherwise, false. + /// + /// If is null, this method tries to clear the focus. However, it is not advised. + /// For a better user experience, focus should be moved to another element when possible. + /// + /// When this method return true, it is not guaranteed that the focus has been moved + /// to . The focus might have been redirected to another element. + /// + bool Focus( + IInputElement? element, + NavigationMethod method = NavigationMethod.Unspecified, + KeyModifiers keyModifiers = KeyModifiers.None); + + /// + /// Attempts to change focus from the element with focus to the next focusable element in the specified direction. + /// + /// + /// The direction that focus moves from element to element. + /// Must be one of , , + /// , , + /// and . + /// + /// + /// The options to help identify the next element to receive focus. + /// They only apply to directional navigation. + /// + /// true if focus moved; otherwise, false. + bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null); + + /// + /// Retrieves the first element that can receive focus. + /// + /// The first focusable element. + IInputElement? FindFirstFocusableElement(); + + /// + /// Retrieves the last element that can receive focus. + /// + /// The last focusable element. + IInputElement? FindLastFocusableElement(); + + /// + /// Retrieves the element that should receive focus based on the specified navigation direction. + /// + /// + /// The direction that focus moves from element to element. + /// Must be one of , , + /// , , + /// and . + /// + /// + /// The options to help identify the next element to receive focus. + /// They only apply to directional navigation. + /// + /// The next element to receive focus, if any. + IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null); } } diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 1beccf341e..e908e818e8 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -523,7 +523,7 @@ namespace Avalonia.Input if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager && Equals(focusManager.GetFocusedElement(), this)) { - focusManager.ClearFocus(); + focusManager.Focus(null); } } } @@ -995,7 +995,7 @@ namespace Avalonia.Input } else { - focusManager.ClearFocus(); + focusManager.Focus(null); } } } diff --git a/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs b/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs index 4bfcb22502..8e4c847aa9 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs @@ -1,17 +1,38 @@ namespace Avalonia.Input.Navigation; -internal class XYFocusOptions +internal sealed class XYFocusOptions { public InputElement? SearchRoot { get; set; } public Rect ExclusionRect { get; set; } public Rect? FocusHintRectangle { get; set; } public Rect? FocusedElementBounds { get; set; } public XYFocusNavigationStrategy? NavigationStrategyOverride { get; set; } - public bool IgnoreClipping { get; set; } = true; + public bool IgnoreClipping { get; set; } public bool IgnoreCone { get; set; } public KeyDeviceType? KeyDeviceType { get; set; } - public bool ConsiderEngagement { get; set; } = true; - public bool UpdateManifold { get; set; } = true; + public bool ConsiderEngagement { get; set; } + public bool UpdateManifold { get; set; } public bool UpdateManifoldsFromFocusHintRect { get; set; } public bool IgnoreOcclusivity { get; set; } + + public XYFocusOptions() + { + Reset(); + } + + internal void Reset() + { + SearchRoot = null; + ExclusionRect = default; + FocusHintRectangle = null; + FocusedElementBounds = null; + NavigationStrategyOverride = null; + IgnoreClipping = true; + IgnoreCone = false; + KeyDeviceType = null; + ConsiderEngagement = true; + UpdateManifold = true; + UpdateManifoldsFromFocusHintRect = false; + IgnoreOcclusivity = false; + } } diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs index c98a380640..9917f82c93 100644 --- a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs @@ -61,7 +61,7 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi field?.SetPresentationSourceForRootVisual(this); Renderer.CompositionTarget.Root = field?.CompositionVisual; - FocusManager.SetContentRoot(value as IInputElement); + FocusManager.ContentRoot = value; } } @@ -152,4 +152,4 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi } return null; } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs index 7755eb80cf..cdb4588fff 100644 --- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs +++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs @@ -577,7 +577,7 @@ namespace Avalonia.Base.UnitTests.Input }; target.Focus(); - root.FocusManager.ClearFocus(); + root.FocusManager.Focus(null); Assert.Null(root.FocusManager.GetFocusedElement()); } diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs index d11872ba6a..b1446d961f 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs @@ -15,7 +15,7 @@ namespace Avalonia.Base.UnitTests.Input using (UnitTestApplication.Start(TestServices.FocusableWindow)) { var window = new Window(); - window.FocusManager.ClearFocus(); + window.FocusManager.Focus(null); int raised = 0; window.KeyDown += (sender, ev) => { @@ -71,7 +71,7 @@ namespace Avalonia.Base.UnitTests.Input using (UnitTestApplication.Start(TestServices.FocusableWindow)) { var window = new Window(); - window.FocusManager.ClearFocus(); + window.FocusManager.Focus(null); int raised = 0; window.TextInput += (sender, ev) => { diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index ed91463346..4400d77267 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -74,7 +74,7 @@ namespace Avalonia.UnitTests IRenderer IPresentationSource.Renderer => Renderer; IHitTester IPresentationSource.HitTester => HitTester; - public IFocusManager FocusManager => _focusManager ??= new FocusManager(this); + public IFocusManager FocusManager => _focusManager ??= new FocusManager { ContentRoot = this }; public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService(); public IInputElement? PointerOverElement { get; set; }