Browse Source

Make sure that TopLevel is no longer the actual root of the visual tree. This is needed for our future changes.

feature-presentation-source-2
Nikita Tsukanov 4 weeks ago
parent
commit
c456998ce8
  1. 7
      src/Avalonia.Base/Input/FocusManager.cs
  2. 5
      src/Avalonia.Base/Input/IInputRoot.cs
  3. 2
      src/Avalonia.Base/Input/KeyboardDevice.cs
  4. 2
      src/Avalonia.Base/VisualTree/IHostedVisualTreeRoot.cs
  5. 4
      src/Avalonia.Controls/Automation/Peers/EmbeddableControlRootAutomationPeer.cs
  6. 2
      src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
  7. 2
      src/Avalonia.Controls/PresentationSource/PresentationSource.Input.cs
  8. 4
      src/Avalonia.Controls/PresentationSource/PresentationSource.cs
  9. 2
      src/Avalonia.Controls/ToolTipService.cs
  10. 8
      src/Avalonia.Controls/TopLevel.cs
  11. 21
      src/Avalonia.Controls/TopLevelHost.cs
  12. 4
      src/Avalonia.X11/X11Window.cs
  13. 8
      tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
  14. 4
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
  15. 4
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  16. 2
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  17. 6
      tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
  18. 5
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  19. 2
      tests/Avalonia.RenderTests/TestRenderRoot.cs
  20. 1
      tests/Avalonia.UnitTests/TestRoot.cs

7
src/Avalonia.Base/Input/FocusManager.cs

@ -13,7 +13,8 @@ namespace Avalonia.Input
/// Manages focus for the application. /// Manages focus for the application.
/// </summary> /// </summary>
[PrivateApi] [PrivateApi]
public class FocusManager : IFocusManager public class
FocusManager : IFocusManager
{ {
/// <summary> /// <summary>
/// Private attached property for storing the currently focused element in a focus scope. /// Private attached property for storing the currently focused element in a focus scope.
@ -246,10 +247,10 @@ namespace Avalonia.Input
if (scope is not Visual v) if (scope is not Visual v)
return null; return null;
var root = v.VisualRoot as Visual; var root = v.PresentationSource?.InputRoot.FocusRoot as Visual;
while (root is IHostedVisualTreeRoot hosted && while (root is IHostedVisualTreeRoot hosted &&
hosted.Host?.VisualRoot is Visual parentRoot) hosted.Host?.PresentationSource?.InputRoot.FocusRoot is {} parentRoot)
{ {
root = parentRoot; root = parentRoot;
} }

5
src/Avalonia.Base/Input/IInputRoot.cs

@ -26,5 +26,10 @@ namespace Avalonia.Input
internal ITextInputMethodImpl? InputMethod { get; } internal ITextInputMethodImpl? InputMethod { get; }
internal InputElement RootElement { get; } internal InputElement RootElement { get; }
// HACK: This is a temporary hack for "default focus" concept.
// If nothing is focused we send keyboard events to Window. Since for now we always
// control PresentationSource, we simply pass the TopLevel as a separate parameter there.
internal InputElement FocusRoot { get; }
} }
} }

