Browse Source

Merge branch 'master' into extend-listbox-and-treeview-sample

Steven Kirk 6 years ago
parent
commit
e4eb517056
73 changed files with 1929 additions and 718 deletions
  1. 12 2
      samples/ControlCatalog.NetCore/Program.cs
  2. 0 5
      src/Android/Avalonia.Android/AndroidPlatform.cs
  3. 0 112
      src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs
  4. 2 0
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  5. 1 1
      src/Avalonia.Controls/ComboBox.cs
  6. 2 0
      src/Avalonia.Controls/ContextMenu.cs
  7. 1 0
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  8. 1 1
      src/Avalonia.Controls/MenuItem.cs
  9. 1 1
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  10. 17 2
      src/Avalonia.Controls/PlacementMode.cs
  11. 1 1
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  12. 3 1
      src/Avalonia.Controls/Platform/IPopupImpl.cs
  13. 2 0
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  14. 1 23
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  15. 27 0
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  16. 0 1
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  17. 0 5
      src/Avalonia.Controls/Platform/PlatformManager.cs
  18. 1 0
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  19. 0 42
      src/Avalonia.Controls/Primitives/AdornerDecorator.cs
  20. 1 1
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  21. 26 0
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  22. 38 0
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  23. 149 0
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  24. 144 140
      src/Avalonia.Controls/Primitives/Popup.cs
  25. 358 0
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  26. 175 0
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  27. 50 0
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  28. 55 64
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  29. 93 0
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  30. 8 6
      src/Avalonia.Controls/ToolTip.cs
  31. 45 0
      src/Avalonia.Controls/Window.cs
  32. 0 44
      src/Avalonia.Controls/WindowBase.cs
  33. 5 0
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  34. 0 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  35. 22 1
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  36. 0 5
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  37. 1 0
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  38. 19 1
      src/Avalonia.Native/PopupImpl.cs
  39. 8 0
      src/Avalonia.Native/WindowImpl.cs
  40. 2 1
      src/Avalonia.Native/WindowImplBase.cs
  41. 4 4
      src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
  42. 8 10
      src/Avalonia.Themes.Default/ComboBox.xaml
  43. 1 0
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  44. 3 3
      src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml
  45. 14 0
      src/Avalonia.Themes.Default/OverlayPopupHost.xaml
  46. 8 6
      src/Avalonia.Themes.Default/PopupRoot.xaml
  47. 2 2
      src/Avalonia.Themes.Default/Window.xaml
  48. 54 1
      src/Avalonia.Visuals/Media/PixelPoint.cs
  49. 10 0
      src/Avalonia.Visuals/Media/PixelRect.cs
  50. 203 0
      src/Avalonia.Visuals/Media/PixelVector.cs
  51. 2 6
      src/Avalonia.X11/X11Platform.cs
  52. 35 12
      src/Avalonia.X11/X11Window.cs
  53. 3 1
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  54. 5 4
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  55. 4 2
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
  56. 3 4
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
  57. 1 0
      src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs
  58. 51 38
      src/Skia/Avalonia.Skia/GlRenderTarget.cs
  59. 2 0
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  60. 15 0
      src/Windows/Avalonia.Win32/PopupImpl.cs
  61. 2 5
      src/Windows/Avalonia.Win32/Win32Platform.cs
  62. 3 4
      src/Windows/Avalonia.Win32/WindowImpl.cs
  63. 2 0
      src/iOS/Avalonia.iOS/TopLevelImpl.cs
  64. 4 1
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  65. 16 12
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  66. 31 0
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
  67. 55 11
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  68. 66 89
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  69. 0 27
      tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
  70. 3 6
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  71. 9 4
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
  72. 1 0
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  73. 38 4
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

+ 12 - 2
samples/ControlCatalog.NetCore/Program.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Diagnostics;
+using System.Globalization;
 using System.Linq;
 using System.Threading;
 using Avalonia;
@@ -28,15 +29,24 @@ namespace ControlCatalog.NetCore
             }
 
             var builder = BuildAvaloniaApp();
+
+            double GetScaling()
+            {
+                var idx = Array.IndexOf(args, "--scaling");
+                if (idx != 0 && args.Length > idx + 1 &&
+                    double.TryParse(args[idx + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out var scaling))
+                    return scaling;
+                return 1;
+            }
             if (args.Contains("--fbdev"))
             {
                 SilenceConsole();
-                return builder.StartLinuxFbDev(args);
+                return builder.StartLinuxFbDev(args, scaling: GetScaling());
             }
             else if (args.Contains("--drm"))
             {
                 SilenceConsole();
-                return builder.StartLinuxDrm(args);
+                return builder.StartLinuxDrm(args, scaling: GetScaling());
             }
             else
                 return builder.StartWithClassicDesktopLifetime(args);

+ 0 - 5
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -71,10 +71,5 @@ namespace Avalonia.Android
         {
             throw new NotSupportedException();
         }
-
-        public IPopupImpl CreatePopup()
-        {
-            return new PopupImpl();
-        }
     }
 }

+ 0 - 112
src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs

@@ -1,112 +0,0 @@
-using System;
-using Android.Content;
-using Android.Graphics;
-using Android.Runtime;
-using Android.Views;
-using Avalonia.Controls;
-using Avalonia.Platform;
-
-namespace Avalonia.Android.Platform.SkiaPlatform
-{
-    class PopupImpl : TopLevelImpl, IPopupImpl
-    {
-        private PixelPoint _position;
-        private bool _isAdded;
-        Action IWindowBaseImpl.Activated { get; set; }
-        public Action<PixelPoint> PositionChanged { get; set; }
-        public Action Deactivated { get; set; }
-
-        public PopupImpl() : base(ActivityTracker.Current, true)
-        {
-        }
-
-        private Size _clientSize = new Size(1, 1);
-
-        public void Resize(Size value)
-        {
-            if (View == null)
-                return;
-            _clientSize = value;
-            UpdateParams();
-        }
-
-        public void SetMinMaxSize(Size minSize, Size maxSize)
-        {
-        }
-
-        public IScreenImpl Screen { get; }
-
-        public PixelPoint Position
-        {
-            get { return _position; }
-            set
-            {
-                _position = value;
-                PositionChanged?.Invoke(_position);
-                UpdateParams();
-            }
-        }
-
-        WindowManagerLayoutParams CreateParams() => new WindowManagerLayoutParams(0,
-            WindowManagerFlags.NotTouchModal, Format.Translucent)
-        {
-            Gravity = GravityFlags.Left | GravityFlags.Top,
-            WindowAnimations = 0,
-            X = (int) _position.X,
-            Y = (int) _position.Y,
-            Width = Math.Max(1, (int) _clientSize.Width),
-            Height = Math.Max(1, (int) _clientSize.Height)
-        };
-
-        void UpdateParams()
-        {
-            if (_isAdded)
-                ActivityTracker.Current?.WindowManager?.UpdateViewLayout(View, CreateParams());
-        }
-
-        public override void Show()
-        {
-            if (_isAdded)
-                return;
-            ActivityTracker.Current.WindowManager.AddView(View, CreateParams());
-            _isAdded = true;
-        }
-
-        public override void Hide()
-        {
-            if (_isAdded)
-            {
-                var wm = View.Context.ApplicationContext.GetSystemService(Context.WindowService)
-                    .JavaCast<IWindowManager>();
-                wm.RemoveView(View);
-                _isAdded = false;
-            }
-        }
-
-        public override void Dispose()
-        {
-            Hide();
-            base.Dispose();
-        }
-
-
-        public void Activate()
-        {
-        }
-
-        public void BeginMoveDrag()
-        {
-            //Not supported
-        }
-
-        public void BeginResizeDrag(WindowEdge edge)
-        {
-            //Not supported
-        }
-
-        public void SetTopmost(bool value)
-        {
-            //Not supported
-        }
-    }
-}

+ 2 - 0
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -191,6 +191,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             }
         }
 
+        public IPopupImpl CreatePopup() => null;
+
         ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface);
     }
 }

+ 1 - 1
src/Avalonia.Controls/ComboBox.cs

@@ -202,7 +202,7 @@ namespace Avalonia.Controls
         {
             if (!e.Handled)
             {
-                if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot)
+                if (_popup?.IsInsidePopup((IVisual)e.Source) == true)
                 {
                     if (UpdateSelectionFromEventSource(e.Source))
                     {

+ 2 - 0
src/Avalonia.Controls/ContextMenu.cs

@@ -91,6 +91,8 @@ namespace Avalonia.Controls
         /// <param name="control">The control.</param>
         public void Open(Control control)
         {
+            if (control == null)
+                throw new ArgumentNullException(nameof(control));
             if (IsOpen)
             {
                 return;

+ 1 - 0
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@@ -61,5 +61,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
 
         public Action Closed { get; set; }
         public abstract IMouseDevice MouseDevice { get; }
+        public IPopupImpl CreatePopup() => null;
     }
 }

+ 1 - 1
src/Avalonia.Controls/MenuItem.cs

@@ -224,7 +224,7 @@ namespace Avalonia.Controls
         public bool IsTopLevel => Parent is Menu;
 
         /// <inheritdoc/>
-        bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false;
+        bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; 
 
         /// <inheritdoc/>
         IMenuElement IMenuItem.Parent => Parent as IMenuElement;

+ 1 - 1
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@@ -150,7 +150,7 @@ namespace Avalonia.Controls.Notifications
         private void Install(Window host)
         {
             var adornerLayer = host.GetVisualDescendants()
-                .OfType<AdornerDecorator>()
+                .OfType<VisualLayerManager>()
                 .FirstOrDefault()
                 ?.AdornerLayer;
 

+ 17 - 2
src/Avalonia.Controls/PlacementMode.cs

@@ -23,6 +23,21 @@ namespace Avalonia.Controls
         /// <summary>
         /// The popup is placed at the top right of its target.
         /// </summary>
-        Right
+        Right,
+        
+        /// <summary>
+        /// The popup is placed at the top left of its target.
+        /// </summary>
+        Left,
+        
+        /// <summary>
+        /// The popup is placed at the top left of its target.
+        /// </summary>
+        Top,
+        
+        /// <summary>
+        /// The popup is placed according to anchor and gravity rules
+        /// </summary>
+        AnchorAndGravity
     }
-}
+}

+ 1 - 1
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@@ -396,7 +396,7 @@ namespace Avalonia.Controls.Platform
 
         protected internal virtual void WindowDeactivated(object sender, EventArgs e)
         {
-            Menu.Close();
+            Menu?.Close();
         }
 
         protected void Click(IMenuItem item)

+ 3 - 1
src/Avalonia.Controls/Platform/IPopupImpl.cs

@@ -1,6 +1,8 @@
 // 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.Primitives.PopupPositioning;
+
 namespace Avalonia.Platform
 {
     /// <summary>
@@ -8,6 +10,6 @@ namespace Avalonia.Platform
     /// </summary>
     public interface IPopupImpl : IWindowBaseImpl
     {
-
+        IPopupPositioner PopupPositioner { get; }
     }
 }

+ 2 - 0
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@@ -107,5 +107,7 @@ namespace Avalonia.Platform
         /// </summary>
         [CanBeNull]
         IMouseDevice MouseDevice { get; }
+
+        IPopupImpl CreatePopup();
     }
 }

+ 1 - 23
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@@ -15,21 +15,10 @@ namespace Avalonia.Platform
         /// </summary>
         void Hide();
 
-        /// <summary>
-        /// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
-        /// </summary>
-        void BeginMoveDrag();
-
-        /// <summary>
-        /// Starts resizing a window. This function is used if an application has window resizing controls. 
-        /// Should be called from left mouse button press event handler
-        /// </summary>
-        void BeginResizeDrag(WindowEdge edge);
-
         /// <summary>
         /// Gets the position of the window in device pixels.
         /// </summary>
-        PixelPoint Position { get; set; }
+        PixelPoint Position { get; }
         
         /// <summary>
         /// Gets or sets a method called when the window's position changes.
@@ -61,17 +50,6 @@ namespace Avalonia.Platform
         /// </summary>
         Size MaxClientSize { get; }
 
-        /// <summary>
-        /// Sets the client size of the top level.
-        /// </summary>
-        void Resize(Size clientSize);
-
-        /// <summary>
-        /// Minimum width of the window.
-        /// </summary>
-        /// 
-        void SetMinMaxSize(Size minSize, Size maxSize);
-
         /// <summary>
         /// Sets whether this window appears on top of all other windows
         /// </summary>

+ 27 - 0
src/Avalonia.Controls/Platform/IWindowImpl.cs

@@ -57,5 +57,32 @@ namespace Avalonia.Platform
         /// Return true to prevent the underlying implementation from closing.
         /// </summary>
         Func<bool> Closing { get; set; }
+        
+        /// <summary>
+        /// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
+        /// </summary>
+        void BeginMoveDrag();
+
+        /// <summary>
+        /// Starts resizing a window. This function is used if an application has window resizing controls. 
+        /// Should be called from left mouse button press event handler
+        /// </summary>
+        void BeginResizeDrag(WindowEdge edge);
+        
+        /// <summary>
+        /// Sets the client size of the top level.
+        /// </summary>
+        void Resize(Size clientSize);
+        
+        /// <summary>
+        /// Sets the client size of the top level.
+        /// </summary>
+        void Move(PixelPoint point);
+        
+        /// <summary>
+        /// Minimum width of the window.
+        /// </summary>
+        /// 
+        void SetMinMaxSize(Size minSize, Size maxSize);
     }
 }

+ 0 - 1
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

@@ -4,6 +4,5 @@ namespace Avalonia.Platform
     {
         IWindowImpl CreateWindow();
         IEmbeddableWindowImpl CreateEmbeddableWindow();
-        IPopupImpl CreatePopup();
     }
 }

+ 0 - 5
src/Avalonia.Controls/Platform/PlatformManager.cs

@@ -41,10 +41,5 @@ namespace Avalonia.Controls.Platform
                 throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered.");
             return platform.CreateEmbeddableWindow();
         }
-
-        public static IPopupImpl CreatePopup()
-        {
-            return AvaloniaLocator.Current.GetService<IWindowingPlatform>().CreatePopup();
-        }
     }
 }

+ 1 - 0
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -229,6 +229,7 @@ namespace Avalonia.Controls.Presenters
                 if (oldChild != null)
                 {
                     VisualChildren.Remove(oldChild);
+                    ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent);
                 }
 
                 if (oldChild?.Parent == this)

+ 0 - 42
src/Avalonia.Controls/Primitives/AdornerDecorator.cs

