From 05b5b4a0c204ae48c10471894dac79fda09903d0 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 3 Apr 2024 18:50:45 +0200 Subject: [PATCH] feat: Port FocusManager adapters from WinUI --- .../Input/Internal/CoreWindowFocusObserver.cs | 15 + .../UI/Xaml/Input/Internal/FocusObserver.cs | 543 +++++++++--------- src/Uno.UI/UI/Xaml/Internal/ContentRoot.cs | 9 +- src/Uno.UI/UI/Xaml/Internal/FocusAdapter.cs | 28 +- .../Internal/FocusManagerCoreWindowAdapter.cs | 34 ++ .../Internal/FocusManagerXamlIslandAdapter.cs | 33 ++ .../Xaml/Internal/Islands/XamlIsland.Root.cs | 3 + 7 files changed, 376 insertions(+), 289 deletions(-) create mode 100644 src/Uno.UI/UI/Xaml/Input/Internal/CoreWindowFocusObserver.cs create mode 100644 src/Uno.UI/UI/Xaml/Internal/FocusManagerCoreWindowAdapter.cs create mode 100644 src/Uno.UI/UI/Xaml/Internal/FocusManagerXamlIslandAdapter.cs diff --git a/src/Uno.UI/UI/Xaml/Input/Internal/CoreWindowFocusObserver.cs b/src/Uno.UI/UI/Xaml/Input/Internal/CoreWindowFocusObserver.cs new file mode 100644 index 000000000000..75ef0a11cce7 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Input/Internal/CoreWindowFocusObserver.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Uno.UI.Xaml.Core; + +namespace Uno.UI.Xaml.Input; + +internal class CoreWindowFocusObserver : FocusObserver +{ + internal CoreWindowFocusObserver(ContentRoot contentRoot) : base(contentRoot) + { + } +} diff --git a/src/Uno.UI/UI/Xaml/Input/Internal/FocusObserver.cs b/src/Uno.UI/UI/Xaml/Input/Internal/FocusObserver.cs index 26f5e372cd75..39f4989653b4 100644 --- a/src/Uno.UI/UI/Xaml/Input/Internal/FocusObserver.cs +++ b/src/Uno.UI/UI/Xaml/Input/Internal/FocusObserver.cs @@ -14,191 +14,235 @@ using static Microsoft/* UWP don't rename */.UI.Xaml.Controls._Tracing; using static Uno.UI.Xaml.Input.FocusConversionFunctions; -namespace Uno.UI.Xaml.Input +namespace Uno.UI.Xaml.Input; + +internal class FocusObserver { - internal class FocusObserver - { - private readonly ContentRoot _contentRoot; + private readonly ContentRoot _contentRoot; - private XamlSourceFocusNavigationRequest? _currentInteraction; + private XamlSourceFocusNavigationRequest? _currentInteraction; - internal FocusObserver(ContentRoot contentRoot) - { - _contentRoot = contentRoot ?? throw new ArgumentNullException(nameof(contentRoot)); - } + internal FocusObserver(ContentRoot contentRoot) + { + _contentRoot = contentRoot ?? throw new ArgumentNullException(nameof(contentRoot)); + } - private Rect GetOriginToComponent(DependencyObject? pOldFocusedElement) - { - Rect focusedElementBounds = new Rect(); + private Rect GetOriginToComponent(DependencyObject? pOldFocusedElement) + { + Rect focusedElementBounds = new Rect(); - //Transform the bounding rect of currently focused element to Component's co-ordinate space - if (pOldFocusedElement != null) + //Transform the bounding rect of currently focused element to Component's co-ordinate space + if (pOldFocusedElement != null) + { + if (FocusableHelper.GetIFocusableForDO(pOldFocusedElement) is { } focusable) { - if (FocusableHelper.GetIFocusableForDO(pOldFocusedElement) is { } focusable) - { - //TODO Uno: Implement support for HyperLink focus - //DependencyObject depObj = focusable.GetDOForIFocusable(); - //IFC_RETURN(do_pointer_cast(depObj).GetContainingFrameworkElement().GetGlobalBounds(&focusedElementBounds, true)); - } - else + //TODO Uno: Implement support for HyperLink focus + //DependencyObject depObj = focusable.GetDOForIFocusable(); + //IFC_RETURN(do_pointer_cast(depObj).GetContainingFrameworkElement().GetGlobalBounds(&focusedElementBounds, true)); + } + else + { + UIElement? pUIElement = pOldFocusedElement as UIElement; + if (pUIElement != null) { - UIElement? pUIElement = pOldFocusedElement as UIElement; - if (pUIElement != null) - { - focusedElementBounds = pUIElement.GetGlobalBounds(true); - } + focusedElementBounds = pUIElement.GetGlobalBounds(true); } } + } - var origin = new Rect(); - origin.X = focusedElementBounds.Left; - origin.Y = focusedElementBounds.Top; - origin.Width = focusedElementBounds.Right - focusedElementBounds.Left; - origin.Height = focusedElementBounds.Bottom - focusedElementBounds.Top; + var origin = new Rect(); + origin.X = focusedElementBounds.Left; + origin.Y = focusedElementBounds.Top; + origin.Width = focusedElementBounds.Right - focusedElementBounds.Left; + origin.Height = focusedElementBounds.Bottom - focusedElementBounds.Top; - return origin; - } + return origin; + } + + private Rect GetOriginFromInteraction() + { + var rectRB = new Rect(); - private Rect GetOriginFromInteraction() + if (_currentInteraction != null) { - var rectRB = new Rect(); + var origin = _currentInteraction.HintRect; - if (_currentInteraction != null) - { - var origin = _currentInteraction.HintRect; + rectRB = new Rect( + new Point(origin.X, origin.Y), + new Point(origin.X + origin.Width, origin.Y + origin.Height)); + } - rectRB = new Rect( - new Point(origin.X, origin.Y), - new Point(origin.X + origin.Width, origin.Y + origin.Height)); - } + return rectRB; + } - return rectRB; - } + private FocusMovementResult NavigateFocusXY( + DependencyObject pComponent, + FocusNavigationDirection direction, + Rect origin) + { + Rect rect = origin; + XYFocusOptions xyFocusOptions = new XYFocusOptions(); + xyFocusOptions.FocusHintRectangle = rect; - private FocusMovementResult NavigateFocusXY( - DependencyObject pComponent, - FocusNavigationDirection direction, - Rect origin) - { - Rect rect = origin; - XYFocusOptions xyFocusOptions = new XYFocusOptions(); - xyFocusOptions.FocusHintRectangle = rect; + // Any value of the manifold is meaning less when Navigating or Departing into or from a component. + // The current manifold needs to be updated from the Origin given. + xyFocusOptions.UpdateManifoldsFromFocusHintRectangle = true; - // Any value of the manifold is meaning less when Navigating or Departing into or from a component. - // The current manifold needs to be updated from the Origin given. - xyFocusOptions.UpdateManifoldsFromFocusHintRectangle = true; + FocusMovement movement = new FocusMovement(xyFocusOptions, direction, pComponent); - FocusMovement movement = new FocusMovement(xyFocusOptions, direction, pComponent); + // We dont handle cancellation of a focus request from a host: + // We could support this by calling DepartFocus from the component + // if the component returns result.WasCanceled() + // We choose to not support it. + movement.CanCancel = false; - // We dont handle cancellation of a focus request from a host: - // We could support this by calling DepartFocus from the component - // if the component returns result.WasCanceled() - // We choose to not support it. - movement.CanCancel = false; + // Do not allow DepartFocus to be called, CoreWindowsFocusAdapter will handle it. + movement.CanDepartFocus = false; - // Do not allow DepartFocus to be called, CoreWindowsFocusAdapter will handle it. - movement.CanDepartFocus = false; + movement.ShouldCompleteAsyncOperation = true; - movement.ShouldCompleteAsyncOperation = true; + return _contentRoot.FocusManager.FindAndSetNextFocus(movement); + } - return _contentRoot.FocusManager.FindAndSetNextFocus(movement); - } + private Rect CalculateNewOrigin(FocusNavigationDirection direction, Rect currentOrigin) + { + var pFocusedElement = _contentRoot.VisualTree.ActiveRootVisual; + var windowBounds = GetOriginToComponent(pFocusedElement); - private Rect CalculateNewOrigin(FocusNavigationDirection direction, Rect currentOrigin) + var newOrigin = currentOrigin; + switch (direction) { - var pFocusedElement = _contentRoot.VisualTree.ActiveRootVisual; - var windowBounds = GetOriginToComponent(pFocusedElement); + case FocusNavigationDirection.Left: + case FocusNavigationDirection.Right: + newOrigin.X = windowBounds.X; + newOrigin.Width = windowBounds.Width; + break; + case FocusNavigationDirection.Up: + case FocusNavigationDirection.Down: + newOrigin.Y = windowBounds.Y; + newOrigin.Height = windowBounds.Height; + break; + } - var newOrigin = currentOrigin; - switch (direction) - { - case FocusNavigationDirection.Left: - case FocusNavigationDirection.Right: - newOrigin.X = windowBounds.X; - newOrigin.Width = windowBounds.Width; - break; - case FocusNavigationDirection.Up: - case FocusNavigationDirection.Down: - newOrigin.Y = windowBounds.Y; - newOrigin.Height = windowBounds.Height; - break; - } + return newOrigin; + } - return newOrigin; - } + internal bool ProcessNavigateFocusRequest(XamlSourceFocusNavigationRequest focusNavigationRequest) + { + var pHandled = false; - internal bool ProcessNavigateFocusRequest(XamlSourceFocusNavigationRequest focusNavigationRequest) - { - var pHandled = false; + UpdateCurrentInteraction(focusNavigationRequest); - UpdateCurrentInteraction(focusNavigationRequest); + var reason = focusNavigationRequest.Reason; - var reason = focusNavigationRequest.Reason; + DependencyObject? pRoot = null; - DependencyObject? pRoot = null; + pRoot = _contentRoot.Type switch + { + ContentRootType.XamlIsland => _contentRoot.VisualTree.RootScrollViewer ?? _contentRoot.VisualTree.ActiveRootVisual, + _ => _contentRoot.VisualTree.ActiveRootVisual, + }; - pRoot = _contentRoot.Type switch - { - ContentRootType.XamlIsland => _contentRoot.VisualTree.RootScrollViewer ?? _contentRoot.VisualTree.ActiveRootVisual, - _ => _contentRoot.VisualTree.ActiveRootVisual, - }; + FocusNavigationDirection direction = GetFocusNavigationDirectionFromReason(reason); - FocusNavigationDirection direction = GetFocusNavigationDirectionFromReason(reason); + if (reason == XamlSourceFocusNavigationReason.First || + reason == XamlSourceFocusNavigationReason.Last) + { + _contentRoot.InputManager.LastInputDeviceType = GetInputDeviceTypeFromDirection(direction); + bool bReverse = (reason == XamlSourceFocusNavigationReason.Last); - if (reason == XamlSourceFocusNavigationReason.First || - reason == XamlSourceFocusNavigationReason.Last) + if (pRoot == null) { - _contentRoot.InputManager.LastInputDeviceType = GetInputDeviceTypeFromDirection(direction); - bool bReverse = (reason == XamlSourceFocusNavigationReason.Last); + // No content has been loaded, bail out + return false; + } - if (pRoot == null) + DependencyObject? pCandidateElement = null; + if (bReverse) + { + pCandidateElement = _contentRoot.FocusManager.GetLastFocusableElement(pRoot); + } + else + { + pCandidateElement = _contentRoot.FocusManager.GetFirstFocusableElement(pRoot); + } + + bool retryWithPopupRoot = (pCandidateElement == null); + if (retryWithPopupRoot) + { + var popupRoot = _contentRoot.VisualTree.PopupRoot; + if (popupRoot != null) { - // No content has been loaded, bail out - return false; + if (bReverse) + { + pCandidateElement = _contentRoot.FocusManager.GetLastFocusableElement(popupRoot); + } + else + { + pCandidateElement = _contentRoot.FocusManager.GetFirstFocusableElement(popupRoot); + } } + } - DependencyObject? pCandidateElement = null; - if (bReverse) + if (pCandidateElement != null) + { + // When we move focus into XAML with tab, we first call ClearFocus to mimic desktop behavior. + // On desktop, we call ClearFocus during a tab cycle in CJupiterWindow.AcceleratorKeyActivated, but when + // running as a component (e.g. in c-shell) the way XAML loses focus when the user tabs away is different + // (we call DepartFocus) and if we call ClearFocus at the same time as we do on desktop, we'll end up + // firing the LostFocus for the previously-focused before calling GettingFocus for the newly focused element. + // So instead, we call ClearFocus as we tab *in* to XAML content. This preserves the focus event ordering on + // tab cycles. + _contentRoot.FocusManager.ClearFocus(); + + FocusMovement movement = new FocusMovement( + pCandidateElement, + bReverse ? FocusNavigationDirection.Previous : FocusNavigationDirection.Next, + FocusState.Keyboard); + + // We dont handle cancellation of a focus request from a host: + // We could support this by calling DepartFocus from the component + // if the component returns result.WasCanceled() + // We choose to not support it. + movement.CanCancel = false; + + FocusMovementResult result = _contentRoot.FocusManager.SetFocusedElement(movement); + if (result.WasMoved) { - pCandidateElement = _contentRoot.FocusManager.GetLastFocusableElement(pRoot); + pHandled = StopInteraction(); } - else + } + else + { + Guid correlationId = focusNavigationRequest.CorrelationId; + + DepartFocus(direction, correlationId, ref pHandled); + } + } + else if (reason == XamlSourceFocusNavigationReason.Restore || + reason == XamlSourceFocusNavigationReason.Programmatic) + { + var pFocusedElement = _contentRoot.FocusManager.FocusedElement; + if (pFocusedElement != null) + { + pHandled = StopInteraction(); + } + else if (pFocusedElement == null && reason == XamlSourceFocusNavigationReason.Programmatic) + { + if (pRoot == null) { - pCandidateElement = _contentRoot.FocusManager.GetFirstFocusableElement(pRoot); + // No content has been loaded, bail out + return false; } - bool retryWithPopupRoot = (pCandidateElement == null); - if (retryWithPopupRoot) + var pCandidateElement = _contentRoot.FocusManager.GetFirstFocusableElement(pRoot); + if (pCandidateElement == null) { - var popupRoot = _contentRoot.VisualTree.PopupRoot; - if (popupRoot != null) - { - if (bReverse) - { - pCandidateElement = _contentRoot.FocusManager.GetLastFocusableElement(popupRoot); - } - else - { - pCandidateElement = _contentRoot.FocusManager.GetFirstFocusableElement(popupRoot); - } - } + pCandidateElement = pRoot; } - if (pCandidateElement != null) { - // When we move focus into XAML with tab, we first call ClearFocus to mimic desktop behavior. - // On desktop, we call ClearFocus during a tab cycle in CJupiterWindow.AcceleratorKeyActivated, but when - // running as a component (e.g. in c-shell) the way XAML loses focus when the user tabs away is different - // (we call DepartFocus) and if we call ClearFocus at the same time as we do on desktop, we'll end up - // firing the LostFocus for the previously-focused before calling GettingFocus for the newly focused element. - // So instead, we call ClearFocus as we tab *in* to XAML content. This preserves the focus event ordering on - // tab cycles. - _contentRoot.FocusManager.ClearFocus(); - - FocusMovement movement = new FocusMovement( - pCandidateElement, - bReverse ? FocusNavigationDirection.Previous : FocusNavigationDirection.Next, - FocusState.Keyboard); + FocusMovement movement = new FocusMovement(pCandidateElement, FocusNavigationDirection.None, FocusState.Programmatic); // We dont handle cancellation of a focus request from a host: // We could support this by calling DepartFocus from the component @@ -212,160 +256,115 @@ internal bool ProcessNavigateFocusRequest(XamlSourceFocusNavigationRequest focus pHandled = StopInteraction(); } } - else - { - Guid correlationId = focusNavigationRequest.CorrelationId; - - DepartFocus(direction, correlationId, ref pHandled); - } } - else if (reason == XamlSourceFocusNavigationReason.Restore || - reason == XamlSourceFocusNavigationReason.Programmatic) - { - var pFocusedElement = _contentRoot.FocusManager.FocusedElement; - if (pFocusedElement != null) - { - pHandled = StopInteraction(); - } - else if (pFocusedElement == null && reason == XamlSourceFocusNavigationReason.Programmatic) - { - if (pRoot == null) - { - // No content has been loaded, bail out - return false; - } - - var pCandidateElement = _contentRoot.FocusManager.GetFirstFocusableElement(pRoot); - if (pCandidateElement == null) - { - pCandidateElement = pRoot; - } - if (pCandidateElement != null) - { - FocusMovement movement = new FocusMovement(pCandidateElement, FocusNavigationDirection.None, FocusState.Programmatic); - - // We dont handle cancellation of a focus request from a host: - // We could support this by calling DepartFocus from the component - // if the component returns result.WasCanceled() - // We choose to not support it. - movement.CanCancel = false; - - FocusMovementResult result = _contentRoot.FocusManager.SetFocusedElement(movement); - if (result.WasMoved) - { - pHandled = StopInteraction(); - } - } - } - } - else if (reason == XamlSourceFocusNavigationReason.Left || - reason == XamlSourceFocusNavigationReason.Right || - reason == XamlSourceFocusNavigationReason.Up || - reason == XamlSourceFocusNavigationReason.Down) + } + else if (reason == XamlSourceFocusNavigationReason.Left || + reason == XamlSourceFocusNavigationReason.Right || + reason == XamlSourceFocusNavigationReason.Up || + reason == XamlSourceFocusNavigationReason.Down) + { + if (pRoot == null) { - if (pRoot == null) - { - // No content has been loaded, bail out - return false; - } - - _contentRoot.InputManager.LastInputDeviceType = GetInputDeviceTypeFromDirection(direction); - - Rect rect = GetOriginFromInteraction(); - - FocusMovementResult result = NavigateFocusXY(pRoot, direction, rect); - //IFC_RETURN(result.GetHResult()); - if (result.WasMoved) - { - pHandled = StopInteraction(); - } - else - { - // - // If we could not find a target via XY then we need to depart focus again - // But this time from an orgin inside of the component - // - // ┌────────────────────────────────┐ - // │ CoreWindow │ - // │ │ - // │ │ - // ┌──────────┐ Direction ├────────────────────────────────┤ - // │ origin │ ─────────> │ New Origin: │ Depart Focus from new origin - // │ │ │ Calculated as the intersertion │ ─────────> - // │ │ │ from the direction │ - // └──────────┘ ├────────────────────────────────┤ - // │ │ - // │ │ - // │ │ - // └────────────────────────────────┘ - - Rect origin = focusNavigationRequest.HintRect; - Rect newOrigin = CalculateNewOrigin(direction, origin); - Guid correlationId = focusNavigationRequest.CorrelationId; - - DepartFocus(direction, newOrigin, correlationId, ref pHandled); - } + // No content has been loaded, bail out + return false; } - return pHandled; - } + _contentRoot.InputManager.LastInputDeviceType = GetInputDeviceTypeFromDirection(direction); - internal void DepartFocus( - FocusNavigationDirection direction, - Guid correlationId, - ref bool handled) - { - var pFocusedElement = _contentRoot.FocusManager.FocusedElement; - var origin = GetOriginToComponent(pFocusedElement); - - DepartFocus(direction, origin, correlationId, ref handled); - } + Rect rect = GetOriginFromInteraction(); - private void DepartFocus( - FocusNavigationDirection direction, - Rect origin, - Guid correlationId, - ref bool handled) - { - if (handled) //|| _focusController == null) + FocusMovementResult result = NavigateFocusXY(pRoot, direction, rect); + //IFC_RETURN(result.GetHResult()); + if (result.WasMoved) { - return; + pHandled = StopInteraction(); } - - var reason = GetFocusNavigationReasonFromDirection(direction); - if (reason == null) + else { - // Do nothing if we dont support this navigation - return; + // + // If we could not find a target via XY then we need to depart focus again + // But this time from an orgin inside of the component + // + // ┌────────────────────────────────┐ + // │ CoreWindow │ + // │ │ + // │ │ + // ┌──────────┐ Direction ├────────────────────────────────┤ + // │ origin │ ─────────> │ New Origin: │ Depart Focus from new origin + // │ │ │ Calculated as the intersertion │ ─────────> + // │ │ │ from the direction │ + // └──────────┘ ├────────────────────────────────┤ + // │ │ + // │ │ + // │ │ + // └────────────────────────────────┘ + + Rect origin = focusNavigationRequest.HintRect; + Rect newOrigin = CalculateNewOrigin(direction, origin); + Guid correlationId = focusNavigationRequest.CorrelationId; + + DepartFocus(direction, newOrigin, correlationId, ref pHandled); } + } - StartInteraction(reason.Value, origin, correlationId); + return pHandled; + } - //_focusController.DepartFocus(_currentInteraction); - handled = true; - } + internal void DepartFocus( + FocusNavigationDirection direction, + Guid correlationId, + ref bool handled) + { + var pFocusedElement = _contentRoot.FocusManager.FocusedElement; + var origin = GetOriginToComponent(pFocusedElement); + + DepartFocus(direction, origin, correlationId, ref handled); + } - private void StartInteraction( - XamlSourceFocusNavigationReason reason, - Rect origin, - Guid correlationId) + private void DepartFocus( + FocusNavigationDirection direction, + Rect origin, + Guid correlationId, + ref bool handled) + { + if (handled) //|| _focusController == null) { - var request = new XamlSourceFocusNavigationRequest(reason, origin, correlationId); - _currentInteraction = request; + return; } - private bool StopInteraction() + var reason = GetFocusNavigationReasonFromDirection(direction); + if (reason == null) { - MUX_ASSERT(_currentInteraction != null); - _currentInteraction = null; - return true; + // Do nothing if we dont support this navigation + return; } - protected virtual CoreWindowActivationMode GetActivationMode() => CoreWindowActivationMode.None; + StartInteraction(reason.Value, origin, correlationId); - private void UpdateCurrentInteraction(XamlSourceFocusNavigationRequest? pRequest) - { - _currentInteraction = pRequest; - } + //_focusController.DepartFocus(_currentInteraction); + handled = true; + } + + private void StartInteraction( + XamlSourceFocusNavigationReason reason, + Rect origin, + Guid correlationId) + { + var request = new XamlSourceFocusNavigationRequest(reason, origin, correlationId); + _currentInteraction = request; + } + + private bool StopInteraction() + { + MUX_ASSERT(_currentInteraction != null); + _currentInteraction = null; + return true; + } + + protected virtual CoreWindowActivationMode GetActivationMode() => CoreWindowActivationMode.None; + + private void UpdateCurrentInteraction(XamlSourceFocusNavigationRequest? pRequest) + { + _currentInteraction = pRequest; } } diff --git a/src/Uno.UI/UI/Xaml/Internal/ContentRoot.cs b/src/Uno.UI/UI/Xaml/Internal/ContentRoot.cs index 7f4a2c9311bb..0f719c65de2d 100644 --- a/src/Uno.UI/UI/Xaml/Internal/ContentRoot.cs +++ b/src/Uno.UI/UI/Xaml/Internal/ContentRoot.cs @@ -68,10 +68,6 @@ public ContentRoot(ContentRootType type, Color backgroundColor, UIElement? rootE _contentRootEventListener = new ContentRootEventListener(this); FocusManager = new FocusManager(this); - //TODO Uno: We may want to create a custom version of adapter and observer for Island vs CoreWindow. - FocusAdapter = new FocusAdapter(this); - FocusManager.SetFocusObserver(new FocusObserver(this)); - CompositionTarget = new CompositionTarget(this); CompositionTarget.Root = ElementCompositionPreview.GetElementVisual(VisualTree.RootElement); CompositionTarget.Root.CompositionTarget = CompositionTarget; @@ -81,8 +77,13 @@ public ContentRoot(ContentRootType type, Color backgroundColor, UIElement? rootE case ContentRootType.CoreWindow: MUX_ASSERT(coreServices.ContentRootCoordinator.CoreWindowContentRoot == null); coreServices.ContentRootCoordinator.CoreWindowContentRoot = this; + + FocusAdapter = new FocusManagerCoreWindowAdapter(this); + FocusManager.SetFocusObserver(new CoreWindowFocusObserver(this)); break; case ContentRootType.XamlIsland: + FocusAdapter = new FocusManagerXamlIslandAdapter(this); + FocusManager.SetFocusObserver(new FocusObserver(this)); break; } } diff --git a/src/Uno.UI/UI/Xaml/Internal/FocusAdapter.cs b/src/Uno.UI/UI/Xaml/Internal/FocusAdapter.cs index 3af4eacc1dc3..77228efed7b6 100644 --- a/src/Uno.UI/UI/Xaml/Internal/FocusAdapter.cs +++ b/src/Uno.UI/UI/Xaml/Internal/FocusAdapter.cs @@ -1,21 +1,23 @@ -#nullable enable +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// MUX Reference dxaml\xcp\components\ContentRoot\FocusAdapter.cpp, tag winui3/release/1.5.1, commit 3d10001ba8 + +#nullable enable using Microsoft.UI.Xaml.Input; -namespace Uno.UI.Xaml.Core +namespace Uno.UI.Xaml.Core; + +internal abstract class FocusAdapter { - // TODO Uno: Add support - internal class FocusAdapter - { - private readonly ContentRoot _contentRoot; + protected readonly ContentRoot _contentRoot; - public FocusAdapter(ContentRoot contentRoot) - { - _contentRoot = contentRoot; - } + public FocusAdapter(ContentRoot contentRoot) + { + _contentRoot = contentRoot ?? throw new System.ArgumentNullException(nameof(contentRoot)); + } - internal virtual void SetFocus() { } + internal abstract void SetFocus(); - internal virtual bool ShouldDepartFocus(FocusNavigationDirection direction) => false; - } + internal virtual bool ShouldDepartFocus(FocusNavigationDirection direction) => false; } diff --git a/src/Uno.UI/UI/Xaml/Internal/FocusManagerCoreWindowAdapter.cs b/src/Uno.UI/UI/Xaml/Internal/FocusManagerCoreWindowAdapter.cs new file mode 100644 index 000000000000..336508db5f3d --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/FocusManagerCoreWindowAdapter.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// MUX Reference dxaml\xcp\components\ContentRoot\FocusManagerCoreWindowAdapter.cpp, tag winui3/release/1.5.1, commit 3d10001ba8 + +#nullable enable + +using Microsoft.UI.Xaml.Input; + +namespace Uno.UI.Xaml.Core; + +internal class FocusManagerCoreWindowAdapter : FocusAdapter +{ + public FocusManagerCoreWindowAdapter(ContentRoot contentRoot) : base(contentRoot) + { + } + + internal override void SetFocus() + { + // TODO Uno: Implement if required + } + +#if HAS_UNO // Uno specific: WinUI implementation just returns false. + internal override bool ShouldDepartFocus(FocusNavigationDirection direction) + { +#if __WASM__ // In case of WASM we want to depart focus when tabbing out of the root visual. + bool isTabbingDirection = direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous; + + return isTabbingDirection && focusScopeIsIsland; +#else + return base.ShouldDepartFocus(direction); +#endif + } +#endif +} diff --git a/src/Uno.UI/UI/Xaml/Internal/FocusManagerXamlIslandAdapter.cs b/src/Uno.UI/UI/Xaml/Internal/FocusManagerXamlIslandAdapter.cs new file mode 100644 index 000000000000..ee77dfd61ec0 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/FocusManagerXamlIslandAdapter.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// MUX Reference dxaml\xcp\components\ContentRoot\FocusManagerXamlIslandAdapter.cpp, tag winui3/release/1.5.1, commit 3d10001ba8 + +#nullable enable + +using Microsoft.UI.Xaml.Input; +using static Microsoft.UI.Xaml.Controls._Tracing; + +namespace Uno.UI.Xaml.Core; + +internal class FocusManagerXamlIslandAdapter : FocusAdapter +{ + public FocusManagerXamlIslandAdapter(ContentRoot contentRoot) : base(contentRoot) + { + } + + internal override void SetFocus() + { + // We have moved the focus to an element hosted in an Island + // Make sure that this island has also focus + bool hasFocusNow = _contentRoot.XamlIslandRoot!.TrySetFocus(); + MUX_ASSERT(hasFocusNow, "Failed to move focus to xaml island"); + } + + internal override bool ShouldDepartFocus(FocusNavigationDirection direction) + { + bool isTabbingDirection = direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous; + bool focusScopeIsIsland = _contentRoot.XamlIslandRoot!.IsActive(); + + return isTabbingDirection && focusScopeIsIsland; + } +} diff --git a/src/Uno.UI/UI/Xaml/Internal/Islands/XamlIsland.Root.cs b/src/Uno.UI/UI/Xaml/Internal/Islands/XamlIsland.Root.cs index ed525cd11d02..d2303a89de57 100644 --- a/src/Uno.UI/UI/Xaml/Internal/Islands/XamlIsland.Root.cs +++ b/src/Uno.UI/UI/Xaml/Internal/Islands/XamlIsland.Root.cs @@ -99,4 +99,7 @@ protected override Size ArrangeOverride(Size finalSize) } public Size GetSize() => new Size(ActualWidth, ActualHeight); + + // TODO Uno: Implement focus on island. + public bool TrySetFocus() => true; }