2
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -225,7 +225,7 @@ namespace Avalonia.Input
if(e.Handled) if(e.Handled)
return; return;
var element = FocusedElement ?? e.Root.RootElement; var element = FocusedElement ?? e.Root.FocusRoot;
if (e is RawKeyEventArgs keyInput) if (e is RawKeyEventArgs keyInput)
{ {

2
src/Avalonia.Base/VisualTree/IHostedVisualTreeRoot.cs

@ -3,7 +3,7 @@ namespace Avalonia.VisualTree
/// <summary> /// <summary>
/// Interface for controls that are at the root of a hosted visual tree, such as popups. /// Interface for controls that are at the root of a hosted visual tree, such as popups.
/// </summary> /// </summary>
public interface IHostedVisualTreeRoot internal interface IHostedVisualTreeRoot
{ {
/// <summary> /// <summary>
/// Gets the visual tree host. /// Gets the visual tree host.

4
src/Avalonia.Controls/Automation/Peers/EmbeddableControlRootAutomationPeer.cs

@ -58,8 +58,8 @@ namespace Avalonia.Controls.Automation.Peers
{ {
var oldFocus = _focus; var oldFocus = _focus;
var c = focus as Control; var c = focus as Control;
_focus = c?.VisualRoot == Owner ? c : null; _focus = Owner.IsVisualAncestorOf(c) ? c : null;
if (_focus != oldFocus) if (_focus != oldFocus)
{ {

2
src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs

@ -70,7 +70,7 @@ namespace Avalonia.Automation.Peers
var oldFocus = _focus; var oldFocus = _focus;
var c = focus as Control; var c = focus as Control;
_focus = c?.VisualRoot == Owner ? c : null; _focus = Owner.IsVisualAncestorOf(c) ? c : null;
if (_focus != oldFocus) if (_focus != oldFocus)
{ {

2
src/Avalonia.Controls/PresentationSource/PresentationSource.Input.cs

@ -57,4 +57,6 @@ internal partial class PresentationSource
} }
return candidate; return candidate;
} }
public InputElement FocusRoot { get; }
} }

4
src/Avalonia.Controls/PresentationSource/PresentationSource.cs

@ -19,7 +19,8 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi
internal FocusManager FocusManager { get; } = new(); internal FocusManager FocusManager { get; } = new();
public PresentationSource(InputElement rootVisual, ITopLevelImpl platformImpl, public PresentationSource(InputElement rootVisual, InputElement defaultFocusVisual,
ITopLevelImpl platformImpl,
IAvaloniaDependencyResolver dependencyResolver, Func<Size> clientSizeProvider) IAvaloniaDependencyResolver dependencyResolver, Func<Size> clientSizeProvider)
{ {
_clientSizeProvider = clientSizeProvider; _clientSizeProvider = clientSizeProvider;
@ -41,6 +42,7 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi
LayoutManager = CreateLayoutManager(); LayoutManager = CreateLayoutManager();
RootVisual = rootVisual; RootVisual = rootVisual;
FocusRoot = defaultFocusVisual;
} }
// In WPF it's a Visual and it's nullable. For now we have it as non-nullable InputElement since // In WPF it's a Visual and it's nullable. For now we have it as non-nullable InputElement since

2
src/Avalonia.Controls/ToolTipService.cs

@ -38,7 +38,7 @@ namespace Avalonia.Controls
if (e is RawPointerEventArgs pointerEvent) if (e is RawPointerEventArgs pointerEvent)
{ {
bool isTooltipEvent = false; bool isTooltipEvent = false;
if (_tipControl?.GetValue(ToolTip.ToolTipProperty) is { } currentTip && e.Root.RootElement == currentTip.PopupHost) if (_tipControl?.GetValue(ToolTip.ToolTipProperty) is { } currentTip && e.Root == (currentTip.PopupHost as Visual)?.GetInputRoot())
{ {
isTooltipEvent = true; isTooltipEvent = true;
_lastTipEventTime = pointerEvent.Timestamp; _lastTipEventTime = pointerEvent.Timestamp;

8
src/Avalonia.Controls/TopLevel.cs

@ -189,8 +189,12 @@ namespace Avalonia.Controls
PlatformImpl = impl ?? throw new InvalidOperationException( PlatformImpl = impl ?? throw new InvalidOperationException(
"Could not create window implementation: maybe no windowing subsystem was initialized?"); "Could not create window implementation: maybe no windowing subsystem was initialized?");
dependencyResolver ??= AvaloniaLocator.Current; dependencyResolver ??= AvaloniaLocator.Current;
_source = new PresentationSource(this, impl, dependencyResolver, () => ClientSize);
_source.RootVisual = this; var hostVisual = new TopLevelHost(this);
((ISetLogicalParent)hostVisual).SetParent(this);
_source = new PresentationSource(hostVisual, this,
impl, dependencyResolver, () => ClientSize);
_source.Renderer.SceneInvalidated += SceneInvalidated; _source.Renderer.SceneInvalidated += SceneInvalidated;
_scaling = ValidateScaling(impl.RenderScaling); _scaling = ValidateScaling(impl.RenderScaling);

21
src/Avalonia.Controls/TopLevelHost.cs

@ -0,0 +1,21 @@
using Avalonia.Input;
namespace Avalonia.Controls;
/// <summary>
/// For now this is a stub class that is needed to prevent people from assuming that TopLevel sits at the root of the
/// visual tree.
/// In future 12.x releases it will serve more roles like hosting popups and CSD.
/// </summary>
internal class TopLevelHost : Control
{
static TopLevelHost()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TopLevelHost>(KeyboardNavigationMode.Cycle);
}
public TopLevelHost(TopLevel tl)
{
VisualChildren.Add(tl);
}
}

4
src/Avalonia.X11/X11Window.cs

@ -268,8 +268,8 @@ namespace Avalonia.X11
: Task.FromResult<IStorageProvider?>(null), : Task.FromResult<IStorageProvider?>(null),
() => GtkSystemDialog.TryCreate(this), () => GtkSystemDialog.TryCreate(this),
// TODO: This will be incompatible with "root element is not a TopLevel" scenarios, // TODO: This will be incompatible with "root element is not a TopLevel" scenarios,
// we should probably have a separate API for this // HACK: this relies on focus root being TopLevel which currently is true
() => Task.FromResult(InputRoot.RootElement is TopLevel tl () => Task.FromResult(InputRoot.FocusRoot is TopLevel tl
? (IStorageProvider?)new ManagedStorageProvider(tl) ? (IStorageProvider?)new ManagedStorageProvider(tl)
: null) : null)
}); });

8
tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs

@ -14,6 +14,8 @@ public class CompositorInvalidationClippingTests : CompositorTestsBase
foreach (var child in visual.VisualChildren) count += CountVisuals(child); foreach (var child in visual.VisualChildren) count += CountVisuals(child);
return count; return count;
} }
private const int TopLevelOverhead = 2; // TopLevel + TopLevelHost
[Theory, [Theory,
// If canvas itself has no background, the second render won't draw any visuals at all, since // If canvas itself has no background, the second render won't draw any visuals at all, since
@ -22,9 +24,9 @@ public class CompositorInvalidationClippingTests : CompositorTestsBase
InlineData(true, false, false, 1, 0), InlineData(true, false, false, 1, 0),
InlineData(false, true, false, 1, 0), InlineData(false, true, false, 1, 0),
// If canvas has background, the second render will draw only the canvas visual itself // If canvas has background, the second render will draw only the canvas visual itself
InlineData(false, false, true, 5, 4), InlineData(false, false, true, 4 + TopLevelOverhead, 3 + TopLevelOverhead),
InlineData(true, false, true,5, 4), InlineData(true, false, true,4 + TopLevelOverhead, 3 + TopLevelOverhead),
InlineData(false, true, true, 5, 4), InlineData(false, true, true, 4 + TopLevelOverhead, 3 + TopLevelOverhead),
] ]
public void Do_Not_Re_Render_Unaffected_Visual_Trees(bool clipToBounds, bool clipGeometry, public void Do_Not_Re_Render_Unaffected_Visual_Trees(bool clipToBounds, bool clipGeometry,
bool canvasHasContent, bool canvasHasContent,

4
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@ -256,7 +256,7 @@ namespace Avalonia.Controls.UnitTests
var hitTester = new Mock<IHitTester>(); var hitTester = new Mock<IHitTester>();
window.HitTesterOverride = hitTester.Object; window.HitTesterOverride = hitTester.Object;
hitTester.Setup(x => hitTester.Setup(x =>
x.HitTestFirst(new Point(90, 90), window, It.IsAny<Func<Visual, bool>>())) x.HitTestFirst(new Point(90, 90), (Visual)window.VisualRoot!, It.IsAny<Func<Visual, bool>>()))
.Returns(button); .Returns(button);
var e = CreatePointerPressedEventArgs(window, new Point(90, 90)); var e = CreatePointerPressedEventArgs(window, new Point(90, 90));
@ -299,7 +299,7 @@ namespace Avalonia.Controls.UnitTests
var hitTester = new Mock<IHitTester>(); var hitTester = new Mock<IHitTester>();
window.HitTesterOverride = hitTester.Object; window.HitTesterOverride = hitTester.Object;
hitTester.Setup(x => hitTester.Setup(x =>
x.HitTestFirst(new Point(90, 90), window, It.IsAny<Func<Visual, bool>>())) x.HitTestFirst(new Point(90, 90), (Visual)window.VisualRoot!, It.IsAny<Func<Visual, bool>>()))
.Returns(button); .Returns(button);
var e = CreatePointerPressedEventArgs(window, new Point(90, 90)); var e = CreatePointerPressedEventArgs(window, new Point(90, 90));

4
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@ -100,7 +100,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
[Fact] [Fact]
public void PopupRoot_Should_Have_Null_VisualParent() public void PopupRoot_Should_Have_TopLevelHost_VisualParent()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
@ -108,7 +108,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Open(); target.Open();
Assert.Null(((Visual)target.Host!).GetVisualParent()); Assert.IsType<TopLevelHost>(((Visual)target.Host!).GetVisualParent());
} }
} }