@@ -1,42 +0,0 @@
-// 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.LogicalTree;
-
-namespace Avalonia.Controls.Primitives
-{
-    public class AdornerDecorator : Decorator
-    {
-        public AdornerDecorator()
-        {
-            AdornerLayer = new AdornerLayer();
-            ((ISetLogicalParent)AdornerLayer).SetParent(this);
-            AdornerLayer.ZIndex = int.MaxValue;
-            VisualChildren.Add(AdornerLayer);
-        }
-
-        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
-        {
-            base.OnAttachedToLogicalTree(e);
-
-            ((ILogical)AdornerLayer).NotifyAttachedToLogicalTree(e);
-        }
-
-        public AdornerLayer AdornerLayer
-        {
-            get;
-        }
-
-        protected override Size MeasureOverride(Size availableSize)
-        {
-            AdornerLayer.Measure(availableSize);
-            return base.MeasureOverride(availableSize);
-        }
-
-        protected override Size ArrangeOverride(Size finalSize)
-        {
-            AdornerLayer.Arrange(new Rect(finalSize));
-            return base.ArrangeOverride(finalSize);
-        }
-    }
-}

+ 1 - 1
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives
         public static AdornerLayer GetAdornerLayer(IVisual visual)
         {
             return visual.GetVisualAncestors()
-                .OfType<AdornerDecorator>()
+                .OfType<VisualLayerManager>()
                 .FirstOrDefault()
                 ?.AdornerLayer;
         }

+ 26 - 0
src/Avalonia.Controls/Primitives/IPopupHost.cs

@@ -0,0 +1,26 @@
+using System;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+    public interface IPopupHost : IDisposable
+    {
+        void SetChild(IControl control);
+        IContentPresenter Presenter { get; }
+        IVisual HostedVisualTreeRoot { get; }
+
+        event EventHandler<TemplateAppliedEventArgs> TemplateApplied;
+
+        void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
+            PopupPositioningEdge anchor = PopupPositioningEdge.None,
+            PopupPositioningEdge gravity = PopupPositioningEdge.None);
+        void Show();
+        void Hide();
+        IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty,
+            StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty,
+            StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
+            StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty);
+    }
+}

+ 38 - 0
src/Avalonia.Controls/Primitives/OverlayLayer.cs

@@ -0,0 +1,38 @@
+using System.Linq;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+    public class OverlayLayer : Canvas, ICustomSimpleHitTest
+    {
+        public Size AvailableSize { get; private set; }
+        public static OverlayLayer GetOverlayLayer(IVisual visual)
+        {
+            foreach(var v in visual.GetVisualAncestors())
+                if(v is VisualLayerManager vlm)
+                    if (vlm.OverlayLayer != null)
+                        return vlm.OverlayLayer;
+            if (visual is TopLevel tl)
+            {
+                var layers = tl.GetVisualDescendants().OfType<VisualLayerManager>().FirstOrDefault();
+                return layers?.OverlayLayer;
+            }
+
+            return null;
+        }
+        
+        public bool HitTest(Point point)
+        {
+            return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true);
+        }
+        
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            // We are saving it here since child controls might need to know the entire size of the overlay
+            // and Bounds won't be updated in time
+            AvailableSize = finalSize;
+            return base.ArrangeOverride(finalSize);
+        }
+    }
+}

+ 149 - 0
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+    public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup
+    {
+        private readonly OverlayLayer _overlayLayer;
+        private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters();
+        private ManagedPopupPositioner _positioner;
+        private Point _lastRequestedPosition;
+        private bool _shown;
+
+        public OverlayPopupHost(OverlayLayer overlayLayer)
+        {
+            _overlayLayer = overlayLayer;
+            _positioner = new ManagedPopupPositioner(this);
+        }
+
+        public void SetChild(IControl control)
+        {
+            Content = control;
+        }
+
+        public IVisual HostedVisualTreeRoot => null;
+        
+        /// <inheritdoc/>
+        IInteractive IInteractive.InteractiveParent => Parent;
+
+        public void Dispose() => Hide();
+
+
+        public void Show()
+        {
+            _overlayLayer.Children.Add(this);
+            _shown = true;
+        }
+
+        public void Hide()
+        {
+            _overlayLayer.Children.Remove(this);
+            _shown = false;
+        }
+
+        public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
+            StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
+            StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
+        {
+            // Topmost property is not supported
+            var bindings = new List<IDisposable>();
+
+            void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
+            Bind(WidthProperty, widthProperty);
+            Bind(MinWidthProperty, minWidthProperty);
+            Bind(MaxWidthProperty, maxWidthProperty);
+            Bind(HeightProperty, heightProperty);
+            Bind(MinHeightProperty, minHeightProperty);
+            Bind(MaxHeightProperty, maxHeightProperty);
+            
+            return Disposable.Create(() =>
+            {
+                foreach (var x in bindings)
+                    x.Dispose();
+            });
+        }
+
+        public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
+            PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None)
+        {
+            _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor,
+                gravity);
+            UpdatePosition();
+        }
+
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            if (_positionerParameters.Size != finalSize)
+            {
+                _positionerParameters.Size = finalSize;
+                UpdatePosition();
+            }
+            return base.ArrangeOverride(finalSize);
+        }
+
+
+        private void UpdatePosition()
+        {
+            // Don't bother the positioner with layout system artifacts
+            if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0)
+                return;
+            if (_shown)
+            {
+                _positioner.Update(_positionerParameters);
+            }
+        }
+
+        IReadOnlyList<ManagedPopupPositionerScreenInfo> IManagedPopupPositionerPopup.Screens
+        {
+            get
+            {
+                var rc = new Rect(default, _overlayLayer.AvailableSize);
+                return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)};
+            }
+        }
+
+        Rect IManagedPopupPositionerPopup.ParentClientAreaScreenGeometry =>
+            new Rect(default, _overlayLayer.Bounds.Size);
+
+        void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize)
+        {
+            _lastRequestedPosition = devicePoint;
+            Dispatcher.UIThread.Post(() =>
+            {
+                OverlayLayer.SetLeft(this, _lastRequestedPosition.X);
+                OverlayLayer.SetTop(this, _lastRequestedPosition.Y);
+            }, DispatcherPriority.Layout);
+        }
+
+        Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt;
+
+        Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size;
+        
+        public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver)
+        {
+            var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup();
+            if (platform != null)
+                return new PopupRoot((TopLevel)target.GetVisualRoot(), platform, dependencyResolver);
+            
+            var overlayLayer = OverlayLayer.GetOverlayLayer(target);
+            if (overlayLayer == null)
+                throw new InvalidOperationException(
+                    "Unable to create IPopupImpl and no overlay layer is found for the target control");
+
+
+            return new OverlayPopupHost(overlayLayer);
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size));
+        }
+    }
+}

+ 144 - 140
src/Avalonia.Controls/Primitives/Popup.cs

@@ -2,7 +2,12 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Presenters;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
@@ -42,7 +47,7 @@ namespace Avalonia.Controls.Primitives
         /// Defines the <see cref="ObeyScreenEdges"/> property.
         /// </summary>
         public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
-            AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges));
+            AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
 
         /// <summary>
         /// Defines the <see cref="HorizontalOffset"/> property.
@@ -75,10 +80,12 @@ namespace Avalonia.Controls.Primitives
             AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
 
         private bool _isOpen;
-        private PopupRoot _popupRoot;
+        private IPopupHost _popupHost;
         private TopLevel _topLevel;
         private IDisposable _nonClientListener;
+        private IDisposable _presenterSubscription;
         bool _ignoreIsOpenChanged = false;
+        private List<IDisposable> _bindings = new List<IDisposable>();
 
         /// <summary>
         /// Initializes static members of the <see cref="Popup"/> class.
@@ -88,7 +95,11 @@ namespace Avalonia.Controls.Primitives
             IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
             ChildProperty.Changed.AddClassHandler<Popup>(x => x.ChildChanged);
             IsOpenProperty.Changed.AddClassHandler<Popup>(x => x.IsOpenChanged);
-            TopmostProperty.Changed.AddClassHandler<Popup>((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue);
+        }
+
+        public Popup()
+        {
+            
         }
 
         /// <summary>
@@ -101,10 +112,7 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public event EventHandler Opened;
 
-        /// <summary>
-        /// Raised when the popup root has been created, but before it has been shown.
-        /// </summary>
-        public event EventHandler PopupRootCreated;
+        public IPopupHost Host => _popupHost;
 
         /// <summary>
         /// Gets or sets the control to display in the popup.
@@ -147,10 +155,7 @@ namespace Avalonia.Controls.Primitives
             set { SetValue(PlacementModeProperty, value); }
         }
 
-        /// <summary>
-        /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary
-        /// when its opened at a position where it would otherwise overlap the screen edge.
-        /// </summary>
+        [Obsolete("This property has no effect")]
         public bool ObeyScreenEdges
         {
             get => GetValue(ObeyScreenEdgesProperty);
@@ -184,11 +189,6 @@ namespace Avalonia.Controls.Primitives
             set { SetValue(PlacementTargetProperty, value); }
         }
 
-        /// <summary>
-        /// Gets the root of the popup window.
-        /// </summary>
-        public PopupRoot PopupRoot => _popupRoot;
-
         /// <summary>
         /// Gets or sets a value indicating whether the popup should stay open when the popup is
         /// pressed or loses focus.
@@ -211,63 +211,58 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets the root of the popup window.
         /// </summary>
-        IVisual IVisualTreeHost.Root => _popupRoot;
+        IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot;
 
         /// <summary>
         /// Opens the popup.
         /// </summary>
         public void Open()
         {
-            if (_popupRoot == null)
+            // Popup is currently open
+            if (_topLevel != null)
+                return;
+            CloseCurrent();
+            var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().FirstOrDefault();
+            if (placementTarget == null)
+                throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null");
+            
+            _topLevel = placementTarget.GetVisualRoot() as TopLevel;
+
+            if (_topLevel == null)
             {
-                _popupRoot = new PopupRoot(DependencyResolver)
-                {
-                    [~ContentControl.ContentProperty] = this[~ChildProperty],
-                    [~WidthProperty] = this[~WidthProperty],
-                    [~HeightProperty] = this[~HeightProperty],
-                    [~MinWidthProperty] = this[~MinWidthProperty],
-                    [~MaxWidthProperty] = this[~MaxWidthProperty],
-                    [~MinHeightProperty] = this[~MinHeightProperty],
-                    [~MaxHeightProperty] = this[~MaxHeightProperty],
-                };
-
-                ((ISetLogicalParent)_popupRoot).SetParent(this);
+                throw new InvalidOperationException(
+                    "Attempted to open a popup not attached to a TopLevel");
             }
 
-            _popupRoot.Position = GetPosition();
+            _popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
+
+            _bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
+                HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
 
-            if (_topLevel == null && PlacementTarget != null)
+            _popupHost.SetChild(Child);
+            ((ISetLogicalParent)_popupHost).SetParent(this);
+            _popupHost.ConfigurePosition(placementTarget,
+                PlacementMode, new Point(HorizontalOffset, VerticalOffset));
+            _popupHost.TemplateApplied += RootTemplateApplied;
+            
+            var window = _topLevel as Window;
+            if (window != null)
             {
-                _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel;
+                window.Deactivated += WindowDeactivated;
             }
-
-            if (_topLevel != null)
+            else
             {
-                var window = _topLevel as Window;
-                if (window != null)
+                var parentPopuproot = _topLevel as PopupRoot;
+                if (parentPopuproot?.Parent is Popup popup)
                 {
-                    window.Deactivated += WindowDeactivated;
+                    popup.Closed += ParentClosed;
                 }
-                else
-                {
-                    var parentPopuproot = _topLevel as PopupRoot;
-                    if (parentPopuproot?.Parent is Popup popup)
-                    {
-                        popup.Closed += ParentClosed;
-                    }
-                }
-                _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
-                _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick);
             }
+            _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
+            _nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick);
+        
 
-            PopupRootCreated?.Invoke(this, EventArgs.Empty);
-
-            _popupRoot.Show();
-
-            if (ObeyScreenEdges)
-            {
-                _popupRoot.SnapInsideScreenEdges();
-            }
+            _popupHost.Show();
 
             using (BeginIgnoringIsOpen())
             {
@@ -282,29 +277,14 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public void Close()
         {
-            if (_popupRoot != null)
+            if (_popupHost != null)
             {
-                if (_topLevel != null)
-                {
-                    _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
-                    var window = _topLevel as Window;
-                    if (window != null)
-                        window.Deactivated -= WindowDeactivated;
-                    else
-                    {
-                        var parentPopuproot = _topLevel as PopupRoot;
-                        if (parentPopuproot?.Parent is Popup popup)
-                        {
-                            popup.Closed -= ParentClosed;
-                        }
-                    }
-                    _nonClientListener?.Dispose();
-                    _nonClientListener = null;
-                }
-
-                _popupRoot.Hide();
+                _popupHost.TemplateApplied -= RootTemplateApplied;
             }
 
+            _presenterSubscription?.Dispose();
+
+            CloseCurrent();
             using (BeginIgnoringIsOpen())
             {
                 IsOpen = false;
@@ -313,6 +293,41 @@ namespace Avalonia.Controls.Primitives
             Closed?.Invoke(this, EventArgs.Empty);
         }
 
+        void CloseCurrent()
+        {
+            if (_topLevel != null)
+            {
+                _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
+                var window = _topLevel as Window;
+                if (window != null)
+                    window.Deactivated -= WindowDeactivated;
+                else
+                {
+                    var parentPopuproot = _topLevel as PopupRoot;
+                    if (parentPopuproot?.Parent is Popup popup)
+                    {
+                        popup.Closed -= ParentClosed;
+                    }
+                }
+                _nonClientListener?.Dispose();
+                _nonClientListener = null;
+                
+                _topLevel = null;
+            }
+            if (_popupHost != null)
+            {
+                foreach(var b in _bindings)
+                    b.Dispose();
+                _bindings.Clear();
+                _popupHost.SetChild(null);
+                _popupHost.Hide();
+                ((ISetLogicalParent)_popupHost).SetParent(null);
+                _popupHost.Dispose();
+                _popupHost = null;
+            }
+
+        }
+
         /// <summary>
         /// Measures the control.
         /// </summary>
@@ -323,27 +338,14 @@ namespace Avalonia.Controls.Primitives
             return new Size();
         }
 
-        /// <inheritdoc/>
-        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
-        {
-            base.OnAttachedToLogicalTree(e);
-            _topLevel = e.Root as TopLevel;
-        }
-
         /// <inheritdoc/>
         protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnDetachedFromLogicalTree(e);
-            _topLevel = null;
-
-            if (_popupRoot != null)
-            {
-                ((ISetLogicalParent)_popupRoot).SetParent(null);
-                _popupRoot.Dispose();
-                _popupRoot = null;
-            }
+            Close();
         }
 
