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. 2
      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.
/// </summary>
[PrivateApi]
public class FocusManager : IFocusManager
public class
FocusManager : IFocusManager
{
/// <summary>
/// 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)
return null;
var root = v.VisualRoot as Visual;
var root = v.PresentationSource?.InputRoot.FocusRoot as Visual;
while (root is IHostedVisualTreeRoot hosted &&
hosted.Host?.VisualRoot is Visual parentRoot)
hosted.Host?.PresentationSource?.InputRoot.FocusRoot is {} parentRoot)
{
root = parentRoot;
}

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

@ -26,5 +26,10 @@ namespace Avalonia.Input
internal ITextInputMethodImpl? InputMethod { 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)
return;
var element = FocusedElement ?? e.Root.RootElement;
var element = FocusedElement ?? e.Root.FocusRoot;
if (e is RawKeyEventArgs keyInput)
{

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

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

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

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

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

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

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

@ -57,4 +57,6 @@ internal partial class PresentationSource
}
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();
public PresentationSource(InputElement rootVisual, ITopLevelImpl platformImpl,
public PresentationSource(InputElement rootVisual, InputElement defaultFocusVisual,
ITopLevelImpl platformImpl,
IAvaloniaDependencyResolver dependencyResolver, Func<Size> clientSizeProvider)
{
_clientSizeProvider = clientSizeProvider;
@ -41,6 +42,7 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi
LayoutManager = CreateLayoutManager();
RootVisual = rootVisual;
FocusRoot = defaultFocusVisual;
}
// 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)
{
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;
_lastTipEventTime = pointerEvent.Timestamp;

8
src/Avalonia.Controls/TopLevel.cs

@ -189,8 +189,12 @@ namespace Avalonia.Controls
PlatformImpl = impl ?? throw new InvalidOperationException(
"Could not create window implementation: maybe no windowing subsystem was initialized?");
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;
_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),
() => GtkSystemDialog.TryCreate(this),
// TODO: This will be incompatible with "root element is not a TopLevel" scenarios,
// we should probably have a separate API for this
() => Task.FromResult(InputRoot.RootElement is TopLevel tl
// HACK: this relies on focus root being TopLevel which currently is true
() => Task.FromResult(InputRoot.FocusRoot is TopLevel tl
? (IStorageProvider?)new ManagedStorageProvider(tl)
: null)
});

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

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

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

@ -256,7 +256,7 @@ namespace Avalonia.Controls.UnitTests
var hitTester = new Mock<IHitTester>();
window.HitTesterOverride = hitTester.Object;
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);
var e = CreatePointerPressedEventArgs(window, new Point(90, 90));
@ -299,7 +299,7 @@ namespace Avalonia.Controls.UnitTests
var hitTester = new Mock<IHitTester>();
window.HitTesterOverride = hitTester.Object;
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);
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]
public void PopupRoot_Should_Have_Null_VisualParent()
public void PopupRoot_Should_Have_TopLevelHost_VisualParent()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
@ -108,7 +108,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
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;
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);
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);
Assert.NotNull(toolTip);
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);
}
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);
var root = control?.GetInputRoot() ?? window.InputRoot;
@ -585,7 +585,7 @@ namespace Avalonia.Controls.UnitTests
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));
}

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

@ -222,7 +222,7 @@ namespace Avalonia.Controls.UnitTests
}
[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))
{
@ -233,8 +233,7 @@ namespace Avalonia.Controls.UnitTests
target.Template = CreateTemplate();
target.Content = child;
target.ApplyTemplate();
target.Presenter!.ApplyTemplate();
Assert.Throws<InvalidOperationException>(() => target.Presenter!.ApplyTemplate());
}
}

2
tests/Avalonia.RenderTests/TestRenderRoot.cs

@ -75,6 +75,6 @@ namespace Avalonia.Skia.RenderTests
public IInputElement? PointerOverElement { get; set; }
public ITextInputMethodImpl? InputMethod { get; }
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 ITextInputMethodImpl? InputMethod { get; }
public InputElement RootElement => this;
public InputElement FocusRoot => this;
public bool ShowAccessKeys { get; set; }

Loading…
Cancel
Save