2
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -607,7 +607,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
window.Content = border; window.Content = border;
hitTester.Setup(x => hitTester.Setup(x =>
x.HitTestFirst(new Point(10, 15), window, It.IsAny<Func<Visual, bool>>())) x.HitTestFirst(new Point(10, 15), (Visual)window.VisualRoot!, It.IsAny<Func<Visual, bool>>()))
.Returns(border); .Returns(border);
border.PointerPressed += (s, e) => border.PointerPressed += (s, e) =>

6
tests/Avalonia.Controls.UnitTests/ToolTipTests.cs

@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests
var toolTip = control.GetValue(ToolTip.ToolTipProperty); var toolTip = control.GetValue(ToolTip.ToolTipProperty);
Assert.NotNull(toolTip); Assert.NotNull(toolTip);
Assert.IsType<PopupRoot>(toolTip.PopupHost); Assert.IsType<PopupRoot>(toolTip.PopupHost);
Assert.Same(toolTip.VisualRoot, toolTip.PopupHost); Assert.Same(TopLevel.GetTopLevel(toolTip), toolTip.PopupHost);
} }
} }
@ -574,7 +574,7 @@ namespace Avalonia.Controls.UnitTests
point = new Point(id, int.MaxValue); point = new Point(id, int.MaxValue);
} }
hitTesterMock.Setup(m => m.HitTestFirst(point, window, It.IsAny<Func<Visual, bool>>())) hitTesterMock.Setup(m => m.HitTestFirst(point, It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>()))
.Returns(control); .Returns(control);
var root = control?.GetInputRoot() ?? window.InputRoot; var root = control?.GetInputRoot() ?? window.InputRoot;
@ -585,7 +585,7 @@ namespace Avalonia.Controls.UnitTests
if (lastRoot != null && lastRoot != root) if (lastRoot != null && lastRoot != root)
{ {
((TopLevel)lastRoot.RootElement)?.PlatformImpl?.Input?.Invoke(new RawPointerEventArgs(s_mouseDevice, timestamp, ((PresentationSource)lastRoot)?.PlatformImpl?.Input?.Invoke(new RawPointerEventArgs(s_mouseDevice, timestamp,
lastRoot, RawPointerEventType.LeaveWindow, new Point(-1,-1), RawInputModifiers.None)); lastRoot, RawPointerEventType.LeaveWindow, new Point(-1,-1), RawInputModifiers.None));
} }