+
         /// <summary>
         /// Called when the <see cref="IsOpen"/> property changes.
         /// </summary>
@@ -380,49 +382,6 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        /// <summary>
-        /// Gets the position for the popup based on the placement properties.
-        /// </summary>
-        /// <returns>The popup's position in screen coordinates.</returns>
-        protected virtual PixelPoint GetPosition()
-        {
-            var result = GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot,
-                HorizontalOffset, VerticalOffset);
-
-            return result;
-        }
-
-        internal static PixelPoint GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
-        {
-            var root = target?.GetVisualRoot();
-            var mode = root != null ? placement : PlacementMode.Pointer;
-            var scaling = root?.RenderScaling ?? 1;
-
-            switch (mode)
-            {
-                case PlacementMode.Pointer:
-                    if (popupRoot != null)
-                    {
-                        var screenOffset = PixelPoint.FromPoint(new Point(horizontalOffset, verticalOffset), scaling);
-                        var mouseOffset = ((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default;
-                        return new PixelPoint(
-                            screenOffset.X + mouseOffset.X,
-                            screenOffset.Y + mouseOffset.Y);
-                    }
-
-                    return default;
-
-                case PlacementMode.Bottom:
-                    return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? default;
-
-                case PlacementMode.Right:
-                    return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? default;
-
-                default:
-                    throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
-            }
-        }
-
         private void ListenForNonClientClick(RawInputEventArgs e)
         {
             var mouse = e as RawPointerEventArgs;
@@ -445,17 +404,62 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        private bool IsChildOrThis(IVisual child)
+        private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e)
         {
-            IVisual root = child.GetVisualRoot();
-            while (root is PopupRoot)
+            _popupHost.TemplateApplied -= RootTemplateApplied;
+
+            if (_presenterSubscription != null)
             {
-                if (root == PopupRoot) return true;
-                root = ((PopupRoot)root).Parent.GetVisualRoot();
+                _presenterSubscription.Dispose();
+                _presenterSubscription = null;
+            }
+
+            // If the Popup appears in a control template, then the child controls
+            // that appear in the popup host need to have their TemplatedParent
+            // properties set.
+            if (TemplatedParent != null)
+            {
+                _popupHost.Presenter?.ApplyTemplate();
+                _popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty)
+                    .Subscribe(SetTemplatedParentAndApplyChildTemplates);
+            }
+        }
+
+        private void SetTemplatedParentAndApplyChildTemplates(IControl control)
+        {
+            if (control != null)
+            {
+                var templatedParent = TemplatedParent;
+
+                if (control.TemplatedParent == null)
+                {
+                    control.SetValue(TemplatedParentProperty, templatedParent);
+                }
+
+                control.ApplyTemplate();
+
+                if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
+                {
+                    foreach (IControl child in control.GetVisualChildren())
+                    {
+                        SetTemplatedParentAndApplyChildTemplates(child);
+                    }
+                }
             }
-            return false;
         }
 
+        private bool IsChildOrThis(IVisual child)
+        {
+            return _popupHost != null && ((IVisual)_popupHost).FindCommonVisualAncestor(child) == _popupHost;
+        }
+        
+        public bool IsInsidePopup(IVisual visual)
+        {
+            return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true;
+        }
+
+        public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver;
+
         private void WindowDeactivated(object sender, EventArgs e)
         {
             if (!StaysOpen)

+ 358 - 0
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@@ -0,0 +1,358 @@
+// The documentation and flag names in this file are initially taken from
+// xdg_shell wayland protocol this API is designed after
+// therefore, I'm including the license from wayland-protocols repo
+
+/* 
+Copyright © 2008-2013 Kristian Høgsberg
+Copyright © 2010-2013 Intel Corporation
+Copyright © 2013      Rafael Antognolli
+Copyright © 2013      Jasper St. Pierre
+Copyright © 2014      Jonas Ådahl
+Copyright © 2014      Jason Ekstrand
+Copyright © 2014-2015 Collabora, Ltd.
+Copyright © 2015      Red Hat Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice (including the next
+paragraph) shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+---
+
+The above is the version of the MIT "Expat" License used by X.org:
+
+    http://cgit.freedesktop.org/xorg/xserver/tree/COPYING
+    
+    
+Adjustments for Avalonia needs:
+Copyright © 2019 Nikita Tsukanov
+    
+    
+*/
+
+using System;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives.PopupPositioning
+{
+    /// <summary>
+    /// 
+    /// The IPopupPositioner provides a collection of rules for the placement of a
+    /// a popup relative to its parent. Rules can be defined to ensure
+    /// the popup remains within the visible area's borders, and to
+    /// specify how the popup changes its position, such as sliding along
+    /// an axis, or flipping around a rectangle. These positioner-created rules are
+    /// constrained by the requirement that a popup must intersect with or
+    /// be at least partially adjacent to its parent surface.
+    /// </summary>
+    public struct PopupPositionerParameters
+    {
+        private PopupPositioningEdge _gravity;
+        private PopupPositioningEdge _anchor;
+
+        /// <summary>
+        /// Set the size of the popup that is to be positioned with the positioner
+        /// object. The size is in scaled coordinates.
+        /// </summary>
+        public Size Size { get; set; }
+
+        /// <summary>
+        /// Specify the anchor rectangle within the parent that the popup
+        /// will be placed relative to. The rectangle is relative to the
+        /// parent geometry
+        /// 
+        /// The anchor rectangle may not extend outside the window geometry of the
+        /// popup's parent. The anchor rectangle is in scaled coordinates
+        /// </summary>
+        public Rect AnchorRectangle { get; set; }
+
+
+        /// <summary>
+        /// Defines the anchor point for the anchor rectangle. The specified anchor
+        /// is used derive an anchor point that the popup will be
+        /// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or
+        /// 'BottomRight'), the anchor point will be at the specified corner;
+        /// otherwise, the derived anchor point will be centered on the specified
+        /// edge, or in the center of the anchor rectangle if no edge is specified.
+        /// </summary>
+        public PopupPositioningEdge Anchor
+        {
+            get => _anchor;
+            set
+            {
+                PopupPositioningEdgeHelper.ValidateEdge(value);
+                _anchor = value;
+            }
+        }
+
+        /// <summary>
+        /// Defines in what direction a popup should be positioned, relative to
+        /// the anchor point of the parent. If a corner gravity is
+        /// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup
+        /// will be placed towards the specified gravity; otherwise, the popup
+        /// will be centered over the anchor point on any axis that had no
+        /// gravity specified.
+        /// </summary>
+        public PopupPositioningEdge Gravity
+        {
+            get => _gravity;
+            set
+            {
+                PopupPositioningEdgeHelper.ValidateEdge(value);
+                _gravity = value;
+            }
+        }
+
+        /// <summary>
+        /// Specify how the popup should be positioned if the originally intended
+        /// position caused the popup to be constrained, meaning at least
+        /// partially outside positioning boundaries set by the positioner. The
+        /// adjustment is set by constructing a bitmask describing the adjustment to
+        /// be made when the popup is constrained on that axis.
+        /// 
+        /// If no bit for one axis is set, the positioner will assume that the child
+        /// surface should not change its position on that axis when constrained.
+        /// 
+        /// If more than one bit for one axis is set, the order of how adjustments
+        /// are applied is specified in the corresponding adjustment descriptions.
+        /// 
+        /// The default adjustment is none.
+        /// </summary>
+        public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
+        
+        /// <summary>
+        /// Specify the popup position offset relative to the position of the
+        /// anchor on the anchor rectangle and the anchor on the popup. For
+        /// example if the anchor of the anchor rectangle is at (x, y), the popup
+        /// has the gravity bottom|right, and the offset is (ox, oy), the calculated
+        /// surface position will be (x + ox, y + oy). The offset position of the
+        /// surface is the one used for constraint testing. See
+        /// set_constraint_adjustment.
+        /// 
+        /// An example use case is placing a popup menu on top of a user interface
+        /// element, while aligning the user interface element of the parent surface
+        /// with some user interface element placed somewhere in the popup.
+        /// </summary>
+        public Point Offset { get; set; }
+    }
+    
+    /// <summary>
+    /// The constraint adjustment value define ways how popup position will
+    /// be adjusted if the unadjusted position would result in the popup
+    /// being partly constrained.
+    /// 
+    /// Whether a popup is considered 'constrained' is left to the positioner
+    /// to determine. For example, the popup may be partly outside the
+    /// target platform defined 'work area', thus necessitating the popup's
+    /// position be adjusted until it is entirely inside the work area.
+    /// </summary>
+    [Flags]
+    public enum PopupPositionerConstraintAdjustment
+    {
+        /// <summary>
+        /// Don't alter the surface position even if it is constrained on some
+        /// axis, for example partially outside the edge of an output.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Slide the surface along the x axis until it is no longer constrained.
+        ///        First try to slide towards the direction of the gravity on the x axis
+        ///        until either the edge in the opposite direction of the gravity is
+        ///        unconstrained or the edge in the direction of the gravity is
+        ///        constrained.
+        ///
+        ///        Then try to slide towards the opposite direction of the gravity on the
+        ///        x axis until either the edge in the direction of the gravity is
+        ///        unconstrained or the edge in the opposite direction of the gravity is
+        ///        constrained.
+        /// </summary>
+        SlideX = 1,
+
+
+        /// <summary>
+        ///            Slide the surface along the y axis until it is no longer constrained.
+        /// 
+        /// First try to slide towards the direction of the gravity on the y axis
+        /// until either the edge in the opposite direction of the gravity is
+        /// unconstrained or the edge in the direction of the gravity is
+        /// constrained.
+        /// 
+        /// Then try to slide towards the opposite direction of the gravity on the
+        /// y axis until either the edge in the direction of the gravity is
+        /// unconstrained or the edge in the opposite direction of the gravity is
+        /// constrained.
+        /// */
+        /// </summary>
+        SlideY = 2,
+
+        /// <summary>
+        /// Invert the anchor and gravity on the x axis if the surface is
+        /// constrained on the x axis. For example, if the left edge of the
+        /// surface is constrained, the gravity is 'left' and the anchor is
+        /// 'left', change the gravity to 'right' and the anchor to 'right'.
+        /// 
+        /// If the adjusted position also ends up being constrained, the resulting
+        /// position of the flip_x adjustment will be the one before the
+        /// adjustment.
+        /// </summary>
+        FlipX = 4,
+
+        /// <summary>
+        /// Invert the anchor and gravity on the y axis if the surface is
+        /// constrained on the y axis. For example, if the bottom edge of the
+        /// surface is constrained, the gravity is 'bottom' and the anchor is
+        /// 'bottom', change the gravity to 'top' and the anchor to 'top'.
+        /// 
+        /// The adjusted position is calculated given the original anchor
+        /// rectangle and offset, but with the new flipped anchor and gravity
+        /// values.
+        /// 
+        /// If the adjusted position also ends up being constrained, the resulting
+        /// position of the flip_y adjustment will be the one before the
+        /// adjustment.
+        /// </summary>
+        FlipY = 8,
+        All = SlideX|SlideY|FlipX|FlipY
+    }
+
+    static class PopupPositioningEdgeHelper
+    {
+        public static void ValidateEdge(this PopupPositioningEdge edge)
+        {
+            if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0)
+                ||
+                ((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0))
+                throw new ArgumentException("Opposite edges specified");
+        }
+
+        public static PopupPositioningEdge Flip(this PopupPositioningEdge edge)
+        {
+            var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right;
+            var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom;
+            if ((edge & hmask) != 0)
+                edge ^= hmask;
+            if ((edge & vmask) != 0)
+                edge ^= vmask;
+            return edge;
+        }
+
+        public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge)
+        {
+            if ((edge & PopupPositioningEdge.HorizontalMask) != 0)
+                edge ^= PopupPositioningEdge.HorizontalMask;
+            return edge;
+        }
+        
+        public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge)
+        {
+            if ((edge & PopupPositioningEdge.VerticalMask) != 0)
+                edge ^= PopupPositioningEdge.VerticalMask;
+            return edge;
+        }
+        
+    }
+
+    [Flags]
+    public enum PopupPositioningEdge
+    {
+        None,
+        Top = 1,
+        Bottom = 2,
+        Left = 4,
+        Right = 8,
+        TopLeft = Top | Left,
+        TopRight = Top | Right,
+        BottomLeft = Bottom | Left,
+        BottomRight = Bottom | Right,
+
+        
+        VerticalMask = Top | Bottom,
+        HorizontalMask = Left | Right,
+        AllMask = VerticalMask|HorizontalMask
+    }
+
+    public interface IPopupPositioner
+    {
+        void Update(PopupPositionerParameters parameters);
+    }
+
+    static class PopupPositionerExtensions
+    {
+        public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
+            TopLevel topLevel,
+            IVisual target, PlacementMode placement, Point offset,
+            PopupPositioningEdge anchor, PopupPositioningEdge gravity)
+        {
+            // We need a better way for tracking the last pointer position
+            var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position);
+            
+            positionerParameters.Offset = offset;
+            positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All;
+            if (placement == PlacementMode.Pointer)
+            {
+                positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1));
+                positionerParameters.Anchor = PopupPositioningEdge.BottomRight;
+                positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+            }
+            else
+            {
+                if (target == null)
+                    throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
+                var matrix = target.TransformToVisual(topLevel);
+                if (matrix == null)
+                {
+                    if (target.GetVisualRoot() == null)
+                        throw new InvalidCastException("Target control is not attached to the visual tree");
+                    throw new InvalidCastException("Target control is not in the same tree as the popup parent");
+                }
+
+                positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size)
+                    .TransformToAABB(matrix.Value);
+
+                if (placement == PlacementMode.Right)
+                {
+                    positionerParameters.Anchor = PopupPositioningEdge.TopRight;
+                    positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+                }
+                else if (placement == PlacementMode.Bottom)
+                {
+                    positionerParameters.Anchor = PopupPositioningEdge.BottomLeft;
+                    positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+                }
+                else if (placement == PlacementMode.Left)
+                {
+                    positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
+                    positionerParameters.Gravity = PopupPositioningEdge.BottomLeft;
+                }
+                else if (placement == PlacementMode.Top)
+                {
+                    positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
+                    positionerParameters.Gravity = PopupPositioningEdge.TopRight;
+                }
+                else if (placement == PlacementMode.AnchorAndGravity)
+                {
+                    positionerParameters.Anchor = anchor;
+                    positionerParameters.Gravity = gravity;
+                }
+                else
+                    throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
+            }
+        }
+    }
+
+}

+ 175 - 0
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Controls.Primitives.PopupPositioning
+{
+    public interface IManagedPopupPositionerPopup
+    {
+        IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens { get; }
+        Rect ParentClientAreaScreenGeometry { get; }
+        void MoveAndResize(Point devicePoint, Size virtualSize);
+        Point TranslatePoint(Point pt);
+        Size TranslateSize(Size size);
+    }
+
+    public class ManagedPopupPositionerScreenInfo
+    {
+        public Rect Bounds { get; }
+        public Rect WorkingArea { get; }
+
+        public ManagedPopupPositionerScreenInfo(Rect bounds, Rect workingArea)
+        {
+            Bounds = bounds;
+            WorkingArea = workingArea;
+        }
+    }
+
+    public class ManagedPopupPositioner : IPopupPositioner
+    {
+        private readonly IManagedPopupPositionerPopup _popup;
+
+        public ManagedPopupPositioner(IManagedPopupPositionerPopup popup)
+        {
+            _popup = popup;
+        }
+
+
+        private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge)
+        {
+            double x, y;
+            if ((edge & PopupPositioningEdge.Left) != 0)
+                x = anchorRect.X;
+            else if ((edge & PopupPositioningEdge.Right) != 0)
+                x = anchorRect.Right;
+            else
+                x = anchorRect.X + anchorRect.Width / 2;
+            
+            if ((edge & PopupPositioningEdge.Top) != 0)
+                y = anchorRect.Y;
+            else if ((edge & PopupPositioningEdge.Bottom) != 0)
+                y = anchorRect.Bottom;
+            else
+                y = anchorRect.Y + anchorRect.Height / 2;
+            return new Point(x, y);
+        }
+
+        private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity)
+        {
+            double x, y;
+            if ((gravity & PopupPositioningEdge.Left) != 0)
+                x = -size.Width;
+            else if ((gravity & PopupPositioningEdge.Right) != 0)
+                x = 0;
+            else
+                x = -size.Width / 2;
+            
+            if ((gravity & PopupPositioningEdge.Top) != 0)
+                y = -size.Height;
+            else if ((gravity & PopupPositioningEdge.Bottom) != 0)
+                y = 0;
+            else
+                y = -size.Height / 2;
+            return anchorPoint + new Point(x, y);
+        }
+
+        public void Update(PopupPositionerParameters parameters)
+        {
+
+            Update(_popup.TranslateSize(parameters.Size), parameters.Size,
+                new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft),
+                    _popup.TranslateSize(parameters.AnchorRectangle.Size)),
+                parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment,
+                _popup.TranslatePoint(parameters.Offset));
+        }
+
+        
+        private void Update(Size translatedSize, Size originalSize,
+            Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity,
+            PopupPositionerConstraintAdjustment constraintAdjustment, Point offset)
+        {
+            var parentGeometry = _popup.ParentClientAreaScreenGeometry;
+            anchorRect = anchorRect.Translate(parentGeometry.TopLeft);
+            
+            Rect GetBounds()
+            {
+                var screens = _popup.Screens;
+                
+                var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft))
+                                   ?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect))
+                                   ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft))
+                                   ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry))
+                                   ?? screens.FirstOrDefault();
+                return targetScreen?.WorkingArea
+                       ?? new Rect(0, 0, double.MaxValue, double.MaxValue);
+            }
+
+            var bounds = GetBounds();
+
+            bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask)
+            {
+                if ((edge & PopupPositioningEdge.Left) != 0
+                    && rc.X < bounds.X)
+                    return false;
+
+                if ((edge & PopupPositioningEdge.Top) != 0
+                    && rc.Y < bounds.Y)
+                    return false;
+
+                if ((edge & PopupPositioningEdge.Right) != 0
+                    && rc.Right > bounds.Right)
+                    return false;
+
+                if ((edge & PopupPositioningEdge.Bottom) != 0
+                    && rc.Bottom > bounds.Bottom)
+                    return false;
+
+                return true;
+            }
+
+            Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) =>
+                new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize);
+
+
+            var geo = GetUnconstrained(anchor, gravity);
+
+            // If flipping geometry and anchor is allowed and helps, use the flipped one,
+            // otherwise leave it as is
+            if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask)
+                && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0)
+            {
+                var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
+                if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask))
+                    geo = geo.WithX(flipped.X);
+            }
+
+            // If sliding is allowed, try moving the rect into the bounds
+            if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0)
+            {
+                geo = geo.WithX(Math.Max(geo.X, bounds.X));
+                if (geo.Right > bounds.Right)
+                    geo = geo.WithX(bounds.Right - geo.Width);
+            }
+            
+            // If flipping geometry and anchor is allowed and helps, use the flipped one,
+            // otherwise leave it as is
+            if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask)
+                && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0)
+            {
+                var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
+                if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask))
+                    geo = geo.WithY(flipped.Y);
+            }
+
+            // If sliding is allowed, try moving the rect into the bounds
+            if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0)
+            {
+                geo = geo.WithY(Math.Max(geo.Y, bounds.Y));
+                if (geo.Bottom > bounds.Bottom)
+                    geo = geo.WithY(bounds.Bottom - geo.Height);
+            }
+
+            _popup.MoveAndResize(geo.TopLeft, originalSize);
+        }
+    }
+}

+ 50 - 0
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs

@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Platform;
+
+namespace Avalonia.Controls.Primitives.PopupPositioning
+{
+    /// <summary>
+    /// This class is used to simplify integration of IPopupImpl implementations with popup positioner
+    /// </summary>
+    public class ManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup 
+    {
+        private readonly IWindowBaseImpl _parent;
+
+        public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling);
+        private readonly MoveResizeDelegate _moveResize;
+
+        public ManagedPopupPositionerPopupImplHelper(IWindowBaseImpl parent, MoveResizeDelegate moveResize)
+        {
+            _parent = parent;
+            _moveResize = moveResize;
+        }
+
+        public IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens =>
+
+            _parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo(
+                s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))).ToList();
+
+        public Rect ParentClientAreaScreenGeometry
+        {
+            get
+            {
+                // Popup positioner operates with abstract coordinates, but in our case they are pixel ones
+                var point = _parent.PointToScreen(default);
+                var size = PixelSize.FromSize(_parent.ClientSize, _parent.Scaling);
+                return new Rect(point.X, point.Y, size.Width, size.Height);
+
+            }
+        }
+
+        public void MoveAndResize(Point devicePoint, Size virtualSize)
+        {
+            _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling);
+        }
+
+        public Point TranslatePoint(Point pt) => pt * _parent.Scaling;
+
+        public Size TranslateSize(Size size) => size * _parent.Scaling;
+    }
+}

+ 55 - 64
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -2,8 +2,9 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using Avalonia.Controls.Platform;
-using Avalonia.Controls.Presenters;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.Platform;
@@ -16,9 +17,10 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// The root window of a <see cref="Popup"/>.
     /// </summary>
-    public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost
+    public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost
     {
-        private IDisposable _presenterSubscription;
+        private readonly TopLevel _parent;
+        private PopupPositionerParameters _positionerParameters;
 
         /// <summary>
         /// Initializes static members of the <see cref="PopupRoot"/> class.
@@ -31,8 +33,8 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Initializes a new instance of the <see cref="PopupRoot"/> class.
         /// </summary>
-        public PopupRoot()
-            : this(null)
+        public PopupRoot(TopLevel parent, IPopupImpl impl)
+            : this(parent, impl,null)
         {
         }
 
@@ -42,9 +44,10 @@ namespace Avalonia.Controls.Primitives
         /// <param name="dependencyResolver">
         /// The dependency resolver to use. If null the default dependency resolver will be used.
         /// </param>
-        public PopupRoot(IAvaloniaDependencyResolver dependencyResolver)
-            : base(PlatformManager.CreatePopup(), dependencyResolver)
+        public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver)
+            : base(impl, dependencyResolver)
         {
+            _parent = parent;
         }
 
         /// <summary>
@@ -74,73 +77,61 @@ namespace Avalonia.Controls.Primitives
         /// <inheritdoc/>
         public void Dispose() => PlatformImpl?.Dispose();
 
-        /// <summary>
-        /// Moves the Popups position so that it doesnt overlap screen edges.
-        /// This method can be called immediately after Show has been called.
-        /// </summary>
-        public void SnapInsideScreenEdges()
+        private void UpdatePosition()
         {
-            var screen = (VisualRoot as WindowBase)?.Screens?.ScreenFromPoint(Position);
-
-            if (screen != null)
-            {
-                var scaling = VisualRoot.RenderScaling;
-                var bounds = PixelRect.FromRect(Bounds, scaling);
-                var screenX = Position.X + bounds.Width - screen.Bounds.X;
-                var screenY = Position.Y + bounds.Height - screen.Bounds.Y;
-
-                if (screenX > screen.Bounds.Width)
-                {
-                    Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
-                }
-
-                if (screenY > screen.Bounds.Height)
-                {
-                    Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
-                }
-            }
+            PlatformImpl?.PopupPositioner.Update(_positionerParameters);
         }
 
-        /// <inheritdoc/>
-        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
+            PopupPositioningEdge anchor = PopupPositioningEdge.None,
+            PopupPositioningEdge gravity = PopupPositioningEdge.None)
         {
-            base.OnTemplateApplied(e);
+            _positionerParameters.ConfigurePosition(_parent, target,
+                placement, offset, anchor, gravity);
+
+            if (_positionerParameters.Size != default)
+                UpdatePosition();
+        }
+
+        public void SetChild(IControl control) => Content = control;
 
-            if (Parent?.TemplatedParent != null)
+        IVisual IPopupHost.HostedVisualTreeRoot => this;
+        
+        public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
+            StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
+            StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
+        {
+            var bindings = new List<IDisposable>();
+
+            void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
+            Bind(WidthProperty, widthProperty);
+            Bind(MinWidthProperty, minWidthProperty);
+            Bind(MaxWidthProperty, maxWidthProperty);
+            Bind(HeightProperty, heightProperty);
+            Bind(MinHeightProperty, minHeightProperty);
+            Bind(MaxHeightProperty, maxHeightProperty);
+            Bind(TopmostProperty, topmostProperty);
+            return Disposable.Create(() =>
             {
-                if (_presenterSubscription != null)
-                {
-                    _presenterSubscription.Dispose();
-                    _presenterSubscription = null;
-                }
-
-                Presenter?.ApplyTemplate();
-                Presenter?.GetObservable(ContentPresenter.ChildProperty)
-                    .Subscribe(SetTemplatedParentAndApplyChildTemplates);
-            }
+                foreach (var x in bindings)
+                    x.Dispose();
+            });
         }
 
-        private void SetTemplatedParentAndApplyChildTemplates(IControl control)
+        /// <summary>
+        /// Carries out the arrange pass of the window.
+        /// </summary>
+        /// <param name="finalSize">The final window size.</param>
+        /// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
+        protected override Size ArrangeOverride(Size finalSize)
         {
-            if (control != null)
+            using (BeginAutoSizing())
             {
-                var templatedParent = Parent.TemplatedParent;
-
-                if (control.TemplatedParent == null)
-                {
-                    control.SetValue(TemplatedParentProperty, templatedParent);
-                }
-
-                control.ApplyTemplate();
-
-                if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
-                {
-                    foreach (IControl child in control.GetVisualChildren())
-                    {
-                        SetTemplatedParentAndApplyChildTemplates(child);
-                    }
-                }
+                _positionerParameters.Size = finalSize;
+                UpdatePosition();
             }
+
+            return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
         }
     }
 }

+ 93 - 0
src/Avalonia.Controls/Primitives/VisualLayerManager.cs

@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using Avalonia.LogicalTree;
+using Avalonia.Styling;
+
+namespace Avalonia.Controls.Primitives
+{
+    public class VisualLayerManager : Decorator
+    {
+        private const int AdornerZIndex = int.MaxValue - 100;
+        private const int OverlayZIndex = int.MaxValue - 99;
+        private IStyleHost _styleRoot;
+        private readonly List<Control> _layers = new List<Control>();
+        
+
+        public bool IsPopup { get; set; }
+        
+        public AdornerLayer AdornerLayer
+        {
+            get
+            {
+                var rv = FindLayer<AdornerLayer>();
+                if (rv == null)
+                    AddLayer(rv = new AdornerLayer(), AdornerZIndex);
+                return rv;
+            }
+        }
+
+        public OverlayLayer OverlayLayer
+        {
+            get
+            {
+                if (IsPopup)
+                    return null;
+                var rv = FindLayer<OverlayLayer>();
+                if(rv == null)
+                    AddLayer(rv = new OverlayLayer(), OverlayZIndex);
+                return rv;
+            }
+        }
+
+        T FindLayer<T>() where T : class
+        {
+            foreach (var layer in _layers)
+                if (layer is T match)
+                    return match;
+            return null;
+        }
+
+        void AddLayer(Control layer, int zindex)
+        {
+            _layers.Add(layer);
+            ((ISetLogicalParent)layer).SetParent(this);
+            layer.ZIndex = zindex;
+            VisualChildren.Add(layer);
+            if (((ILogical)this).IsAttachedToLogicalTree)
+                ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot));
+            InvalidateArrange();
+        }
+        
+        
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToLogicalTree(e);
+            _styleRoot = e.Root;
+
+            foreach (var l in _layers)
+                ((ILogical)l).NotifyAttachedToLogicalTree(e);
+        }
+
+        protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            _styleRoot = null;
+            base.OnDetachedFromLogicalTree(e);
+            foreach (var l in _layers)
+                ((ILogical)l).NotifyDetachedFromLogicalTree(e);
+        }
+
+
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            foreach (var l in _layers)
+                l.Measure(availableSize);
+            return base.MeasureOverride(availableSize);
+        }
+
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            foreach (var l in _layers)
+                l.Arrange(new Rect(finalSize));
+            return base.ArrangeOverride(finalSize);
+        }
+    }
+}

+ 8 - 6
src/Avalonia.Controls/ToolTip.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Reactive.Linq;
 using Avalonia.Controls.Primitives;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -60,7 +61,7 @@ namespace Avalonia.Controls
         private static readonly AttachedProperty<ToolTip> ToolTipProperty =
             AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip>("ToolTip");
 
-        private PopupRoot _popup;
+        private IPopupHost _popup;
 
         /// <summary>
         /// Initializes static members of the <see cref="ToolTip"/> class.