5
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@ -222,7 +222,7 @@ namespace Avalonia.Controls.UnitTests
} }
[Fact] [Fact]
public void Adding_Top_Level_As_Child_Should_Not_Exception() public void Adding_Top_Level_As_Child_Should_Throw_Exception()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
@ -233,8 +233,7 @@ namespace Avalonia.Controls.UnitTests
target.Template = CreateTemplate(); target.Template = CreateTemplate();
target.Content = child; target.Content = child;
target.ApplyTemplate(); target.ApplyTemplate();
Assert.Throws<InvalidOperationException>(() => target.Presenter!.ApplyTemplate());
target.Presenter!.ApplyTemplate();
} }
} }

2
tests/Avalonia.RenderTests/TestRenderRoot.cs

@ -75,6 +75,6 @@ namespace Avalonia.Skia.RenderTests
public IInputElement? PointerOverElement { get; set; } public IInputElement? PointerOverElement { get; set; }
public ITextInputMethodImpl? InputMethod { get; } public ITextInputMethodImpl? InputMethod { get; }
public InputElement RootElement => this; public InputElement RootElement => this;
public InputElement FocusRoot => this;
} }
} }

1
tests/Avalonia.UnitTests/TestRoot.cs

@ -80,6 +80,7 @@ namespace Avalonia.UnitTests
public IInputElement? PointerOverElement { get; set; } public IInputElement? PointerOverElement { get; set; }
public ITextInputMethodImpl? InputMethod { get; } public ITextInputMethodImpl? InputMethod { get; }
public InputElement RootElement => this; public InputElement RootElement => this;
public InputElement FocusRoot => this;
public bool ShowAccessKeys { get; set; } public bool ShowAccessKeys { get; set; }

Loading…
Cancel
Save