@@ -234,19 +235,20 @@ namespace Avalonia.Controls
         {
             Close();
 
-            _popup = new PopupRoot { Content = this,  };
+            _popup = OverlayPopupHost.CreatePopupHost(control, null);
+            _popup.SetChild(this);
             ((ISetLogicalParent)_popup).SetParent(control);
-            _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup,
-                GetHorizontalOffset(control), GetVerticalOffset(control));
+            
+            _popup.ConfigurePosition(control, GetPlacement(control), 
+                new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
             _popup.Show();
-            _popup.SnapInsideScreenEdges();
         }
 
         private void Close()
         {
             if (_popup != null)
             {
-                _popup.Content = null;
+                _popup.SetChild(null);
                 _popup.Hide();
                 _popup = null;
             }

+ 45 - 0
src/Avalonia.Controls/Window.cs

@@ -135,6 +135,12 @@ namespace Avalonia.Controls
 
             WindowStateProperty.Changed.AddClassHandler<Window>(
                 (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; });
+            
+            MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
+            MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
+            MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
+            MaxHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
+
         }
 
         /// <summary>
@@ -155,6 +161,7 @@ namespace Avalonia.Controls
             impl.Closing = HandleClosing;
             impl.WindowStateChanged = HandleWindowStateChanged;
             _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
+            this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
         }
 
         /// <summary>
@@ -239,6 +246,44 @@ namespace Avalonia.Controls
             set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the window position in screen coordinates.
+        /// </summary>
+        public PixelPoint Position
+        {
+            get { return PlatformImpl?.Position ?? PixelPoint.Origin; }
+            set
+            {
+                PlatformImpl?.Move(value);
+            }
+        }
+        
+        /// <summary>
+        /// Starts moving a window with left button being held. Should be called from left mouse button press event handler
+        /// </summary>
+        public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag();
+
+        /// <summary>
+        /// Starts resizing a window. This function is used if an application has window resizing controls. 
+        /// Should be called from left mouse button press event handler
+        /// </summary>
+        public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge);
+        
+        /// <summary>
+        /// Carries out the arrange pass of the window.
+        /// </summary>
+        /// <param name="finalSize">The final window size.</param>
+        /// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            using (BeginAutoSizing())
+            {
+                PlatformImpl?.Resize(finalSize);
+            }
+
+            return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
+        }
+        
         /// <inheritdoc/>
         Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
 

+ 0 - 44
src/Avalonia.Controls/WindowBase.cs

@@ -49,10 +49,6 @@ namespace Avalonia.Controls
             IsVisibleProperty.OverrideDefaultValue<WindowBase>(false);
             IsVisibleProperty.Changed.AddClassHandler<WindowBase>(x => x.IsVisibleChanged);
 
-            MinWidthProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
-            MinHeightProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
-            MaxWidthProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
-            MaxHeightProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
             
             TopmostProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue));
         }
@@ -67,7 +63,6 @@ namespace Avalonia.Controls
             impl.Activated = HandleActivated;
             impl.Deactivated = HandleDeactivated;
             impl.PositionChanged = HandlePositionChanged;
-            this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
         }
 
         /// <summary>
@@ -96,19 +91,6 @@ namespace Avalonia.Controls
             get { return _isActive; }
             private set { SetAndRaise(IsActiveProperty, ref _isActive, value); }
         }
-
-        /// <summary>
-        /// Gets or sets the window position in screen coordinates.
-        /// </summary>
-        public PixelPoint Position
-        {
-            get { return PlatformImpl?.Position ?? PixelPoint.Origin; }
-            set
-            {
-                if (PlatformImpl is IWindowBaseImpl impl)
-                    impl.Position = value;
-            }
-        }
         
         public Screens Screens { get; private set; }
 
@@ -208,21 +190,6 @@ namespace Avalonia.Controls
             return Disposable.Create(() => AutoSizing = false);
         }
 
-        /// <summary>
-        /// Carries out the arrange pass of the window.
-        /// </summary>
-        /// <param name="finalSize">The final window size.</param>
-        /// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
-        protected override Size ArrangeOverride(Size finalSize)
-        {
-            using (BeginAutoSizing())
-            {
-                PlatformImpl?.Resize(finalSize);
-            }
-
-            return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
-        }
-
         /// <summary>
         /// Ensures that the window is initialized.
         /// </summary>
@@ -318,16 +285,5 @@ namespace Avalonia.Controls
                 }
             }
         }
-
-        /// <summary>
-        /// Starts moving a window with left button being held. Should be called from left mouse button press event handler
-        /// </summary>
-        public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag();
-
-        /// <summary>
-        /// Starts resizing a window. This function is used if an application has window resizing controls. 
-        /// Should be called from left mouse button press event handler
-        /// </summary>
-        public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge);
     }
 }

+ 5 - 0
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@@ -72,6 +72,11 @@ namespace Avalonia.DesignerSupport.Remote
             RenderIfNeeded();
         }
 
+        public void Move(PixelPoint point)
+        {
+            
+        }
+
         public void SetMinMaxSize(Size minSize, Size maxSize)
         {
         }

+ 0 - 2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@@ -40,8 +40,6 @@ namespace Avalonia.DesignerSupport.Remote
             return s_lastWindow;
         }
 
-        public IPopupImpl CreatePopup() => new WindowStub();
-
         public static void Initialize(IAvaloniaRemoteTransportConnection transport)
         {
             s_transport = transport;

+ 22 - 1
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -5,6 +5,7 @@ using System.Reactive.Disposables;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
+using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Input.Raw;
@@ -13,7 +14,7 @@ using Avalonia.Rendering;
 
 namespace Avalonia.DesignerSupport.Remote
 {
-    class WindowStub : IPopupImpl, IWindowImpl
+    class WindowStub : IWindowImpl, IPopupImpl
     {
         public Action Deactivated { get; set; }
         public Action Activated { get; set; }
@@ -29,10 +30,23 @@ namespace Avalonia.DesignerSupport.Remote
         public Func<bool> Closing { get; set; }
         public Action Closed { get; set; }
         public IMouseDevice MouseDevice { get; } = new MouseDevice();
+        public IPopupImpl CreatePopup() => new WindowStub(this);
+
         public PixelPoint Position { get; set; }
         public Action<PixelPoint> PositionChanged { get; set; }
         public WindowState WindowState { get; set; }
         public Action<WindowState> WindowStateChanged { get; set; }
+
+        public WindowStub(IWindowImpl parent = null)
+        {
+            if (parent != null)
+                PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent,
+                    (_, size, __) =>
+                    {
+                        Resize(size);
+                    }));
+        }
+        
         public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root);
         public void Dispose()
         {
@@ -77,6 +91,11 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
+        public void Move(PixelPoint point)
+        {
+            
+        }
+
         public IScreenImpl Screen { get; } = new ScreenStub();
 
         public void SetMinMaxSize(Size minSize, Size maxSize)
@@ -110,6 +129,8 @@ namespace Avalonia.DesignerSupport.Remote
         public void SetTopmost(bool value)
         {
         }
+
+        public IPopupPositioner PopupPositioner { get; }
     }
 
     class ClipboardStub : IClipboard

+ 0 - 5
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -97,11 +97,6 @@ namespace Avalonia.Native
         {
             throw new NotImplementedException();
         }
-
-        public IPopupImpl CreatePopup()
-        {
-            return new PopupImpl(_factory, _options);
-        }
     }
 
     public class AvaloniaNativeMacOptions

+ 1 - 0
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@@ -24,6 +24,7 @@ namespace Avalonia
     {
         public bool UseDeferredRendering { get; set; } = true;
         public bool UseGpu { get; set; } = true;
+        public bool OverlayPopups { get; set; }
         public string AvaloniaNativeLibraryPath { get; set; }
     }
 

+ 19 - 1
src/Avalonia.Native/PopupImpl.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Native.Interop;
 using Avalonia.Platform;
 
@@ -9,12 +10,26 @@ namespace Avalonia.Native
 {
     public class PopupImpl : WindowBaseImpl, IPopupImpl
     {
-        public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts)
+        private readonly IAvaloniaNativeFactory _factory;
+        private readonly AvaloniaNativePlatformOptions _opts;
+        public PopupImpl(IAvaloniaNativeFactory factory,
+            AvaloniaNativePlatformOptions opts,
+            IWindowBaseImpl parent) : base(opts)
         {
+            _factory = factory;
+            _opts = opts;
             using (var e = new PopupEvents(this))
             {
                 Init(factory.CreatePopup(e), factory.CreateScreens());
             }
+            PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize));
+        }
+
+        private void MoveResize(PixelPoint position, Size size, double scaling)
+        {
+            Position = position;
+            Resize(size);
+            //TODO: We ignore the scaling override for now
         }
 
         class PopupEvents : WindowBaseEvents, IAvnWindowEvents
@@ -35,5 +50,8 @@ namespace Avalonia.Native
             {
             }
         }
+
+        public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this);
+        public IPopupPositioner PopupPositioner { get; }
     }
 }

+ 8 - 0
src/Avalonia.Native/WindowImpl.cs

@@ -11,9 +11,13 @@ namespace Avalonia.Native
 {
     public class WindowImpl : WindowBaseImpl, IWindowImpl
     {
+        private readonly IAvaloniaNativeFactory _factory;
+        private readonly AvaloniaNativePlatformOptions _opts;
         IAvnWindow _native;
         public WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts)
         {
+            _factory = factory;
+            _opts = opts;
             using (var e = new WindowEvents(this))
             {
                 Init(_native = factory.CreateWindow(e), factory.CreateScreens());
@@ -100,5 +104,9 @@ namespace Avalonia.Native
         }
 
         public Func<bool> Closing { get; set; }
+        public void Move(PixelPoint point) => Position = point;
+
+        public override IPopupImpl CreatePopup() =>
+            _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, this);
     }
 }

+ 2 - 1
src/Avalonia.Native/WindowImplBase.cs

@@ -15,7 +15,7 @@ using Avalonia.Threading;
 
 namespace Avalonia.Native
 {
-    public class WindowBaseImpl : IWindowBaseImpl,
+    public abstract class WindowBaseImpl : IWindowBaseImpl,
         IFramebufferPlatformSurface
     {
         IInputRoot _inputRoot;
@@ -91,6 +91,7 @@ namespace Avalonia.Native
         public Action<Size> Resized { get; set; }
         public Action Closed { get; set; }
         public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice;
+        public abstract IPopupImpl CreatePopup();
 
 
         class FramebufferWrapper : ILockedFramebuffer

+ 4 - 4
src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj

@@ -12,11 +12,11 @@
     <ProjectReference Include="..\Avalonia.Layout\Avalonia.Layout.csproj" />
     <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
     <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
-    <AvaloniaResource Include="DefaultTheme.xaml"/>
-    <AvaloniaResource Include="Accents/*.xaml"/>
+    <AvaloniaResource Include="DefaultTheme.xaml" />
+    <AvaloniaResource Include="Accents/*.xaml" />
     <!-- Compatibility with old apps, probably need to replace with AvaloniaResource -->
-    <EmbeddedResource Include="**/*.xaml"/>
+    <EmbeddedResource Include="**/*.xaml" />
   </ItemGroup>
-  <Import Project="..\..\build\BuildTargets.targets"/>
+  <Import Project="..\..\build\BuildTargets.targets" />
   <Import Project="..\..\build\Rx.props" />
 </Project>

+ 8 - 10
src/Avalonia.Themes.Default/ComboBox.xaml

@@ -40,16 +40,14 @@
                    StaysOpen="False">
               <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="1">
-                  <AdornerDecorator Margin="-1 -1 0 0">
-                      <ScrollViewer>
-                          <ItemsPresenter Name="PART_ItemsPresenter"
-                                          Items="{TemplateBinding Items}"
-                                          ItemsPanel="{TemplateBinding ItemsPanel}"
-                                          ItemTemplate="{TemplateBinding ItemTemplate}"
-                                          VirtualizationMode="{TemplateBinding VirtualizationMode}"
-                                  />
-                      </ScrollViewer>
-                  </AdornerDecorator>
+                  <ScrollViewer>
+                      <ItemsPresenter Name="PART_ItemsPresenter"
+                                      Items="{TemplateBinding Items}"
+                                      ItemsPanel="{TemplateBinding ItemsPanel}"
+                                      ItemTemplate="{TemplateBinding ItemTemplate}"
+                                      VirtualizationMode="{TemplateBinding VirtualizationMode}"
+                              />
+                  </ScrollViewer>
               </Border>
             </Popup>
           </Grid>

+ 1 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -19,6 +19,7 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.Menu.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.ContextMenu.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.MenuItem.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.OverlayPopupHost.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.PopupRoot.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.ProgressBar.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.RadioButton.xaml?assembly=Avalonia.Themes.Default"/>

+ 3 - 3
src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml

@@ -4,13 +4,13 @@
   <Setter Property="Template">
     <ControlTemplate>
       <Border Background="{TemplateBinding Background}">
-        <AdornerDecorator>
+        <VisualLayerManager>
           <ContentPresenter Name="PART_ContentPresenter" 
                             ContentTemplate="{TemplateBinding ContentTemplate}"
                             Content="{TemplateBinding Content}" 
                             Margin="{TemplateBinding Padding}"/>
-        </AdornerDecorator>
+        </VisualLayerManager>
       </Border>
     </ControlTemplate>
   </Setter>
-</Style>
+</Style>

+ 14 - 0
src/Avalonia.Themes.Default/OverlayPopupHost.xaml

@@ -0,0 +1,14 @@
+<Style xmlns="https://github.com/avaloniaui" Selector="OverlayPopupHost">
+  <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
+  <Setter Property="Template">
+    <ControlTemplate>
+      <VisualLayerManager IsPopup="True" Margin="-1 -1 0 0">
+        <ContentPresenter Name="PART_ContentPresenter"
+                          Background="{TemplateBinding Background}"
+                          ContentTemplate="{TemplateBinding ContentTemplate}"
+                          Content="{TemplateBinding Content}" 
+                          Padding="{TemplateBinding Padding}"/>
+      </VisualLayerManager>
+    </ControlTemplate>
+  </Setter>
+</Style>

+ 8 - 6
src/Avalonia.Themes.Default/PopupRoot.xaml

@@ -2,11 +2,13 @@
   <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
   <Setter Property="Template">
     <ControlTemplate>
-      <ContentPresenter Name="PART_ContentPresenter"
-                        Background="{TemplateBinding Background}"
-                        ContentTemplate="{TemplateBinding ContentTemplate}"
-                        Content="{TemplateBinding Content}" 
-                        Padding="{TemplateBinding Padding}"/>
+      <VisualLayerManager IsPopup="True" Margin="-1 -1 0 0">
+        <ContentPresenter Name="PART_ContentPresenter"
+                          Background="{TemplateBinding Background}"
+                          ContentTemplate="{TemplateBinding ContentTemplate}"
+                          Content="{TemplateBinding Content}" 
+                          Padding="{TemplateBinding Padding}"/>
+      </VisualLayerManager>
     </ControlTemplate>
   </Setter>
-</Style>
+</Style>

+ 2 - 2
src/Avalonia.Themes.Default/Window.xaml

@@ -5,14 +5,14 @@
   <Setter Property="Template">
     <ControlTemplate>
       <Border Background="{TemplateBinding Background}">
-        <AdornerDecorator>
+        <VisualLayerManager>
           <ContentPresenter Name="PART_ContentPresenter"
                             ContentTemplate="{TemplateBinding ContentTemplate}"
                             Content="{TemplateBinding Content}"
                             Margin="{TemplateBinding Padding}"
                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                             VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>           
-        </AdornerDecorator>
+        </VisualLayerManager>
       </Border>
     </ControlTemplate>
   </Setter>

+ 54 - 1
src/Avalonia.Visuals/Media/PixelPoint.cs

@@ -59,6 +59,59 @@ namespace Avalonia
         {
             return !(left == right);
         }
+        
+        /// <summary>
+        /// Converts the <see cref="Point"/> to a <see cref="Vector"/>.
+        /// </summary>
+        /// <param name="p">The point.</param>
+        public static implicit operator PixelVector(PixelPoint p)
+        {
+            return new PixelVector(p.X, p.Y);
+        }
+        
+        /// <summary>
+        /// Adds two points.
+        /// </summary>
+        /// <param name="a">The first point.</param>
+        /// <param name="b">The second point.</param>
+        /// <returns>A point that is the result of the addition.</returns>
+        public static PixelPoint operator +(PixelPoint a, PixelPoint b)
+        {
+            return new PixelPoint(a.X + b.X, a.Y + b.Y);
+        }
+
+        /// <summary>
+        /// Adds a vector to a point.
+        /// </summary>
+        /// <param name="a">The point.</param>
+        /// <param name="b">The vector.</param>
+        /// <returns>A point that is the result of the addition.</returns>
+        public static PixelPoint operator +(PixelPoint a, PixelVector b)
+        {
+            return new PixelPoint(a.X + b.X, a.Y + b.Y);
+        }
+
+        /// <summary>
+        /// Subtracts two points.
+        /// </summary>
+        /// <param name="a">The first point.</param>
+        /// <param name="b">The second point.</param>
+        /// <returns>A point that is the result of the subtraction.</returns>
+        public static PixelPoint operator -(PixelPoint a, PixelPoint b)
+        {
+            return new PixelPoint(a.X - b.X, a.Y - b.Y);
+        }
+
+        /// <summary>
+        /// Subtracts a vector from a point.
+        /// </summary>
+        /// <param name="a">The point.</param>
+        /// <param name="b">The vector.</param>
+        /// <returns>A point that is the result of the subtraction.</returns>
+        public static PixelPoint operator -(PixelPoint a, PixelVector b)
+        {
+            return new PixelPoint(a.X - b.X, a.Y - b.Y);
+        }
 
         /// <summary>
         /// Parses a <see cref="PixelPoint"/> string.
@@ -106,7 +159,7 @@ namespace Avalonia
                 return hash;
             }
         }
-
+        
         /// <summary>
         /// Returns a new <see cref="PixelPoint"/> with the same Y co-ordinate and the specified X co-ordinate.
         /// </summary>

+ 10 - 0
src/Avalonia.Visuals/Media/PixelRect.cs

@@ -261,6 +261,16 @@ namespace Avalonia
         {
             return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom);
         }
+        
+        /// <summary>
+        /// Translates the rectangle by an offset.
+        /// </summary>
+        /// <param name="offset">The offset.</param>
+        /// <returns>The translated rectangle.</returns>
+        public PixelRect Translate(PixelVector offset)
+        {
+            return new PixelRect(Position + offset, Size);
+        }
 
         /// <summary>
         /// Gets the union of two rectangles.

+ 203 - 0
src/Avalonia.Visuals/Media/PixelVector.cs

@@ -0,0 +1,203 @@
+// 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;
+using System.Globalization;
+using Avalonia.Animation.Animators;
+using JetBrains.Annotations;
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Defines a vector.
+    /// </summary>
+    public readonly struct PixelVector
+    {
+        /// <summary>
+        /// The X vector.
+        /// </summary>
+        private readonly int _x;
+
+        /// <summary>
+        /// The Y vector.
+        /// </summary>
+        private readonly int _y;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PixelVector"/> structure.
+        /// </summary>
+        /// <param name="x">The X vector.</param>
+        /// <param name="y">The Y vector.</param>
+        public PixelVector(int x, int y)
+        {
+            _x = x;
+            _y = y;
+        }
+
+        /// <summary>
+        /// Gets the X vector.
+        /// </summary>
+        public int X => _x;
+
+        /// <summary>
+        /// Gets the Y vector.
+        /// </summary>
+        public int Y => _y;
+
+        /// <summary>
+        /// Converts the <see cref="PixelVector"/> to a <see cref="PixelPoint"/>.
+        /// </summary>
+        /// <param name="a">The vector.</param>
+        public static explicit operator PixelPoint(PixelVector a)
+        {
+            return new PixelPoint(a._x, a._y);
+        }
+
+        /// <summary>
+        /// Calculates the dot product of two vectors
+        /// </summary>
+        /// <param name="a">First vector</param>
+        /// <param name="b">Second vector</param>
+        /// <returns>The dot product</returns>
+        public static int operator *(PixelVector a, PixelVector b)
+        {
+            return a.X * b.X + a.Y * b.Y;
+        }
+
+        /// <summary>
+        /// Scales a vector.
+        /// </summary>
+        /// <param name="vector">The vector</param>
+        /// <param name="scale">The scaling factor.</param>
+        /// <returns>The scaled vector.</returns>
+        public static PixelVector operator *(PixelVector vector, int scale)
+        {
+            return new PixelVector(vector._x * scale, vector._y * scale);
+        }
+
+        /// <summary>
+        /// Scales a vector.
+        /// </summary>
+        /// <param name="vector">The vector</param>
+        /// <param name="scale">The divisor.</param>
+        /// <returns>The scaled vector.</returns>
+        public static PixelVector operator /(PixelVector vector, int scale)
+        {
+            return new PixelVector(vector._x / scale, vector._y / scale);
+        }
+
+        /// <summary>
+        /// Length of the vector
+        /// </summary>
+        public double Length => Math.Sqrt(X * X + Y * Y);
+
+        /// <summary>
+        /// Negates a vector.
+        /// </summary>
+        /// <param name="a">The vector.</param>
+        /// <returns>The negated vector.</returns>
+        public static PixelVector operator -(PixelVector a)
+        {
+            return new PixelVector(-a._x, -a._y);
+        }
+
+        /// <summary>
+        /// Adds two vectors.
+        /// </summary>
+        /// <param name="a">The first vector.</param>
+        /// <param name="b">The second vector.</param>
+        /// <returns>A vector that is the result of the addition.</returns>
+        public static PixelVector operator +(PixelVector a, PixelVector b)
+        {
+            return new PixelVector(a._x + b._x, a._y + b._y);
+        }
+
+        /// <summary>
+        /// Subtracts two vectors.
+        /// </summary>
+        /// <param name="a">The first vector.</param>
+        /// <param name="b">The second vector.</param>
+        /// <returns>A vector that is the result of the subtraction.</returns>
+        public static PixelVector operator -(PixelVector a, PixelVector b)
+        {
+            return new PixelVector(a._x - b._x, a._y - b._y);
+        }
+
+        /// <summary>
+        /// Check if two vectors are equal (bitwise).
+        /// </summary>
+        /// <param name="other"></param>
+        /// <returns></returns>
+        public bool Equals(PixelVector other)
+        {
+            return _x == other._x && _y == other._y;
+        }
+
+        /// <summary>
+        /// Check if two vectors are nearly equal (numerically).
+        /// </summary>
+        /// <param name="other">The other vector.</param>
+        /// <returns>True if vectors are nearly equal.</returns>
+        [Pure]
+        public bool NearlyEquals(PixelVector other)
+        {
+            const float tolerance = float.Epsilon;
+
+            return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance;
+        }
+
+        public override bool Equals(object obj)
+        {
+            if (ReferenceEquals(null, obj)) return false;
+
+            return obj is PixelVector vector && Equals(vector);
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                return (_x.GetHashCode() * 397) ^ _y.GetHashCode();
+            }
+        }
+
+        public static bool operator ==(PixelVector left, PixelVector right)
+        {
+            return left.Equals(right);
+        }
+
+        public static bool operator !=(PixelVector left, PixelVector right)
+        {
+            return !left.Equals(right);
+        }
+
+        /// <summary>
+        /// Returns the string representation of the point.
+        /// </summary>
+        /// <returns>The string representation of the point.</returns>
+        public override string ToString()
+        {
+            return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y);
+        }
+
+        /// <summary>
+        /// Returns a new vector with the specified X coordinate.
+        /// </summary>
+        /// <param name="x">The X coordinate.</param>
+        /// <returns>The new vector.</returns>
+        public PixelVector WithX(int x)
+        {
+            return new PixelVector(x, _y);
+        }
+
+        /// <summary>
+        /// Returns a new vector with the specified Y coordinate.
+        /// </summary>
+        /// <param name="y">The Y coordinate.</param>
+        /// <returns>The new vector.</returns>
+        public PixelVector WithY(int y)
+        {
+            return new PixelVector(_x, y);
+        }
+    }
+}

+ 2 - 6
src/Avalonia.X11/X11Platform.cs

@@ -74,18 +74,13 @@ namespace Avalonia.X11
         public IntPtr Display { get; set; }
         public IWindowImpl CreateWindow()
         {
-            return new X11Window(this, false);
+            return new X11Window(this, null);
         }
 
         public IEmbeddableWindowImpl CreateEmbeddableWindow()
         {
             throw new NotSupportedException();
         }
-
-        public IPopupImpl CreatePopup()
-        {
-            return new X11Window(this, true);
-        }
     }
 }
 
@@ -96,6 +91,7 @@ namespace Avalonia
     {
         public bool UseEGL { get; set; }
         public bool UseGpu { get; set; } = true;
+        public bool OverlayPopups { get; set; }
 
         public List<string> GlxRendererBlacklist { get; set; } = new List<string>
         {

+ 35 - 12
src/Avalonia.X11/X11Window.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Reactive.Disposables;
 using System.Text;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.OpenGL;
@@ -21,6 +22,7 @@ namespace Avalonia.X11
     unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client
     {
         private readonly AvaloniaX11Platform _platform;
+        private readonly IWindowImpl _popupParent;
         private readonly bool _popup;
         private readonly X11Info _x11;
         private bool _invalidated;
@@ -38,6 +40,7 @@ namespace Avalonia.X11
         private bool _mapped;
         private HashSet<X11Window> _transientChildren = new HashSet<X11Window>();
         private X11Window _transientParent;
+        private double? _scalingOverride;
         public object SyncRoot { get; } = new object();
 
         class InputEventContainer
@@ -47,10 +50,10 @@ namespace Avalonia.X11
         private readonly Queue<InputEventContainer> _inputQueue = new Queue<InputEventContainer>();
         private InputEventContainer _lastEvent;
         private bool _useRenderWindow = false;
-        public X11Window(AvaloniaX11Platform platform, bool popup)
+        public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent)
         {
             _platform = platform;
-            _popup = popup;
+            _popup = popupParent != null;
             _x11 = platform.Info;
             _mouse = platform.MouseDevice;
             _keyboard = platform.KeyboardDevice;
@@ -66,7 +69,7 @@ namespace Avalonia.X11
                          | SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore
                          | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity;
 
-            if (popup)
+            if (_popup)
             {
                 attr.override_redirect = true;
                 valueMask |= SetWindowValuemask.OverrideRedirect;
@@ -150,6 +153,8 @@ namespace Avalonia.X11
             _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
                 XNames.XNClientWindow, _handle, IntPtr.Zero);
             XFlush(_x11.Display);
+            if(_popup)
+                PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
         }
 
         class SurfaceInfo  : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
@@ -453,22 +458,28 @@ namespace Avalonia.X11
             }
         }
 
-        private bool UpdateScaling()
+        private bool UpdateScaling(bool skipResize = false)
         {
             lock (SyncRoot)
             {
-                var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
-                    .FirstOrDefault(m => m.Bounds.Contains(Position));
-                var newScaling = monitor?.PixelDensity ?? Scaling;
+                double newScaling;
+                if (_scalingOverride.HasValue)
+                    newScaling = _scalingOverride.Value;
+                else
+                {
+                    var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
+                        .FirstOrDefault(m => m.Bounds.Contains(Position));
+                    newScaling = monitor?.PixelDensity ?? Scaling;
+                }
+
                 if (Scaling != newScaling)
                 {
-                    Console.WriteLine(
-                        $"Updating scaling from {Scaling} to {newScaling} as a response to position change to {Position}");
                     var oldScaledSize = ClientSize;
                     Scaling = newScaling;
                     ScalingChanged?.Invoke(Scaling);
                     SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
-                    Resize(oldScaledSize, true);
+                    if(!skipResize)
+                        Resize(oldScaledSize, true);
                     return true;
                 }
 
@@ -730,6 +741,14 @@ namespace Avalonia.X11
 
 
         public void Resize(Size clientSize) => Resize(clientSize, false);
+        public void Move(PixelPoint point) => Position = point;
+        private void MoveResize(PixelPoint position, Size size, double scaling)
+        {
+            Move(position);
+            _scalingOverride = scaling;
+            UpdateScaling(true);
+            Resize(size, true);
+        }
 
         PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling));
         
@@ -793,7 +812,9 @@ namespace Avalonia.X11
         }
 
         public IMouseDevice MouseDevice => _mouse;
-       
+        public IPopupImpl CreatePopup() 
+            => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this);
+
         public void Activate()
         {
             if (_x11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero)
@@ -937,6 +958,8 @@ namespace Avalonia.X11
         {
             SendNetWMMessage(_x11.Atoms._NET_WM_STATE,
                 (IntPtr)(value ? 0 : 1), _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR, IntPtr.Zero);
-        }        
+        }
+
+        public IPopupPositioner PopupPositioner { get; }
     }
 }

+ 3 - 1
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@@ -59,7 +59,9 @@ namespace Avalonia.LinuxFramebuffer
 
         public Size ClientSize => ScaledSize;
         public IMouseDevice MouseDevice => new MouseDevice();
-        public double Scaling => 1;
+        public IPopupImpl CreatePopup() => null;
+
+        public double Scaling => _outputBackend.Scaling;
         public IEnumerable<object> Surfaces => new object[] {_outputBackend};
         public Action<RawInputEventArgs> Input { get; set; }
         public Action<Rect> Paint { get; set; }

+ 5 - 4
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -107,11 +107,12 @@ namespace Avalonia.LinuxFramebuffer
 
 public static class LinuxFramebufferPlatformExtensions
 {
-    public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev = null)
-        where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev));
+    public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev = null, double scaling = 1)
+        where T : AppBuilderBase<T>, new() =>
+        StartLinuxDirect(builder, args, new FbdevOutput(fbdev) {Scaling = scaling});
 
-    public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null)
-        where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card));
+    public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null, double scaling = 1)
+        where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling});
     
     public static int StartLinuxDirect<T>(this T builder, string[] args, IOutputBackend backend)
         where T : AppBuilderBase<T>, new()

+ 4 - 2
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs

@@ -14,7 +14,7 @@ namespace Avalonia.LinuxFramebuffer.Output
         private DrmCard _card;
         private readonly EglGlPlatformSurface _eglPlatformSurface;
         public PixelSize PixelSize => _mode.Resolution;
-
+        public double Scaling { get; set; }
         public DrmOutput(string path = null)
         {
             var card = new DrmCard(path);
@@ -233,7 +233,7 @@ namespace Avalonia.LinuxFramebuffer.Output
 
                 public PixelSize Size => _parent._mode.Resolution;
 
-                public double Scaling => 1;
+                public double Scaling => _parent.Scaling;
             }
 
             public IGlPlatformSurfaceRenderingSession BeginDraw()
@@ -241,6 +241,8 @@ namespace Avalonia.LinuxFramebuffer.Output
                 _parent._deferredContext.MakeCurrent(_parent._eglSurface);
                 return new RenderSession(_parent);
             }
+            
+            
         }
 
         IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext;

+ 3 - 4
src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs

@@ -9,16 +9,15 @@ namespace Avalonia.LinuxFramebuffer
 {
     public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend
     {
-        private readonly Vector _dpi;
         private int _fd;
         private fb_fix_screeninfo _fixedInfo;
         private fb_var_screeninfo _varInfo;
         private IntPtr _mappedLength;
         private IntPtr _mappedAddress;
+        public double Scaling { get; set; }
 
-        public FbdevOutput(string fileName = null, Vector? dpi = null)
+        public FbdevOutput(string fileName = null)
         {
-            _dpi = dpi ?? new Vector(96, 96);
             fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
             _fd = NativeUnsafeMethods.open(fileName, 2, 0);
             if (_fd <= 0)
@@ -101,7 +100,7 @@ namespace Avalonia.LinuxFramebuffer
         {
             if (_fd <= 0)
                 throw new ObjectDisposedException("LinuxFramebuffer");
-            return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, _dpi);
+            return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, new Vector(96, 96) * Scaling);
         }
 
 

+ 1 - 0
src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs

@@ -3,5 +3,6 @@ namespace Avalonia.LinuxFramebuffer.Output
     public interface IOutputBackend
     {
         PixelSize PixelSize { get; }
+        double Scaling { get; set; }
     }
 }

+ 51 - 38
src/Skia/Avalonia.Skia/GlRenderTarget.cs

@@ -26,51 +26,64 @@ namespace Avalonia.Skia
         public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
         {
             var session = _surface.BeginDraw();
-            var disp = session.Display;
-            var gl = disp.GlInterface;
-            gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb);
-
-            var size = session.Size;
-            var scaling = session.Scaling;
-            if (size.Width <= 0 || size.Height <= 0 || scaling < 0)
-            {
-                throw new InvalidOperationException(
-                    $"Can't create drawing context for surface with {size} size and {scaling} scaling");
-            }
-
-            gl.Viewport(0, 0, size.Width, size.Height);
-            gl.ClearStencil(0);
-            gl.ClearColor(0, 0, 0, 0);
-            gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-            lock (_grContext)
+            bool success = false;
+            try
             {
-                _grContext.ResetContext();
-
-                GRBackendRenderTarget renderTarget =
-                    new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize,
-                        new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat()));
-                var surface = SKSurface.Create(_grContext, renderTarget,
-                    GRSurfaceOrigin.BottomLeft,
-                    GRPixelConfig.Rgba8888.ToColorType());
+                var disp = session.Display;
+                var gl = disp.GlInterface;
+                gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb);
 
-                var nfo = new DrawingContextImpl.CreateInfo
+                var size = session.Size;
+                var scaling = session.Scaling;
+                if (size.Width <= 0 || size.Height <= 0 || scaling < 0)
                 {
-                    GrContext = _grContext,
-                    Canvas = surface.Canvas,
-                    Dpi = SkiaPlatform.DefaultDpi * scaling,
-                    VisualBrushRenderer = visualBrushRenderer,
-                    DisableTextLcdRendering = true
-                };
+                    session.Dispose();
+                    throw new InvalidOperationException(
+                        $"Can't create drawing context for surface with {size} size and {scaling} scaling");
+                }
 
-                return new DrawingContextImpl(nfo, Disposable.Create(() =>
+                gl.Viewport(0, 0, size.Width, size.Height);
+                gl.ClearStencil(0);
+                gl.ClearColor(0, 0, 0, 0);
+                gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+                lock (_grContext)
                 {
+                    _grContext.ResetContext();
+
+                    GRBackendRenderTarget renderTarget =
+                        new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize,
+                            new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat()));
+                    var surface = SKSurface.Create(_grContext, renderTarget,
+                        GRSurfaceOrigin.BottomLeft,
+                        GRPixelConfig.Rgba8888.ToColorType());
+
+                    var nfo = new DrawingContextImpl.CreateInfo
+                    {
+                        GrContext = _grContext,
+                        Canvas = surface.Canvas,
+                        Dpi = SkiaPlatform.DefaultDpi * scaling,
+                        VisualBrushRenderer = visualBrushRenderer,
+                        DisableTextLcdRendering = true
+                    };
+
                     
-                    surface.Canvas.Flush();
-                    surface.Dispose();
-                    renderTarget.Dispose();
-                    _grContext.Flush();
+                    var ctx = new DrawingContextImpl(nfo, Disposable.Create(() =>
+                    {
+
+                        surface.Canvas.Flush();
+                        surface.Dispose();
+                        renderTarget.Dispose();
+                        _grContext.Flush();
+                        session.Dispose();
+                    }));
+                    success = true;
+                    return ctx;
+                }
+            }
+            finally
+            {
+                if(!success)
                     session.Dispose();
-                }));
             }
         }
     }

+ 2 - 0
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

@@ -240,5 +240,7 @@ namespace Avalonia.Win32.Interop.Wpf
                 return new Vector(1, 1);
             return new Vector(src.TransformToDevice.M11, src.TransformToDevice.M22);
         }
+
+        public IPopupImpl CreatePopup() => null;
     }
 }

+ 15 - 0
src/Windows/Avalonia.Win32/PopupImpl.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Platform;
 using Avalonia.Win32.Interop;
 
@@ -57,5 +58,19 @@ namespace Avalonia.Win32
                     return base.WndProc(hWnd, msg, wParam, lParam);
             }
         }
+
+        public PopupImpl(IWindowBaseImpl parent)
+        {
+            PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize));
+        }
+
+        private void MoveResize(PixelPoint position, Size size, double scaling)
+        {
+            Move(position);
+            Resize(size);
+            //TODO: We ignore the scaling override for now
+        }
+
+        public IPopupPositioner PopupPositioner { get; }
     }
 }

+ 2 - 5
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -41,6 +41,7 @@ namespace Avalonia
         public bool UseDeferredRendering { get; set; } = true;
         public bool AllowEglInitialization { get; set; }
         public bool? EnableMultitouch { get; set; }
+        public bool OverlayPopups { get; set; }
     }
 }
 
@@ -61,6 +62,7 @@ namespace Avalonia.Win32
         }
 
         public static bool UseDeferredRendering => Options.UseDeferredRendering;
+        internal static bool UseOverlayPopups => Options.OverlayPopups;
         public static Win32PlatformOptions Options { get; private set; }
 
         public Size DoubleClickSize => new Size(
@@ -210,11 +212,6 @@ namespace Avalonia.Win32
             return embedded;
         }
 
-        public IPopupImpl CreatePopup()
-        {
-            return new PopupImpl();
-        }
-
         public IWindowIconImpl LoadIcon(string fileName)
         {
             using (var stream = File.OpenRead(fileName))

+ 3 - 4
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -131,6 +131,8 @@ namespace Avalonia.Win32
             }
         }
 
+        public void Move(PixelPoint point) => Position = point;
+
         public void SetMinMaxSize(Size minSize, Size maxSize)
         {
             _minSize = minSize;
@@ -248,10 +250,7 @@ namespace Avalonia.Win32
             UnmanagedMethods.SetActiveWindow(_hwnd);
         }
 
-        public IPopupImpl CreatePopup()
-        {
-            return new PopupImpl();
-        }
+        public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this);
 
         public void Dispose()
         {

+ 2 - 0
src/iOS/Avalonia.iOS/TopLevelImpl.cs

@@ -134,5 +134,7 @@ namespace Avalonia.iOS
         }
         
         public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this);
+
+        public IPopupImpl CreatePopup() => null;
     }
 }

+ 4 - 1
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@@ -982,6 +982,8 @@ namespace Avalonia.Controls.UnitTests
                 AutoCompleteBox control = CreateControl();
                 control.Items = CreateSimpleStringArray();
                 TextBox textBox = GetTextBox(control);
+                var window = new Window {Content = control};
+                window.ApplyTemplate();
                 Dispatcher.UIThread.RunJobs();
                 test.Invoke(control, textBox);
             }
@@ -1027,7 +1029,8 @@ namespace Avalonia.Controls.UnitTests
                 var popup =
                     new Popup
                     {
-                        Name = "PART_Popup"
+                        Name = "PART_Popup",
+                        PlacementTarget = control
                     }.RegisterInNameScope(scope);
 
                 var panel = new Panel();

+ 16 - 12
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests
                     ContextMenu = sut
                 };
 
-                new Window { Content = target };
+                new Window { Content = target }.ApplyTemplate();
 
                 int openedCount = 0;
 
@@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests
                     openedCount++;
                 };
 
-                sut.Open(null);
+                sut.Open(target);
 
                 Assert.Equal(1, openedCount);
             }
@@ -53,9 +53,9 @@ namespace Avalonia.Controls.UnitTests
                     ContextMenu = sut
                 };
 
-                new Window { Content = target };
+                new Window { Content = target }.ApplyTemplate();
 
-                sut.Open(null);
+                sut.Open(target);
 
                 int closedCount = 0;
 
@@ -84,7 +84,8 @@ namespace Avalonia.Controls.UnitTests
                     ContextMenu = sut
                 };
 
-                new Window { Content = target };
+                var window = new Window {Content = target};
+                window.ApplyTemplate();
 
                 _mouse.Click(target, MouseButton.Right);
 
@@ -112,7 +113,8 @@ namespace Avalonia.Controls.UnitTests
                     ContextMenu = sut
                 };
 
-                var window = new Window { Content = target };
+                var window = new Window {Content = target};
+                window.ApplyTemplate();
                 
                 _mouse.Click(target, MouseButton.Right);
 
@@ -151,7 +153,7 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        [Fact]
+        [Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")]
         public void Cancelling_Closing_Leaves_ContextMenuOpen()
         {
             using (Application())
@@ -165,7 +167,9 @@ namespace Avalonia.Controls.UnitTests
                 {
                     ContextMenu = sut
                 };
-                new Window { Content = target };
+                
+                var window = new Window {Content = target};
+                window.ApplyTemplate();
 
                 sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; };
 
@@ -190,12 +194,12 @@ namespace Avalonia.Controls.UnitTests
             screenImpl.Setup(x => x.ScreenCount).Returns(1);
             screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(screen, screen, true) });
 
-            var windowImpl = new Mock<IWindowImpl>();
-            windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
-
-            popupImpl = new Mock<IPopupImpl>();
+            popupImpl = MockWindowingPlatform.CreatePopupMock();
             popupImpl.SetupGet(x => x.Scaling).Returns(1);
 
+            var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object);
+            windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
+
             var services = TestServices.StyledWindow.With(
                                         inputManager: new InputManager(),
                                         windowImpl: windowImpl.Object,

+ 31 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs

@@ -281,6 +281,37 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Content = 42;
         }
 
+        [Fact]
+        public void Should_Set_InheritanceParent_Even_When_LogicalParent_Is_Already_Set()
+        {
+            var logicalParent = new Canvas();
+            var child = new TextBlock();
+            var (target, host) = CreateTarget();
+
+            ((ISetLogicalParent)child).SetParent(logicalParent);
+            target.Content = child;
+
+            Assert.Same(logicalParent, child.Parent);
+
+            // InheritanceParent is exposed via StylingParent.
+            Assert.Same(target, ((IStyledElement)child).StylingParent);
+        }
+
+        [Fact]
+        public void Should_Reset_InheritanceParent_When_Child_Removed()
+        {
+            var logicalParent = new Canvas();
+            var child = new TextBlock();
+            var (target, _) = CreateTarget();
+
+            ((ISetLogicalParent)child).SetParent(logicalParent);
+            target.Content = child;
+            target.Content = null;
+
+            // InheritanceParent is exposed via StylingParent.
+            Assert.Same(logicalParent, ((IStyledElement)child).StylingParent);
+        }
+
         (ContentPresenter presenter, ContentControl templatedParent) CreateTarget()
         {
             var templatedParent = new ContentControl

+ 55 - 11
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var target = CreateTarget();
+                var target = CreateTarget(new Window());
 
                 Assert.True(((ILogical)target).IsAttachedToLogicalTree);
             }
@@ -32,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var target = CreateTarget();
+                var target = CreateTarget(new Window());
 
                 Assert.True(target.Presenter.IsAttachedToLogicalTree);
             }
@@ -43,28 +43,70 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
+                var window = new Window();
                 var target = new TemplatedControlWithPopup
                 {
                     PopupContent = new Canvas(),
                 };
+                window.Content = target;
 
-                var root = new TestRoot { Child = target };
-
+                window.ApplyTemplate();
                 target.ApplyTemplate();
                 target.Popup.Open();
 
-                Assert.Equal(target.Popup, ((IStyleHost)target.Popup.PopupRoot).StylingParent);
+                Assert.Equal(target.Popup, ((IStyleHost)target.Popup.Host).StylingParent);
             }
         }
 
+        [Fact]
+        public void PopupRoot_Should_Have_Template_Applied()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var target = new Popup {PlacementMode = PlacementMode.Pointer};
+                var child = new Control();
+
+                window.Content = target;
+                window.ApplyTemplate();
+                target.Open();
+
+               
+                Assert.Single(((Visual)target.Host).GetVisualChildren());
+
+                var templatedChild = ((Visual)target.Host).GetVisualChildren().Single();
+                
+                Assert.IsType<VisualLayerManager>(templatedChild);
+                var contentPresenter = templatedChild.VisualChildren.Single();
+                Assert.IsType<ContentPresenter>(contentPresenter);
+                
+                
+                Assert.Equal((PopupRoot)target.Host, ((IControl)templatedChild).TemplatedParent);
+                Assert.Equal((PopupRoot)target.Host, ((IControl)contentPresenter).TemplatedParent);
+            }
+        }
+        
+        [Fact]
+        public void PopupRoot_Should_Have_Null_VisualParent()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var target = new Popup() {PlacementTarget = new Window()};
+
+                target.Open();
+
+                Assert.Null(((Visual)target.Host).GetVisualParent());
+            }
+        }
+        
         [Fact]
         public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree()
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
                 var child = new Decorator();
-                var target = CreateTarget();
                 var window = new Window();
+                var target = CreateTarget(window);
                 var detachedCount = 0;
                 var attachedCount = 0;
 
@@ -88,8 +130,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
                 var child = new Decorator();
-                var target = CreateTarget();
                 var window = new Window();
+                var target = CreateTarget(window);
                 var detachedCount = 0;
                 var attachedCount = 0;
 
@@ -117,22 +159,23 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
+                var window = new Window();
                 var target = new TemplatedControlWithPopup
                 {
                     PopupContent = new Canvas(),
                 };
+                window.Content = target;
 
-                var root = new TestRoot { Child = target };
-
+                window.ApplyTemplate();
                 target.ApplyTemplate();
                 target.Popup.Open();
                 target.PopupContent = null;
             }
         }
 
-        private PopupRoot CreateTarget()
+        private PopupRoot CreateTarget(TopLevel popupParent)
         {
-            var result = new PopupRoot
+            var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup())
             {
                 Template = new FuncControlTemplate<PopupRoot>((parent, scope) =>
                     new ContentPresenter
@@ -158,6 +201,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                     new Popup
                     {
                         [!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty],
+                        PlacementTarget = parent
                     });
             }
 

+ 66 - 89
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
 {
     public class PopupTests
     {
+        protected bool UsePopupHost;
+        
         [Fact]
         public void Setting_Child_Should_Set_Child_Controls_LogicalParent()
         {
@@ -137,20 +139,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             {
                 var target = new Popup();
 
-                Assert.Null(target.PopupRoot);
-            }
-        }
-
-        [Fact]
-        public void PopupRoot_Should_Have_Null_VisualParent()
-        {
-            using (CreateServices())
-            {
-                var target = new Popup();
-
-                target.Open();
-
-                Assert.Null(target.PopupRoot.GetVisualParent());
+                Assert.Null(((Visual)target.Host));
             }
         }
 
@@ -159,12 +148,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (CreateServices())
             {
-                var target = new Popup();
+                var target = new Popup() {PlacementTarget = PreparedWindow()};
 
                 target.Open();
 
-                Assert.Equal(target, target.PopupRoot.Parent);
-                Assert.Equal(target, target.PopupRoot.GetLogicalParent());
+                Assert.Equal(target, ((Visual)target.Host).Parent);
+                Assert.Equal(target, ((Visual)target.Host).GetLogicalParent());
             }
         }
 
@@ -173,15 +162,15 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (CreateServices())
             {
-                var target = new Popup();
-                var root = new TestRoot { Child = target };
+                var target = new Popup() {PlacementMode = PlacementMode.Pointer};
+                var root = PreparedWindow(target);
 
                 target.Open();
 
-                var popupRoot = (ILogical)target.PopupRoot;
+                var popupRoot = (ILogical)((Visual)target.Host);
 
                 Assert.True(popupRoot.IsAttachedToLogicalTree);
-                root.Child = null;
+                root.Content = null;
                 Assert.False(((ILogical)target).IsAttachedToLogicalTree);
             }
         }
@@ -191,8 +180,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (CreateServices())
             {
-                var window = new Window();
-                var target = new Popup();
+                var window = PreparedWindow();
+                var target = new Popup() {PlacementMode = PlacementMode.Pointer};
 
                 window.Content = target;
 
@@ -214,10 +203,11 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (CreateServices())
             {
-                var window = new Window();
-                var target = new Popup();
+                var window = PreparedWindow();
+                var target = new Popup() {PlacementMode = PlacementMode.Pointer};
 
                 window.Content = target;
+                window.ApplyTemplate();
                 target.Open();
 
                 int closedCount = 0;
@@ -233,46 +223,28 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
 
-        [Fact]
-        public void PopupRoot_Should_Have_Template_Applied()
-        {
-            using (CreateServices())
-            {
-                var window = new Window();
-                var target = new Popup();
-                var child = new Control();
-
-                window.Content = target;
-                target.Open();
-
-                Assert.Single(target.PopupRoot.GetVisualChildren());
-
-                var templatedChild = target.PopupRoot.GetVisualChildren().Single();
-                Assert.IsType<ContentPresenter>(templatedChild);
-                Assert.Equal(target.PopupRoot, ((IControl)templatedChild).TemplatedParent);
-            }
-        }
-
+        
         [Fact]
         public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent()
         {
             using (CreateServices())
             {
                 PopupContentControl target;
-                var root = new TestRoot
+                var root = PreparedWindow(target = new PopupContentControl
                 {
-                    Child = target = new PopupContentControl
-                    {
-                        Content = new Border(),
-                        Template = new FuncControlTemplate<PopupContentControl>(PopupContentControlTemplate),
-                    },
-                    StylingParent = AvaloniaLocator.Current.GetService<IGlobalStyles>()
-                };
+                    Content = new Border(),
+                    Template = new FuncControlTemplate<PopupContentControl>(PopupContentControlTemplate),
+                });
+                root.Show();
 
                 target.ApplyTemplate();
+
                 var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup");
                 popup.Open();
-                var popupRoot = popup.PopupRoot;
+
+                var popupRoot = (Control)popup.Host;
+                popupRoot.Measure(Size.Infinity);
+                popupRoot.Arrange(new Rect(popupRoot.DesiredSize));
 
                 var children = popupRoot.GetVisualDescendants().ToList();
                 var types = children.Select(x => x.GetType().Name).ToList();
@@ -280,6 +252,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Assert.Equal(
                     new[]
                     {
+                        "VisualLayerManager",
                         "ContentPresenter",
                         "ContentPresenter",
                         "Border",
@@ -293,6 +266,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Assert.Equal(
                     new object[]
                     {
+                        popupRoot,
                         popupRoot,
                         target,
                         null,
@@ -301,6 +275,13 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
 
+        Window PreparedWindow(object content = null)
+        {
+            var w = new Window {Content = content};
+            w.ApplyTemplate();
+            return w;
+        }
+
         [Fact]
         public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit()
         {
@@ -311,6 +292,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 {
                     Child = child = new TestControl(),
                     DataContext = "foo",
+                    PlacementTarget = PreparedWindow()
                 };
 
                 var beginCalled = false;
@@ -330,46 +312,32 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Assert.False(beginCalled);
             }
         }
-
-
-        private static IDisposable CreateServices()
+        
+        [Fact]
+        public void Popup_Host_Type_Should_Match_Platform_Preference()
         {
-            var result = AvaloniaLocator.EnterScope();
-
-            var styles = new Styles
+            using (CreateServices())
             {
-                new Style(x => x.OfType<PopupRoot>())
-                {
-                    Setters = new[]
-                    {
-                        new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate<PopupRoot>(PopupRootTemplate)),
-                    }
-                },
-            };
-
-            var globalStyles = new Mock<IGlobalStyles>();
-            globalStyles.Setup(x => x.IsStylesInitialized).Returns(true);
-            globalStyles.Setup(x => x.Styles).Returns(styles);
-
-            var renderInterface = new Mock<IPlatformRenderInterface>();
-
-            AvaloniaLocator.CurrentMutable
-                .Bind<IGlobalStyles>().ToFunc(() => globalStyles.Object)
-                .Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformMock())
-                .Bind<IStyler>().ToTransient<Styler>()
-                .Bind<IPlatformRenderInterface>().ToFunc(() => renderInterface.Object)
-                .Bind<IInputManager>().ToConstant(new InputManager());
-            
-            return result;
+                var target = new Popup() {PlacementTarget = PreparedWindow()};
+
+                target.Open();
+                if (UsePopupHost)
+                    Assert.IsType<OverlayPopupHost>(target.Host);
+                else
+                    Assert.IsType<PopupRoot>(target.Host);
+            }
         }
 
-        private static IControl PopupRootTemplate(PopupRoot control, INameScope scope)
+        private IDisposable CreateServices()
         {
-            return new ContentPresenter
-            {
-                Name = "PART_ContentPresenter",
-                [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty],
-            }.RegisterInNameScope(scope);
+            return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
+                new MockWindowingPlatform(null,
+                    () =>
+                    {
+                        if(UsePopupHost)
+                            return null;
+                        return MockWindowingPlatform.CreatePopupMock().Object;
+                    })));
         }
 
         private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope)
@@ -377,6 +345,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             return new Popup
             {
                 Name = "popup",
+                PlacementTarget = control,
                 Child = new ContentPresenter
                 {
                     [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty],
@@ -401,4 +370,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
     }
+
+    public class PopupTestsWithPopupRoot : PopupTests
+    {
+        public PopupTestsWithPopupRoot()
+        {
+            UsePopupHost = true;
+        }
+    }
 }

+ 0 - 27
tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs

@@ -20,33 +20,6 @@ namespace Avalonia.Controls.UnitTests
 {
     public class WindowBaseTests
     {
-        [Fact]
-        public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
-            {
-                var impl = Mock.Of<IWindowBaseImpl>(x => x.Scaling == 1);
-
-                Mock.Get(impl).Setup(x => x.Resize(It.IsAny<Size>())).Callback(() => { });
-
-                var target = new TestWindowBase(impl)
-                {
-                    Template = CreateTemplate(),
-                    Content = new TextBlock
-                    {
-                        Width = 321,
-                        Height = 432,
-                    },
-                    IsVisible = true,
-                };
-
-                target.LayoutManager.ExecuteInitialLayoutPass(target);
-
-                Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432)));
-            }
-        }
-
-
         [Fact]
         public void Activate_Should_Call_Impl_Activate()
         {

+ 3 - 6
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -277,8 +277,7 @@ namespace Avalonia.Controls.UnitTests
             var screens = new Mock<IScreenImpl>();
             screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object });
 
-            var windowImpl = new Mock<IWindowImpl>();
-            windowImpl.SetupProperty(x => x.Position);
+            var windowImpl = MockWindowingPlatform.CreateWindowMock();
             windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
             windowImpl.Setup(x => x.Scaling).Returns(1);
             windowImpl.Setup(x => x.Screen).Returns(screens.Object);
@@ -302,14 +301,12 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner()
         {
-            var parentWindowImpl = new Mock<IWindowImpl>();
-            parentWindowImpl.SetupProperty(x => x.Position);
+            var parentWindowImpl = MockWindowingPlatform.CreateWindowMock();
             parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
             parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
             parentWindowImpl.Setup(x => x.Scaling).Returns(1);
 
-            var windowImpl = new Mock<IWindowImpl>();
-            windowImpl.SetupProperty(x => x.Position);
+            var windowImpl = MockWindowingPlatform.CreateWindowMock();
             windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200));
             windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
             windowImpl.Setup(x => x.Scaling).Returns(1);

+ 9 - 4
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Text;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Styling;
 using Avalonia.UnitTests;
@@ -59,11 +60,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
                     new Setter(
                         Window.TemplateProperty,
                         new FuncControlTemplate<Window>((x, scope) =>
-                            new ContentPresenter
+                            new VisualLayerManager
                             {
-                                Name = "PART_ContentPresenter",
-                                [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
-                            }.RegisterInNameScope(scope)))
+                                Child =
+                                    new ContentPresenter
+                                    {
+                                        Name = "PART_ContentPresenter",
+                                        [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
+                                    }.RegisterInNameScope(scope)
+                            }))
                 }
             };
         }

+ 1 - 0
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@@ -5,6 +5,7 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <OutputType>Library</OutputType>
     <IsPackable>false</IsPackable>
+    <LangVersion>latest</LangVersion>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

+ 38 - 4
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@@ -1,4 +1,6 @@
 using System;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Input;
 using Moq;
 using Avalonia.Platform;
 
@@ -15,16 +17,48 @@ namespace Avalonia.UnitTests
             _popupImpl = popupImpl;
         }
 
+        public static Mock<IWindowImpl> CreateWindowMock(Func<IPopupImpl> popupImpl = null)
+        {
+            var win = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
+            var mock = Mock.Get(win);
+            mock.Setup(x => x.CreatePopup()).Returns(() =>
+            {
+                if (popupImpl != null)
+                    return popupImpl();
+                return CreatePopupMock().Object;
+
+            });
+            PixelPoint pos = default;
+            mock.SetupGet(x => x.Position).Returns(() => pos);
+            mock.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));
+            SetupToplevel(mock);
+            return mock;
+        }
+
+        static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
+        {
+            mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
+        }
+
+        public static Mock<IPopupImpl> CreatePopupMock()
+        {
+            var positioner = Mock.Of<IPopupPositioner>();
+            var popup = Mock.Of<IPopupImpl>(x => x.Scaling == 1);
+            var mock = Mock.Get(popup);
+            mock.SetupGet(x => x.PopupPositioner).Returns(positioner);
+            SetupToplevel(mock);
+            
+            return mock;
+        }
+
         public IWindowImpl CreateWindow()
         {
-            return _windowImpl?.Invoke() ?? Mock.Of<IWindowImpl>(x => x.Scaling == 1);
+            return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object;
         }
 
         public IEmbeddableWindowImpl CreateEmbeddableWindow()
         {
             throw new NotImplementedException();
         }
-
-        public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of<IPopupImpl>(x => x.Scaling == 1);
     }
-}
+}