Kaynağa Gözat

Merge remote-tracking branch 'origin/master' into managed-file-dialog

Dan Walmsley 6 yıl önce
ebeveyn
işleme
b78ebb9557
100 değiştirilmiş dosya ile 2617 ekleme ve 784 silme
  1. 1 0
      azure-pipelines.yml
  2. 16 4
      native/Avalonia.Native/src/OSX/AvnString.mm
  3. 10 1
      native/Avalonia.Native/src/OSX/clipboard.mm
  4. 11 2
      samples/ControlCatalog.NetCore/Program.cs
  5. 1 1
      samples/ControlCatalog/DecoratedWindow.xaml.cs
  6. 1 1
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  7. 3 2
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  8. 11 1
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  9. 14 1
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  10. 46 6
      samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
  11. 23 10
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  12. 84 9
      samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
  13. 27 0
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  14. 0 5
      src/Android/Avalonia.Android/AndroidPlatform.cs
  15. 0 112
      src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs
  16. 2 0
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  17. 14 3
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  18. 1 1
      src/Avalonia.Controls/ComboBox.cs
  19. 2 0
      src/Avalonia.Controls/ContextMenu.cs
  20. 1 0
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  21. 1 1
      src/Avalonia.Controls/Image.cs
  22. 1 1
      src/Avalonia.Controls/MenuItem.cs
  23. 1 1
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  24. 17 2
      src/Avalonia.Controls/PlacementMode.cs
  25. 1 1
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  26. 3 1
      src/Avalonia.Controls/Platform/IPopupImpl.cs
  27. 2 0
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  28. 1 23
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  29. 27 0
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  30. 0 1
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  31. 2 1
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  32. 0 5
      src/Avalonia.Controls/Platform/PlatformManager.cs
  33. 1 0
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  34. 8 11
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  35. 0 42
      src/Avalonia.Controls/Primitives/AdornerDecorator.cs
  36. 1 1
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  37. 26 0
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  38. 38 0
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  39. 149 0
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  40. 144 140
      src/Avalonia.Controls/Primitives/Popup.cs
  41. 358 0
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  42. 175 0
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  43. 50 0
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  44. 55 64
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  45. 93 0
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  46. 2 2
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  47. 5 3
      src/Avalonia.Controls/Repeater/ItemsSourceView.cs
  48. 81 64
      src/Avalonia.Controls/StackPanel.cs
  49. 14 15
      src/Avalonia.Controls/TextBlock.cs
  50. 8 6
      src/Avalonia.Controls/ToolTip.cs
  51. 45 0
      src/Avalonia.Controls/Window.cs
  52. 0 44
      src/Avalonia.Controls/WindowBase.cs
  53. 5 0
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  54. 0 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  55. 22 1
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  56. 35 27
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  57. 4 1
      src/Avalonia.Input/Cursors.cs
  58. 2 2
      src/Avalonia.Input/DragDrop.cs
  59. 1 1
      src/Avalonia.Input/Platform/IPlatformDragSource.cs
  60. 3 0
      src/Avalonia.Layout/Layoutable.cs
  61. 0 6
      src/Avalonia.Layout/UniformGridLayoutState.cs
  62. 0 5
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  63. 1 0
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  64. 19 1
      src/Avalonia.Native/PopupImpl.cs
  65. 8 0
      src/Avalonia.Native/WindowImpl.cs
  66. 2 1
      src/Avalonia.Native/WindowImplBase.cs
  67. 8 3
      src/Avalonia.ReactiveUI/AutoSuspendHelper.cs
  68. 4 4
      src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
  69. 1 0
      src/Avalonia.Themes.Default/ButtonSpinner.xaml
  70. 9 10
      src/Avalonia.Themes.Default/ComboBox.xaml
  71. 1 0
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  72. 3 3
      src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml
  73. 14 0
      src/Avalonia.Themes.Default/OverlayPopupHost.xaml
  74. 8 6
      src/Avalonia.Themes.Default/PopupRoot.xaml
  75. 2 2
      src/Avalonia.Themes.Default/Window.xaml
  76. 21 14
      src/Avalonia.Visuals/Media/BrushExtensions.cs
  77. 85 43
      src/Avalonia.Visuals/Media/DashStyle.cs
  78. 4 4
      src/Avalonia.Visuals/Media/DrawingContext.cs
  79. 1 1
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  80. 20 0
      src/Avalonia.Visuals/Media/IDashStyle.cs
  81. 39 0
      src/Avalonia.Visuals/Media/IPen.cs
  82. 93 0
      src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs
  83. 118 0
      src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
  84. 168 15
      src/Avalonia.Visuals/Media/Pen.cs
  85. 54 1
      src/Avalonia.Visuals/Media/PixelPoint.cs
  86. 10 0
      src/Avalonia.Visuals/Media/PixelRect.cs
  87. 203 0
      src/Avalonia.Visuals/Media/PixelVector.cs
  88. 3 3
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  89. 2 2
      src/Avalonia.Visuals/Platform/IGeometryImpl.cs
  90. 1 1
      src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
  91. 3 3
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  92. 1 1
      src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
  93. 5 4
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  94. 5 4
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  95. 5 4
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  96. 6 1
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  97. 1 1
      src/Avalonia.X11/X11CursorFactory.cs
  98. 2 6
      src/Avalonia.X11/X11Platform.cs
  99. 35 12
      src/Avalonia.X11/X11Window.cs
  100. 3 1
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

+ 1 - 0
azure-pipelines.yml

@@ -134,3 +134,4 @@ jobs:
       pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip'
       artifactName: 'Samples'
     condition: succeeded()
+

+ 16 - 4
native/Avalonia.Native/src/OSX/AvnString.mm

@@ -11,14 +11,26 @@
 class AvnStringImpl : public virtual ComSingleObject<IAvnString, &IID_IAvnString>
 {
 private:
-    NSString* _string;
+    int _length;
+    const char* _cstring;
     
 public:
     FORWARD_IUNKNOWN()
     
     AvnStringImpl(NSString* string)
+    { 
+        auto cstring = [string cStringUsingEncoding:NSUTF8StringEncoding];
+        _length = (int)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+        
+        _cstring = (const char*)malloc(_length + 5);
+        
+        memset((void*)_cstring, 0, _length + 5);
+        memcpy((void*)_cstring, (void*)cstring, _length);
+    }
+    
+    virtual ~AvnStringImpl()
     {
-        _string = string;
+        free((void*)_cstring);
     }
     
     virtual HRESULT Pointer(void**retOut) override
@@ -30,7 +42,7 @@ public:
                 return E_POINTER;
             }
             
-            *retOut = (void*)_string.UTF8String;
+            *retOut = (void*)_cstring;
             
             return S_OK;
         }
@@ -43,7 +55,7 @@ public:
             return E_POINTER;
         }
         
-        *retOut = (int)_string.length;
+        *retOut = _length;
         
         return S_OK;
     }

+ 10 - 1
native/Avalonia.Native/src/OSX/clipboard.mm

@@ -8,6 +8,13 @@ class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard>
 {
 public:
     FORWARD_IUNKNOWN()
+    
+    Clipboard()
+    {
+        NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
+        [pasteBoard stringForType:NSPasteboardTypeString];
+    }
+    
     virtual HRESULT GetText (IAvnString**ppv) override
     {
         @autoreleasepool
@@ -39,7 +46,9 @@ public:
     {
         @autoreleasepool
         {
-            [[NSPasteboard generalPasteboard] clearContents];
+            NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
+            [pasteBoard clearContents];
+            [pasteBoard setString:@"" forType:NSPasteboardTypeString];
         }
         
         return S_OK;

+ 11 - 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;
@@ -32,15 +33,23 @@ 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);

+ 1 - 1
samples/ControlCatalog/DecoratedWindow.xaml.cs

@@ -34,7 +34,7 @@ namespace ControlCatalog
             SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West);
             SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East);
             SetupSide("Top", StandardCursorType.TopSide, WindowEdge.North);
-            SetupSide("Bottom", StandardCursorType.BottomSize, WindowEdge.South);
+            SetupSide("Bottom", StandardCursorType.BottomSide, WindowEdge.South);
             SetupSide("TopLeft", StandardCursorType.TopLeftCorner, WindowEdge.NorthWest);
             SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast);
             SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest);

+ 1 - 1
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@@ -29,7 +29,7 @@ namespace ControlCatalog.Pages
             DataObject dragData = new DataObject();
             dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
 
-            var result = await DragDrop.DoDragDrop(dragData, DragDropEffects.Copy);
+            var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
             switch(result)
             {
                 case DragDropEffects.Copy:

+ 3 - 2
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@@ -6,19 +6,20 @@
       <TextBlock Classes="h1">ItemsRepeater</TextBlock>
       <TextBlock Classes="h2">A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization.</TextBlock>
     </StackPanel>
-    <StackPanel DockPanel.Dock="Right" Margin="8 0">
+    <StackPanel DockPanel.Dock="Right" Margin="8 0" Spacing="4">
       <ComboBox SelectedIndex="0" SelectionChanged="LayoutChanged">
         <ComboBoxItem>Stack - Vertical</ComboBoxItem>
         <ComboBoxItem>Stack - Horizontal</ComboBoxItem>
         <ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
         <ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
       </ComboBox>
+      <Button Command="{Binding AddItem}">Add Item</Button>
     </StackPanel>
     <Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
       <ScrollViewer Name="scroller"
                     HorizontalScrollBarVisibility="Auto"
                     VerticalScrollBarVisibility="Auto">
-        <ItemsRepeater Name="repeater" Items="{Binding}"/>
+        <ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}"/>
       </ScrollViewer>
     </Border>
   </DockPanel>

+ 11 - 1
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@@ -1,8 +1,11 @@
+using System;
 using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
+using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
 
 namespace ControlCatalog.Pages
 {
@@ -16,7 +19,8 @@ namespace ControlCatalog.Pages
             this.InitializeComponent();
             _repeater = this.FindControl<ItemsRepeater>("repeater");
             _scroller = this.FindControl<ScrollViewer>("scroller");
-            DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray();
+            _repeater.PointerPressed += RepeaterClick;
+            DataContext = new ItemsRepeaterPageViewModel();
         }
 
         private void InitializeComponent()
@@ -67,5 +71,11 @@ namespace ControlCatalog.Pages
                     break;
             }
         }
+
+        private void RepeaterClick(object sender, PointerPressedEventArgs e)
+        {
+            var item = (e.Source as TextBlock)?.DataContext as string;
+            ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
+        }
     }
 }

+ 14 - 1
samples/ControlCatalog/Pages/ListBoxPage.xaml

@@ -9,7 +9,20 @@
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
               Spacing="16">
-      <ListBox Items="{Binding}" Width="250" Height="350"></ListBox>
+      <StackPanel Orientation="Vertical" Spacing="8">
+        <ListBox Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
+
+        <Button Command="{Binding AddItemCommand}">Add</Button>
+
+        <Button Command="{Binding RemoveItemCommand}">Remove</Button>
+
+        <ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
+          <ComboBoxItem>Single</ComboBoxItem>
+          <ComboBoxItem>Multiple</ComboBoxItem>
+          <ComboBoxItem>Toggle</ComboBoxItem>
+          <ComboBoxItem>AlwaysSelected</ComboBoxItem>
+        </ComboBox>
+      </StackPanel>
     </StackPanel>
   </StackPanel>
 </UserControl>

+ 46 - 6
samples/ControlCatalog/Pages/ListBoxPage.xaml.cs

@@ -1,9 +1,9 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
+using System.Reactive;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using ReactiveUI;
 
 namespace ControlCatalog.Pages
 {
@@ -11,9 +11,8 @@ namespace ControlCatalog.Pages
     {
         public ListBoxPage()
         {
-            this.InitializeComponent();
-            DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" )
-                .ToArray();
+            InitializeComponent();
+            DataContext = new PageViewModel();
         }
 
         private void InitializeComponent()
@@ -21,5 +20,46 @@ namespace ControlCatalog.Pages
             AvaloniaXamlLoader.Load(this);
         }
 
+        private class PageViewModel : ReactiveObject
+        {
+            private int _counter;
+            private SelectionMode _selectionMode;
+
+            public PageViewModel()
+            {
+                Items = new ObservableCollection<string>(Enumerable.Range(1, 10).Select(i => GenerateItem()));
+                SelectedItems = new ObservableCollection<string>();
+
+                AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
+
+                RemoveItemCommand = ReactiveCommand.Create(() =>
+                {
+                    while (SelectedItems.Count > 0)
+                    {
+                        Items.Remove(SelectedItems[0]);
+                    }
+                });
+            }
+
+            public ObservableCollection<string> Items { get; }
+
+            public ObservableCollection<string> SelectedItems { get; }
+
+            public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
+
+            public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
+
+            public SelectionMode SelectionMode
+            {
+                get => _selectionMode;
+                set
+                {
+                    SelectedItems.Clear();
+                    this.RaiseAndSetIfChanged(ref _selectionMode, value);
+                }
+            }
+
+            private string GenerateItem() => $"Item {_counter++}";
+        }
     }
 }

+ 23 - 10
samples/ControlCatalog/Pages/TreeViewPage.xaml

@@ -6,16 +6,29 @@
     <TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock>
 
     <StackPanel Orientation="Horizontal"
-              Margin="0,16,0,0"
-              HorizontalAlignment="Center"
-              Spacing="16">
-      <TreeView SelectionMode="Multiple" Items="{Binding}" Width="250" Height="350">
-        <TreeView.ItemTemplate>
-          <TreeDataTemplate ItemsSource="{Binding Children}">
-            <TextBlock Text="{Binding Header}"/>
-          </TreeDataTemplate>
-        </TreeView.ItemTemplate>
-      </TreeView>
+                Margin="0,16,0,0"
+                HorizontalAlignment="Center"
+                Spacing="16">
+      <StackPanel Orientation="Vertical" Spacing="8">
+        <TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
+          <TreeView.ItemTemplate>
+            <TreeDataTemplate ItemsSource="{Binding Children}">
+              <TextBlock Text="{Binding Header}"/>
+            </TreeDataTemplate>
+          </TreeView.ItemTemplate>
+        </TreeView>
+
+        <Button Command="{Binding AddItemCommand}">Add</Button>
+
+        <Button Command="{Binding RemoveItemCommand}">Remove</Button>
+
+        <ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
+          <ComboBoxItem>Single</ComboBoxItem>
+          <ComboBoxItem>Multiple</ComboBoxItem>
+          <ComboBoxItem>Toggle</ComboBoxItem>
+          <ComboBoxItem>AlwaysSelected</ComboBoxItem>
+        </ComboBox>
+      </StackPanel>
     </StackPanel>
   </StackPanel>
 </UserControl>

+ 84 - 9
samples/ControlCatalog/Pages/TreeViewPage.xaml.cs

@@ -1,8 +1,9 @@
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
+using System.Reactive;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using ReactiveUI;
 
 namespace ControlCatalog.Pages
 {
@@ -10,8 +11,8 @@ namespace ControlCatalog.Pages
     {
         public TreeViewPage()
         {
-            this.InitializeComponent();
-            DataContext = new Node().Children;
+            InitializeComponent();
+            DataContext = new PageViewModel();
         }
 
         private void InitializeComponent()
@@ -19,22 +20,96 @@ namespace ControlCatalog.Pages
             AvaloniaXamlLoader.Load(this);
         }
 
-        public class Node
+        private class PageViewModel : ReactiveObject
         {
-            private IList<Node> _children;
+            private SelectionMode _selectionMode;
+
+            public PageViewModel()
+            {
+                Node root = new Node();
+                Items = root.Children;
+                SelectedItems = new ObservableCollection<Node>();
+
+                AddItemCommand = ReactiveCommand.Create(() =>
+                {
+                    Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root;
+                    parentItem.AddNewItem();
+                });
+
+                RemoveItemCommand = ReactiveCommand.Create(() =>
+                {
+                    while (SelectedItems.Count > 0)
+                    {
+                        Node lastItem = SelectedItems[0];
+                        RecursiveRemove(Items, lastItem);
+                        SelectedItems.Remove(lastItem);
+                    }
+
+                    bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
+                    {
+                        if (items.Remove(selectedItem))
+                        {
+                            return true;
+                        }
+
+                        foreach (Node item in items)
+                        {
+                            if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem))
+                            {
+                                return true;
+                            }
+                        }
+
+                        return false;
+                    }
+                });
+            }
+
+            public ObservableCollection<Node> Items { get; }
+
+            public ObservableCollection<Node> SelectedItems { get; }
+
+            public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
+
+            public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
+
+            public SelectionMode SelectionMode
+            {
+                get => _selectionMode;
+                set
+                {
+                    SelectedItems.Clear();
+                    this.RaiseAndSetIfChanged(ref _selectionMode, value);
+                }
+            }
+        }
+
+        private class Node
+        {
+            private int _counter;
+            private ObservableCollection<Node> _children;
+
             public string Header { get; private set; }
-            public IList<Node> Children
+
+            public bool AreChildrenInitialized => _children != null;
+
+            public ObservableCollection<Node> Children
             {
                 get
                 {
                     if (_children == null)
                     {
-                        _children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"})
-                            .ToArray();
+                        _children = new ObservableCollection<Node>(Enumerable.Range(1, 10).Select(i => CreateNewNode()));
                     }
                     return _children;
                 }
             }
+
+            public void AddNewItem() => Children.Add(CreateNewNode());
+
+            public override string ToString() => Header;
+
+            private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"};
         }
     }
 }

+ 27 - 0
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@@ -0,0 +1,27 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using ReactiveUI;
+
+namespace ControlCatalog.ViewModels
+{
+    public class ItemsRepeaterPageViewModel : ReactiveObject
+    {
+        private int newItemIndex = 1;
+
+        public ItemsRepeaterPageViewModel()
+        {
+            Items = new ObservableCollection<string>(
+                Enumerable.Range(1, 100000).Select(i => $"Item {i}"));
+        }
+
+        public ObservableCollection<string> Items { get; }
+
+        public string SelectedItem { get; set; }
+
+        public void AddItem()
+        {
+            var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
+            Items.Insert(index + 1, $"New Item {newItemIndex++}");
+        }
+    }
+}

+ 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);
     }
 }

+ 14 - 3
src/Avalonia.Controls/Calendar/CalendarItem.cs

@@ -4,6 +4,7 @@
 // All other rights reserved.
 
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
 using Avalonia.Data;
@@ -193,6 +194,9 @@ namespace Avalonia.Controls.Primitives
         {
             if (MonthView != null)
             {
+                var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth;
+                var children = new List<IControl>(childCount);
+
                 for (int i = 0; i < Calendar.RowsPerMonth; i++)
                 {
                     if (_dayTitleTemplate != null)
@@ -201,7 +205,7 @@ namespace Avalonia.Controls.Primitives
                         cell.DataContext = string.Empty;
                         cell.SetValue(Grid.RowProperty, 0);
                         cell.SetValue(Grid.ColumnProperty, i);
-                        MonthView.Children.Add(cell);
+                        children.Add(cell);
                     }
                 }
 
@@ -222,13 +226,18 @@ namespace Avalonia.Controls.Primitives
                         cell.PointerEnter += Cell_MouseEnter;
                         cell.PointerLeave += Cell_MouseLeave;
                         cell.Click += Cell_Click;
-                        MonthView.Children.Add(cell);
+                        children.Add(cell);
                     }
                 }
+                
+                MonthView.Children.AddRange(children);
             }
 
             if (YearView != null)
             {
+                var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear;
+                var children = new List<IControl>(childCount);
+
                 CalendarButton month;
                 for (int i = 0; i < Calendar.RowsPerYear; i++)
                 {
@@ -246,9 +255,11 @@ namespace Avalonia.Controls.Primitives
                         month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp;
                         month.PointerEnter += Month_MouseEnter;
                         month.PointerLeave += Month_MouseLeave;
-                        YearView.Children.Add(month);
+                        children.Add(month);
                     }
                 }
+
+                YearView.Children.AddRange(children);
             }
         }
 

+ 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/Image.cs

@@ -96,7 +96,7 @@ namespace Avalonia.Controls
                 }
             }
 
-            return result.Constrain(availableSize);
+            return result;
         }
 
         /// <inheritdoc/>

+ 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();
     }
 }

+ 2 - 1
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@@ -33,9 +33,10 @@ namespace Avalonia.Platform
             _dragDrop = AvaloniaLocator.Current.GetService<IDragDropDevice>();
         }
 
-        public async Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+        public async Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
         {
             Dispatcher.UIThread.VerifyAccess();
+            triggerEvent.Pointer.Capture(null);
             if (_draggedData == null)
             {
                 _draggedData = data;

+ 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)

+ 8 - 11
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -49,6 +49,14 @@ namespace Avalonia.Controls.Presenters
             AffectsRender<TextPresenter>(PasswordCharProperty,
                 SelectionBrushProperty, SelectionForegroundBrushProperty,
                 SelectionStartProperty, SelectionEndProperty);
+
+            Observable.Merge(
+                SelectionStartProperty.Changed,
+                SelectionEndProperty.Changed,
+                PasswordCharProperty.Changed
+            ).AddClassHandler<TextPresenter>((x,_) => x.InvalidateFormattedText());
+
+            CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
         }
 
         public TextPresenter()
@@ -56,17 +64,6 @@ namespace Avalonia.Controls.Presenters
             _caretTimer = new DispatcherTimer();
             _caretTimer.Interval = TimeSpan.FromMilliseconds(500);
             _caretTimer.Tick += CaretTimerTick;
-
-            Observable.Merge(
-                this.GetObservable(SelectionStartProperty),
-                this.GetObservable(SelectionEndProperty))
-                .Subscribe(_ => InvalidateFormattedText());
-
-            this.GetObservable(CaretIndexProperty)
-                .Subscribe(CaretIndexChanged);
-
-            this.GetObservable(PasswordCharProperty)
-                .Subscribe(_ => InvalidateFormattedText());
         }
 
         public int CaretIndex

+ 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);
+        }
+    }
+}

+ 2 - 2
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -707,9 +707,9 @@ namespace Avalonia.Controls
             }
         }
 
-        private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure();
+        private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure();
 
-        private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange();
+        private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange();
 
         private VirtualizingLayoutContext GetLayoutContext()
         {

+ 5 - 3
src/Avalonia.Controls/Repeater/ItemsSourceView.cs

@@ -35,9 +35,11 @@ namespace Avalonia.Controls
         {
             Contract.Requires<ArgumentNullException>(source != null);
 
-            _inner = source as IList;
-
-            if (_inner == null && source is IEnumerable<object> objectEnumerable)
+            if (source is IList list)
+            {
+                _inner = list;
+            }
+            else if (source is IEnumerable<object> objectEnumerable)
             {
                 _inner = new List<object>(objectEnumerable);
             }

+ 81 - 64
src/Avalonia.Controls/StackPanel.cs

@@ -1,8 +1,9 @@
-// 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.
+// This source file is adapted from the Windows Presentation Foundation project. 
+// (https://github.com/dotnet/wpf/) 
+// 
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 
 using System;
-using System.Linq;
 using Avalonia.Input;
 using Avalonia.Layout;
 
@@ -155,106 +156,122 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Measures the control.
+        /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content).
+        /// Children in this dimension are encouraged to be as large as they like.  In the other dimension,
+        /// StackPanel will assume the maximum size of its children.
         /// </summary>
-        /// <param name="availableSize">The available size.</param>
-        /// <returns>The desired size of the control.</returns>
+        /// <param name="availableSize">Constraint</param>
+        /// <returns>Desired size</returns>
         protected override Size MeasureOverride(Size availableSize)
         {
-            double childAvailableWidth = double.PositiveInfinity;
-            double childAvailableHeight = double.PositiveInfinity;
+            Size stackDesiredSize = new Size();
+            var children = Children;
+            Size layoutSlotSize = availableSize;
+            bool fHorizontal = (Orientation == Orientation.Horizontal);
+            double spacing = Spacing;
+            bool hasVisibleChild = false;
 
-            if (Orientation == Orientation.Vertical)
+            //
+            // Initialize child sizing and iterator data
+            // Allow children as much size as they want along the stack.
+            //
+            if (fHorizontal)
             {
-                childAvailableWidth = availableSize.Width;
-
-                if (!double.IsNaN(Width))
-                {
-                    childAvailableWidth = Width;
-                }
-
-                childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth);
-                childAvailableWidth = Math.Max(childAvailableWidth, MinWidth);
+                layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity);
             }
             else
             {
-                childAvailableHeight = availableSize.Height;
+                layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity);
+            }
 
-                if (!double.IsNaN(Height))
-                {
-                    childAvailableHeight = Height;
-                }
+            //
+            //  Iterate through children.
+            //  While we still supported virtualization, this was hidden in a child iterator (see source history).
+            //
+            for (int i = 0, count = children.Count; i < count; ++i)
+            {
+                // Get next child.
+                var child = children[i];
 
-                childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight);
-                childAvailableHeight = Math.Max(childAvailableHeight, MinHeight);
-            }
+                if (child == null)
+                { continue; }
 
-            double measuredWidth = 0;
-            double measuredHeight = 0;
-            double spacing = Spacing;
-            bool hasVisibleChild = Children.Any(c => c.IsVisible);
+                bool isVisible = child.IsVisible;
 
-            foreach (Control child in Children)
-            {
-                child.Measure(new Size(childAvailableWidth, childAvailableHeight));
-                Size size = child.DesiredSize;
+                if (isVisible && !hasVisibleChild)
+                {
+                    hasVisibleChild = true;
+                }
 
-                if (Orientation == Orientation.Vertical)
+                // Measure the child.
+                child.Measure(layoutSlotSize);
+                Size childDesiredSize = child.DesiredSize;
+
+                // Accumulate child size.
+                if (fHorizontal)
                 {
-                    measuredHeight += size.Height + (child.IsVisible ? spacing : 0);
-                    measuredWidth = Math.Max(measuredWidth, size.Width);
+                    stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width);
+                    stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height));
                 }
                 else
                 {
-                    measuredWidth += size.Width + (child.IsVisible ? spacing : 0);   
-                    measuredHeight = Math.Max(measuredHeight, size.Height);
+                    stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width));
+                    stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height);
                 }
             }
 
-            if (Orientation == Orientation.Vertical)
+            if (fHorizontal)
             {
-                measuredHeight -= (hasVisibleChild ? spacing : 0);
+                stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0));
             }
             else
-            {
-                measuredWidth -= (hasVisibleChild ? spacing : 0);
+            { 
+                stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0));
             }
 
-            return new Size(measuredWidth, measuredHeight).Constrain(availableSize);
+            return stackDesiredSize;
         }
 
-        /// <inheritdoc/>
+        /// <summary>
+        /// Content arrangement.
+        /// </summary>
+        /// <param name="finalSize">Arrange size</param>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            var orientation = Orientation;
+            var children = Children;
+            bool fHorizontal = (Orientation == Orientation.Horizontal);
+            Rect rcChild = new Rect(finalSize);
+            double previousChildSize = 0.0;
             var spacing = Spacing;
-            var finalRect = new Rect(finalSize);
-            var pos = 0.0;
 
-            foreach (Control child in Children)
+            //
+            // Arrange and Position Children.
+            //
+            for (int i = 0, count = children.Count; i < count; ++i)
             {
-                if (!child.IsVisible)
-                {
-                    continue;
-                }
+                var child = children[i];
 
-                double childWidth = child.DesiredSize.Width;
-                double childHeight = child.DesiredSize.Height;
+                if (child == null)
+                { continue; }
 
-                if (orientation == Orientation.Vertical)
+                if (fHorizontal)
                 {
-                    var rect = new Rect(0, pos, childWidth, childHeight)
-                        .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top);
-                    ArrangeChild(child, rect, finalSize, orientation);
-                    pos += childHeight + spacing;
+                    rcChild = rcChild.WithX(rcChild.X + previousChildSize);
+                    previousChildSize = child.DesiredSize.Width;
+                    rcChild = rcChild.WithWidth(previousChildSize);
+                    rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height));
+                    previousChildSize += spacing;
                 }
                 else
                 {
-                    var rect = new Rect(pos, 0, childWidth, childHeight)
-                        .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment);
-                    ArrangeChild(child, rect, finalSize, orientation);
-                    pos += childWidth + spacing;
+                    rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
+                    previousChildSize = child.DesiredSize.Height;
+                    rcChild = rcChild.WithHeight(previousChildSize);
+                    rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
+                    previousChildSize += spacing;
                 }
+
+                ArrangeChild(child, rcChild, finalSize, Orientation);
             }
 
             return finalSize;

+ 14 - 15
src/Avalonia.Controls/TextBlock.cs

@@ -1,12 +1,9 @@
 // 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.Reactive;
 using System.Reactive.Linq;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
-using Avalonia.Media.Immutable;
 using Avalonia.Metadata;
 
 namespace Avalonia.Controls
@@ -106,6 +103,14 @@ namespace Avalonia.Controls
                 FontWeightProperty,
                 FontSizeProperty,
                 FontStyleProperty);
+
+            Observable.Merge(
+                TextProperty.Changed,
+                TextAlignmentProperty.Changed,
+                FontSizeProperty.Changed,
+                FontStyleProperty.Changed,
+                FontWeightProperty.Changed
+            ).AddClassHandler<TextBlock>((x,_) => x.OnTextPropertiesChanged());
         }
 
         /// <summary>
@@ -114,18 +119,6 @@ namespace Avalonia.Controls
         public TextBlock()
         {
             _text = string.Empty;
-
-            Observable.Merge(
-                this.GetObservable(TextProperty).Select(_ => Unit.Default),
-                this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default),
-                this.GetObservable(FontSizeProperty).Select(_ => Unit.Default),
-                this.GetObservable(FontStyleProperty).Select(_ => Unit.Default),
-                this.GetObservable(FontWeightProperty).Select(_ => Unit.Default))
-                .Subscribe(_ =>
-                {
-                    InvalidateFormattedText();
-                    InvalidateMeasure();
-                });
         }
 
         /// <summary>
@@ -408,5 +401,11 @@ namespace Avalonia.Controls
             InvalidateFormattedText();
             InvalidateMeasure();
         }
+
+        private void OnTextPropertiesChanged()
+        {
+            InvalidateFormattedText();
+            InvalidateMeasure();
+        }
     }
 }

+ 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

+ 35 - 27
src/Avalonia.Diagnostics/DevTools.xaml.cs

@@ -21,7 +21,12 @@ namespace Avalonia
     {
         public static void AttachDevTools(this TopLevel control)
         {
-            Diagnostics.DevTools.Attach(control);
+            Diagnostics.DevTools.Attach(control, new KeyGesture(Key.F12));
+        }
+
+        public static void AttachDevTools(this TopLevel control, KeyGesture gesture)
+        {
+            Diagnostics.DevTools.Attach(control, gesture);
         }
     }
 }
@@ -52,42 +57,45 @@ namespace Avalonia.Diagnostics
 
         public IControl Root { get; }
 
-        public static IDisposable Attach(TopLevel control)
+        public static IDisposable Attach(TopLevel control, KeyGesture gesture)
         {
+            void PreviewKeyDown(object sender, KeyEventArgs e)
+            {
+                if (gesture.Matches(e))
+                {
+                    OpenDevTools(control);
+                }
+            }
+
             return control.AddHandler(
                 KeyDownEvent,
-                WindowPreviewKeyDown,
+                PreviewKeyDown,
                 RoutingStrategies.Tunnel);
         }
 
-        private static void WindowPreviewKeyDown(object sender, KeyEventArgs e)
+        private static void OpenDevTools(TopLevel control)
         {
-            if (e.Key == Key.F12)
+            if (s_open.TryGetValue(control, out var devToolsWindow))
+            {
+                devToolsWindow.Activate();
+            }
+            else
             {
-                var control = (TopLevel)sender;
+                var devTools = new DevTools(control);
 
-                if (s_open.TryGetValue(control, out var devToolsWindow))
+                devToolsWindow = new Window
                 {
-                    devToolsWindow.Activate();
-                }
-                else
-                {
-                    var devTools = new DevTools(control);
-
-                    devToolsWindow = new Window
-                    {
-                        Width = 1024,
-                        Height = 512,
-                        Content = devTools,
-                        DataTemplates = { new ViewLocator<ViewModelBase>() },
-                        Title = "Avalonia DevTools"
-                    };
-
-                    devToolsWindow.Closed += devTools.DevToolsClosed;
-                    s_open.Add(control, devToolsWindow);
-                    MarkAsDevTool(devToolsWindow);
-                    devToolsWindow.Show();
-                }
+                    Width = 1024,
+                    Height = 512,
+                    Content = devTools,
+                    DataTemplates = { new ViewLocator<ViewModelBase>() },
+                    Title = "Avalonia DevTools"
+                };
+
+                devToolsWindow.Closed += devTools.DevToolsClosed;
+                s_open.Add(control, devToolsWindow);
+                MarkAsDevTool(devToolsWindow);
+                devToolsWindow.Show();
             }
         }
 

+ 4 - 1
src/Avalonia.Input/Cursors.cs

@@ -28,7 +28,7 @@ namespace Avalonia.Input
         AppStarting,
         Help,
         TopSide,
-        BottomSize,
+        BottomSide,
         LeftSide,
         RightSide,
         TopLeftCorner,
@@ -40,6 +40,9 @@ namespace Avalonia.Input
         DragLink,
         None,
 
+        [Obsolete("Use BottomSide")]
+        BottomSize = BottomSide
+
         // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ 
         // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image
         // SizeNorthWestSouthEast,

+ 2 - 2
src/Avalonia.Input/DragDrop.cs

@@ -45,10 +45,10 @@ namespace Avalonia.Input
         /// Starts a dragging operation with the given <see cref="IDataObject"/> and returns the applied drop effect from the target.
         /// <seealso cref="DataObject"/>
         /// </summary>
-        public static Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+        public static Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
         {
             var src = AvaloniaLocator.Current.GetService<IPlatformDragSource>();
-            return src?.DoDragDrop(data, allowedEffects) ?? Task.FromResult(DragDropEffects.None);
+            return src?.DoDragDrop(triggerEvent, data, allowedEffects) ?? Task.FromResult(DragDropEffects.None);
         }
     }
 }

+ 1 - 1
src/Avalonia.Input/Platform/IPlatformDragSource.cs

@@ -4,6 +4,6 @@ namespace Avalonia.Input.Platform
 {
     public interface IPlatformDragSource
     {
-        Task<DragDropEffects> DoDragDrop(IDataObject data, DragDropEffects allowedEffects);
+        Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects);
     }
 }

+ 3 - 0
src/Avalonia.Layout/Layoutable.cs

@@ -534,6 +534,9 @@ namespace Avalonia.Layout
                 height = Math.Min(height, MaxHeight);
                 height = Math.Max(height, MinHeight);
 
+                width = Math.Min(width, availableSize.Width);
+                height = Math.Min(height, availableSize.Height);
+
                 if (UseLayoutRounding)
                 {
                     var scale = GetLayoutScale();

+ 0 - 6
src/Avalonia.Layout/UniformGridLayoutState.cs

@@ -72,12 +72,6 @@ namespace Avalonia.Layout
 
                     _cachedFirstElement.Measure(availableSize);
 
-                    // This doesn't need to be done in the UWP version and I'm not sure why. If we
-                    // don't do this here, and we receive a recycled element then it will be shown
-                    // at its previous arrange point, but we don't want it shown at all until its
-                    // arranged.
-                    _cachedFirstElement.Arrange(new Rect(-10000.0, -10000.0, 0, 0));
-
                     SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
 
                     // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.

+ 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

+ 8 - 3
src/Avalonia.ReactiveUI/AutoSuspendHelper.cs

@@ -35,7 +35,12 @@ namespace Avalonia.ReactiveUI
             RxApp.SuspensionHost.IsResuming = Observable.Never<Unit>();
             RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew;
 
-            if (lifetime is IControlledApplicationLifetime controlled)
+            if (Avalonia.Controls.Design.IsDesignMode)
+            {
+                this.Log().Debug("Design mode detected. AutoSuspendHelper won't persist app state.");
+                RxApp.SuspensionHost.ShouldPersistState = Observable.Never<IDisposable>();
+            }
+            else if (lifetime is IControlledApplicationLifetime controlled)
             {
                 this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit.");
                 controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit();
@@ -47,11 +52,11 @@ namespace Avalonia.ReactiveUI
                 var message = $"Don't know how to detect app exit event for {type}.";
                 throw new NotSupportedException(message);
             }
-            else 
+            else
             {
                 var message = "ApplicationLifetime is null. "
                             + "Ensure you are initializing AutoSuspendHelper "
-                            + "when Avalonia application initialization is completed.";
+                            + "after Avalonia application initialization is completed.";
                 throw new ArgumentNullException(message);
             }
             

+ 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>

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

@@ -1,5 +1,6 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="ButtonSpinner">
+    <Setter Property="Background" Value="Transparent"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
     <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
     <Setter Property="HorizontalContentAlignment" Value="Stretch"/>

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

@@ -1,5 +1,6 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="ComboBox">
+    <Setter Property="Background" Value="Transparent"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
     <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
     <Setter Property="Padding" Value="4"/>
@@ -39,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>

+ 21 - 14
src/Avalonia.Visuals/Media/BrushExtensions.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Media.Immutable;
 
 namespace Avalonia.Media
 {
@@ -23,27 +24,33 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        /// Converts a pen to a pen with an immutable brush
+        /// Converts a dash style to an immutable dash style.
+        /// </summary>
+        /// <param name="style">The dash style.</param>
+        /// <returns>
+        /// The result of calling <see cref="DashStyle.ToImmutable"/> if the style is mutable,
+        /// otherwise <paramref name="style"/>.
+        /// </returns>
+        public static ImmutableDashStyle ToImmutable(this IDashStyle style)
+        {
+            Contract.Requires<ArgumentNullException>(style != null);
+
+            return style as ImmutableDashStyle ?? ((DashStyle)style).ToImmutable();
+        }
+
+        /// <summary>
+        /// Converts a pen to an immutable pen.
         /// </summary>
         /// <param name="pen">The pen.</param>
         /// <returns>
-        /// A copy of the pen with an immutable brush, or <paramref name="pen"/> if the pen's brush
-        /// is already immutable or null.
+        /// The result of calling <see cref="Pen.ToImmutable"/> if the brush is mutable,
+        /// otherwise <paramref name="pen"/>.
         /// </returns>
-        public static Pen ToImmutable(this Pen pen)
+        public static ImmutablePen ToImmutable(this IPen pen)
         {
             Contract.Requires<ArgumentNullException>(pen != null);
 
-            var brush = pen.Brush?.ToImmutable();
-            return ReferenceEquals(pen.Brush, brush) ?
-                pen :
-                new Pen(
-                    brush,
-                    thickness: pen.Thickness,
-                    dashStyle: pen.DashStyle,                   
-                    lineCap: pen.LineCap,
-                    lineJoin: pen.LineJoin,
-                    miterLimit: pen.MiterLimit);
+            return pen as ImmutablePen ?? ((Pen)pen).ToImmutable();
         }
     }
 }

+ 85 - 43
src/Avalonia.Visuals/Media/DashStyle.cs

@@ -1,72 +1,114 @@
 namespace Avalonia.Media
 {
+    using System;
     using System.Collections.Generic;
+    using System.Linq;
     using Avalonia.Animation;
+    using Avalonia.Media.Immutable;
 
-    public class DashStyle : Animatable
+    /// <summary>
+    /// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>.
+    /// </summary>
+    public class DashStyle : Animatable, IDashStyle, IAffectsRender
     {
-        private static DashStyle dash;
-        public static DashStyle Dash
-        {
-            get
-            {
-                if (dashDotDot == null)
-                {
-                    dash = new DashStyle(new double[] { 2, 2 }, 1);
-                }
-
-                return dash;
-            }
-        }
+        /// <summary>
+        /// Defines the <see cref="Dashes"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<IReadOnlyList<double>> DashesProperty =
+            AvaloniaProperty.Register<DashStyle, IReadOnlyList<double>>(nameof(Dashes));
 
+        /// <summary>
+        /// Defines the <see cref="Offset"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<double> OffsetProperty =
+            AvaloniaProperty.Register<DashStyle, double>(nameof(Offset));
 
+        private static ImmutableDashStyle s_dash;
+        private static ImmutableDashStyle s_dot;
+        private static ImmutableDashStyle s_dashDot;
+        private static ImmutableDashStyle s_dashDotDot;
 
-        private static DashStyle dot;
-        public static DashStyle Dot
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DashStyle"/> class.
+        /// </summary>
+        public DashStyle()
+            : this(null, 0)
         {
-            get { return dot ?? (dot = new DashStyle(new double[] {0, 2}, 0)); }
         }
 
-        private static DashStyle dashDot;
-        public static DashStyle DashDot
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DashStyle"/> class.
+        /// </summary>
+        /// <param name="dashes">The dashes collection.</param>
+        /// <param name="offset">The dash sequence offset.</param>
+        public DashStyle(IEnumerable<double> dashes, double offset)
         {
-            get
-            {
-                if (dashDot == null)
-                {
-                    dashDot = new DashStyle(new double[] { 2, 2, 0, 2 }, 1);
-                }
-
-                return dashDot;
-            }
+            Dashes = (IReadOnlyList<double>)dashes?.ToList() ?? Array.Empty<double>();
+            Offset = offset;
         }
 
-        private static DashStyle dashDotDot;
-        public static DashStyle DashDotDot
+        static DashStyle()
         {
-            get
+            void RaiseInvalidated(AvaloniaPropertyChangedEventArgs e)
             {
-                if (dashDotDot == null)
-                {
-                    dashDotDot = new DashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1);
-                }
-
-                return dashDotDot;
+                ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty);
             }
+
+            DashesProperty.Changed.Subscribe(RaiseInvalidated);
+            OffsetProperty.Changed.Subscribe(RaiseInvalidated);
         }
 
+        /// <summary>
+        /// Represents a dashed <see cref="DashStyle"/>.
+        /// </summary>
+        public static IDashStyle Dash =>
+            s_dash ?? (s_dash = new ImmutableDashStyle(new double[] { 2, 2 }, 1));
+
+        /// <summary>
+        /// Represents a dotted <see cref="DashStyle"/>.
+        /// </summary>
+        public static IDashStyle Dot =>
+            s_dot ?? (s_dot = new ImmutableDashStyle(new double[] { 0, 2 }, 0));
+
+        /// <summary>
+        /// Represents a dashed dotted <see cref="DashStyle"/>.
+        /// </summary>
+        public static IDashStyle DashDot =>
+            s_dashDot ?? (s_dashDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1));
+
+        /// <summary>
+        /// Represents a dashed double dotted <see cref="DashStyle"/>.
+        /// </summary>
+        public static IDashStyle DashDotDot =>
+            s_dashDotDot ?? (s_dashDotDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1));
 
-        public DashStyle(IReadOnlyList<double> dashes = null, double offset = 0.0)
+        /// <summary>
+        /// Gets or sets the length of alternating dashes and gaps.
+        /// </summary>
+        public IReadOnlyList<double> Dashes
         {
-            this.Dashes = dashes;
-            this.Offset = offset;
+            get => GetValue(DashesProperty);
+            set => SetValue(DashesProperty, value);
         }
 
         /// <summary>
-        /// Gets and sets the length of alternating dashes and gaps.
+        /// Gets or sets how far in the dash sequence the stroke will start.
         /// </summary>
-        public IReadOnlyList<double> Dashes { get; }
+        public double Offset
+        {
+            get => GetValue(OffsetProperty);
+            set => SetValue(OffsetProperty, value);
+        }
 
-        public double Offset { get; }
+        /// <summary>
+        /// Raised when the dash style changes.
+        /// </summary>
+        public event EventHandler Invalidated;
+
+        /// <summary>
+        /// Returns an immutable clone of the <see cref="DashStyle"/>.
+        /// </summary>
+        /// <returns></returns>
+        public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset);
     }
 }

+ 4 - 4
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -94,7 +94,7 @@ namespace Avalonia.Media
         /// <param name="pen">The stroke pen.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
-        public void DrawLine(Pen pen, Point p1, Point p2)
+        public void DrawLine(IPen pen, Point p1, Point p2)
         {
             if (PenIsVisible(pen))
             {
@@ -108,7 +108,7 @@ namespace Avalonia.Media
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
-        public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry)
+        public void DrawGeometry(IBrush brush, IPen pen, Geometry geometry)
         {
             Contract.Requires<ArgumentNullException>(geometry != null);
 
@@ -124,7 +124,7 @@ namespace Avalonia.Media
         /// <param name="pen">The pen.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="cornerRadius">The corner radius.</param>
-        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f)
+        public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f)
         {
             if (PenIsVisible(pen))
             {
@@ -328,7 +328,7 @@ namespace Avalonia.Media
                 PlatformImpl.Dispose();
         }
 
-        private static bool PenIsVisible(Pen pen)
+        private static bool PenIsVisible(IPen pen)
         {
             return pen?.Brush != null && pen.Thickness > 0;
         }

+ 1 - 1
src/Avalonia.Visuals/Media/GeometryDrawing.cs

@@ -23,7 +23,7 @@
         public static readonly StyledProperty<Pen> PenProperty =
             AvaloniaProperty.Register<GeometryDrawing, Pen>(nameof(Pen));
 
-        public Pen Pen
+        public IPen Pen
         {
             get => GetValue(PenProperty);
             set => SetValue(PenProperty, value);

+ 20 - 0
src/Avalonia.Visuals/Media/IDashStyle.cs

@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>.
+    /// </summary>
+    public interface IDashStyle
+    {
+        /// <summary>
+        /// Gets or sets the length of alternating dashes and gaps.
+        /// </summary>
+        IReadOnlyList<double> Dashes { get; }
+
+        /// <summary>
+        /// Gets or sets how far in the dash sequence the stroke will start.
+        /// </summary>
+        double Offset { get; }
+    }
+}

+ 39 - 0
src/Avalonia.Visuals/Media/IPen.cs

@@ -0,0 +1,39 @@
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Describes how a stroke is drawn.
+    /// </summary>
+    public interface IPen
+    {
+        /// <summary>
+        /// Gets the brush used to draw the stroke.
+        /// </summary>
+        IBrush Brush { get; }
+
+        /// <summary>
+        /// Gets the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// </summary>
+        IDashStyle DashStyle { get; }
+
+        /// <summary>
+        /// Gets the type of shape to use on both ends of a line.
+        /// </summary>
+        PenLineCap LineCap { get; }
+
+        /// <summary>
+        /// Gets a value describing how to join consecutive line or curve segments in a 
+        /// <see cref="PathFigure"/> contained in a <see cref="PathGeometry"/> object.
+        /// </summary>
+        PenLineJoin LineJoin { get; }
+
+        /// <summary>
+        /// Gets the limit of the thickness of the join on a mitered corner.
+        /// </summary>
+        double MiterLimit { get; }
+
+        /// <summary>
+        /// Gets the stroke thickness.
+        /// </summary>
+        double Thickness { get; }
+    }
+}

+ 93 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs

@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Media.Immutable
+{
+    /// <summary>
+    /// Represents the sequence of dashes and gaps that will be applied by an
+    /// <see cref="ImmutablePen"/>.
+    /// </summary>
+    public class ImmutableDashStyle : IDashStyle, IEquatable<IDashStyle>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImmutableDashStyle"/> class.
+        /// </summary>
+        /// <param name="dashes">The dashes collection.</param>
+        /// <param name="offset">The dash sequence offset.</param>
+        public ImmutableDashStyle(IEnumerable<double> dashes, double offset)
+        {
+            Dashes = (IReadOnlyList<double>)dashes?.ToList() ?? Array.Empty<double>();
+            Offset = offset;
+        }
+
+        /// <inheritdoc/>
+        public IReadOnlyList<double> Dashes { get; }
+
+        /// <inheritdoc/>
+        public double Offset { get; }
+
+        /// <inheritdoc/>
+        public override bool Equals(object obj) => Equals(obj as IDashStyle);
+
+        /// <inheritdoc/>
+        public bool Equals(IDashStyle other)
+        {
+            if (ReferenceEquals(this, other))
+            {
+                return true;
+            }
+            else if (other is null)
+            {
+                return false;
+            }
+
+            if (Offset != other.Offset)
+            {
+                return false;
+            }
+
+            return SequenceEqual(Dashes, other.Dashes);
+        }
+
+        /// <inheritdoc/>
+        public override int GetHashCode()
+        {
+            var hashCode = 717868523;
+            hashCode = hashCode * -1521134295 + Offset.GetHashCode();
+
+            if (Dashes != null)
+            {
+                foreach (var i in Dashes)
+                {
+                    hashCode = hashCode * -1521134295 + i.GetHashCode();
+                }
+            }
+
+            return hashCode;
+        }
+
+        private static bool SequenceEqual(IReadOnlyList<double> left, IReadOnlyList<double> right)
+        {
+            if (left == right)
+            {
+                return true;
+            }
+
+            if (left == null || right == null || left.Count != right.Count)
+            {
+                return false;
+            }
+
+            for (var c = 0; c < left.Count; c++)
+            {
+                if (left[c] != right[c])
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+    }
+}

+ 118 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs

@@ -0,0 +1,118 @@
+// 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.Collections.Generic;
+
+namespace Avalonia.Media.Immutable
+{
+    /// <summary>
+    /// Describes how a stroke is drawn.
+    /// </summary>
+    public class ImmutablePen : IPen, IEquatable<IPen>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Pen"/> class.
+        /// </summary>
+        /// <param name="color">The stroke color.</param>
+        /// <param name="thickness">The stroke thickness.</param>
+        /// <param name="dashStyle">The dash style.</param>
+        /// <param name="lineCap">Specifies the type of graphic shape to use on both ends of a line.</param>
+        /// <param name="lineJoin">The line join.</param>
+        /// <param name="miterLimit">The miter limit.</param>
+        public ImmutablePen(
+            uint color,
+            double thickness = 1.0,
+            ImmutableDashStyle dashStyle = null,
+            PenLineCap lineCap = PenLineCap.Flat,
+            PenLineJoin lineJoin = PenLineJoin.Miter,
+            double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Pen"/> class.
+        /// </summary>
+        /// <param name="brush">The brush used to draw.</param>
+        /// <param name="thickness">The stroke thickness.</param>
+        /// <param name="dashStyle">The dash style.</param>
+        /// <param name="lineCap">The line cap.</param>
+        /// <param name="lineJoin">The line join.</param>
+        /// <param name="miterLimit">The miter limit.</param>
+        public ImmutablePen(
+            IBrush brush,
+            double thickness = 1.0,
+            ImmutableDashStyle dashStyle = null,
+            PenLineCap lineCap = PenLineCap.Flat,
+            PenLineJoin lineJoin = PenLineJoin.Miter,
+            double miterLimit = 10.0)
+        {
+            Brush = brush;
+            Thickness = thickness;
+            LineCap = lineCap;
+            LineJoin = lineJoin;
+            MiterLimit = miterLimit;
+            DashStyle = dashStyle;
+        }
+
+        /// <summary>
+        /// Gets the brush used to draw the stroke.
+        /// </summary>
+        public IBrush Brush { get; }
+
+        /// <summary>
+        /// Gets the stroke thickness.
+        /// </summary>
+        public double Thickness { get; }
+
+        /// <summary>
+        /// Specifies the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// </summary>
+        public IDashStyle DashStyle { get; }
+
+        /// <summary>
+        /// Specifies the type of graphic shape to use on both ends of a line.
+        /// </summary>
+        public PenLineCap LineCap { get; }
+
+        /// <summary>
+        /// Specifies how to join consecutive line or curve segments in a <see cref="PathFigure"/>
+        /// (subpaths) contained in a <see cref="PathGeometry"/> object.
+        /// </summary>
+        public PenLineJoin LineJoin { get; }
+
+        /// <summary>
+        /// The limit on the ratio of the miter length to half this pen's Thickness.
+        /// </summary>
+        public double MiterLimit { get; }
+
+        /// <inheritdoc/>
+        public override bool Equals(object obj) => Equals(obj as IPen);
+
+        /// <inheritdoc/>
+        public bool Equals(IPen other)
+        {
+            if (ReferenceEquals(this, other))
+            {
+                return true;
+            }
+            else if (other is null)
+            {
+                return false;
+            }
+
+            return EqualityComparer<IBrush>.Default.Equals(Brush, other.Brush) &&
+               Thickness == other.Thickness &&
+               EqualityComparer<IDashStyle>.Default.Equals(DashStyle, other.DashStyle) &&
+               LineCap == other.LineCap &&
+               LineJoin == other.LineJoin &&
+               MiterLimit == other.MiterLimit;
+        }
+
+        /// <inheritdoc/>
+        public override int GetHashCode()
+        {
+            return (Brush, Thickness, DashStyle, LineCap, LineJoin, MiterLimit).GetHashCode();
+        }
+    }
+}

+ 168 - 15
src/Avalonia.Visuals/Media/Pen.cs

@@ -1,13 +1,61 @@
 // 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.Collections.Generic;
+using Avalonia.Media.Immutable;
+using Avalonia.Utilities;
+
 namespace Avalonia.Media
 {
     /// <summary>
     /// Describes how a stroke is drawn.
     /// </summary>
-    public class Pen
+    public class Pen : AvaloniaObject, IPen
     {
+        /// <summary>
+        /// Defines the <see cref="Brush"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IBrush> BrushProperty =
+            AvaloniaProperty.Register<Pen, IBrush>(nameof(Brush));
+
+        /// <summary>
+        /// Defines the <see cref="Thickness"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> ThicknessProperty =
+            AvaloniaProperty.Register<Pen, double>(nameof(Thickness), 1.0);
+
+        /// <summary>
+        /// Defines the <see cref="DashStyle"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IDashStyle> DashStyleProperty =
+            AvaloniaProperty.Register<Pen, IDashStyle>(nameof(DashStyle));
+
+        /// <summary>
+        /// Defines the <see cref="LineCap"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PenLineCap> LineCapProperty =
+            AvaloniaProperty.Register<Pen, PenLineCap>(nameof(LineCap));
+
+        /// <summary>
+        /// Defines the <see cref="LineJoin"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PenLineJoin> LineJoinProperty =
+            AvaloniaProperty.Register<Pen, PenLineJoin>(nameof(LineJoin));
+
+        /// <summary>
+        /// Defines the <see cref="MiterLimit"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MiterLimitProperty =
+            AvaloniaProperty.Register<Pen, double>(nameof(MiterLimit), 10.0);
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Pen"/> class.
+        /// </summary>
+        public Pen()
+        {
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="Pen"/> class.
         /// </summary>
@@ -20,7 +68,7 @@ namespace Avalonia.Media
         public Pen(
             uint color,
             double thickness = 1.0,
-            DashStyle dashStyle = null,
+            IDashStyle dashStyle = null,
             PenLineCap lineCap = PenLineCap.Flat,
             PenLineJoin lineJoin = PenLineJoin.Miter,
             double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
@@ -39,7 +87,7 @@ namespace Avalonia.Media
         public Pen(
             IBrush brush,
             double thickness = 1.0,
-            DashStyle dashStyle = null,
+            IDashStyle dashStyle = null,
             PenLineCap lineCap = PenLineCap.Flat,
             PenLineJoin lineJoin = PenLineJoin.Miter,
             double miterLimit = 10.0)
@@ -52,34 +100,139 @@ namespace Avalonia.Media
             DashStyle = dashStyle;
         }
 
+        static Pen()
+        {
+            AffectsRender<Pen>(
+                BrushProperty,
+                ThicknessProperty,
+                DashStyleProperty,
+                LineCapProperty,
+                LineJoinProperty,
+                MiterLimitProperty);
+        }
+
+        /// <summary>
+        /// Gets or sets the brush used to draw the stroke.
+        /// </summary>
+        public IBrush Brush
+        {
+            get => GetValue(BrushProperty);
+            set => SetValue(BrushProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the stroke thickness.
+        /// </summary>
+        public double Thickness
+        {
+            get => GetValue(ThicknessProperty);
+            set => SetValue(ThicknessProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// </summary>
+        public IDashStyle DashStyle
+        {
+            get => GetValue(DashStyleProperty);
+            set => SetValue(DashStyleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the type of shape to use on both ends of a line.
+        /// </summary>
+        public PenLineCap LineCap
+        {
+            get => GetValue(LineCapProperty);
+            set => SetValue(LineCapProperty, value);
+        }
+
         /// <summary>
-        /// Gets the brush used to draw the stroke.
+        /// Gets or sets the join style for the ends of two consecutive lines drawn with this
+        /// <see cref="Pen"/>.
         /// </summary>
-        public IBrush Brush { get; }
+        public PenLineJoin LineJoin
+        {
+            get => GetValue(LineJoinProperty);
+            set => SetValue(LineJoinProperty, value);
+        }
 
         /// <summary>
-        /// Gets the stroke thickness.
+        /// Gets or sets the limit of the thickness of the join on a mitered corner.
         /// </summary>
-        public double Thickness { get; }
+        public double MiterLimit
+        {
+            get => GetValue(MiterLimitProperty);
+            set => SetValue(MiterLimitProperty, value);
+        }
 
         /// <summary>
-        /// Specifies the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// Raised when the pen changes.
         /// </summary>
-        public DashStyle DashStyle { get; }
+        public event EventHandler Invalidated;
 
         /// <summary>
-        /// Specifies the type of graphic shape to use on both ends of a line.
+        /// Creates an immutable clone of the brush.
         /// </summary>
-        public PenLineCap LineCap { get; }
+        /// <returns>The immutable clone.</returns>
+        public ImmutablePen ToImmutable()
+        {
+            return new ImmutablePen(
+                Brush?.ToImmutable(),
+                Thickness,
+                DashStyle?.ToImmutable(),
+                LineCap,
+                LineJoin,
+                MiterLimit);
+        }
 
         /// <summary>
-        /// Specifies how to join consecutive line or curve segments in a <see cref="PathFigure"/> (subpath) contained in a <see cref="PathGeometry"/> object.
+        /// Marks a property as affecting the pen's visual representation.
         /// </summary>
-        public PenLineJoin LineJoin { get; }
+        /// <param name="properties">The properties.</param>
+        /// <remarks>
+        /// After a call to this method in a pen's static constructor, any change to the
+        /// property will cause the <see cref="Invalidated"/> event to be raised on the pen.
+        /// </remarks>
+        protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
+            where T : Pen
+        {
+            void Invalidate(AvaloniaPropertyChangedEventArgs e)
+            {
+                if (e.Sender is T sender)
+                {
+                    if (e.OldValue is IAffectsRender oldValue)
+                    {
+                        WeakEventHandlerManager.Unsubscribe<EventArgs, T>(
+                            oldValue,
+                            nameof(oldValue.Invalidated),
+                            sender.AffectsRenderInvalidated);
+                    }
+
+                    if (e.NewValue is IAffectsRender newValue)
+                    {
+                        WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(
+                            newValue,
+                            nameof(newValue.Invalidated),
+                            sender.AffectsRenderInvalidated);
+                    }
+
+                    sender.RaiseInvalidated(EventArgs.Empty);
+                }
+            }
+
+            foreach (var property in properties)
+            {
+                property.Changed.Subscribe(Invalidate);
+            }
+        }
 
         /// <summary>
-        /// The limit on the ratio of the miter length to half this pen's Thickness.
+        /// Raises the <see cref="Invalidated"/> event.
         /// </summary>
-        public double MiterLimit { get; }
+        /// <param name="e">The event args.</param>
+        protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
+
+        private void AffectsRenderInvalidated(object sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty);
     }
 }

+ 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);
+        }
+    }
+}

+ 3 - 3
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@@ -50,7 +50,7 @@ namespace Avalonia.Platform
         /// <param name="pen">The stroke pen.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
-        void DrawLine(Pen pen, Point p1, Point p2);
+        void DrawLine(IPen pen, Point p1, Point p2);
 
         /// <summary>
         /// Draws a geometry.
@@ -58,7 +58,7 @@ namespace Avalonia.Platform
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
-        void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry);
+        void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry);
 
         /// <summary>
         /// Draws the outline of a rectangle.
@@ -66,7 +66,7 @@ namespace Avalonia.Platform
         /// <param name="pen">The pen.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="cornerRadius">The corner radius.</param>
-        void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f);
+        void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f);
 
         /// <summary>
         /// Draws text.

+ 2 - 2
src/Avalonia.Visuals/Platform/IGeometryImpl.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Platform
         /// </summary>
         /// <param name="pen">The pen to use. May be null.</param>
         /// <returns>The bounding rectangle.</returns>
-        Rect GetRenderBounds(Pen pen);
+        Rect GetRenderBounds(IPen pen);
 
         /// <summary>
         /// Indicates whether the geometry's fill contains the specified point.
@@ -42,7 +42,7 @@ namespace Avalonia.Platform
         /// <param name="pen">The stroke to use.</param>
         /// <param name="point">The point.</param>
         /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
-        bool StrokeContains(Pen pen, Point point);
+        bool StrokeContains(IPen pen, Point point);
 
         /// <summary>
         /// Makes a clone of the geometry with the specified transform.

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     internal abstract class BrushDrawOperation : DrawOperation
     {
-        public BrushDrawOperation(Rect bounds, Matrix transform, Pen pen)
+        public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen)
             : base(bounds, transform, pen)
         {
         }

+ 3 - 3
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
         {
             var next = NextDrawAs<GeometryNode>();
 
@@ -137,7 +137,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void DrawLine(Pen pen, Point p1, Point p2)
+        public void DrawLine(IPen pen, Point p1, Point p2)
         {
             var next = NextDrawAs<LineNode>();
 
@@ -152,7 +152,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+        public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0)
         {
             var next = NextDrawAs<RectangleNode>();
 

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     internal abstract class DrawOperation : IDrawOperation
     {
-        public DrawOperation(Rect bounds, Matrix transform, Pen pen)
+        public DrawOperation(Rect bounds, Matrix transform, IPen pen)
         {
             bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform);
             Bounds = new Rect(

+ 5 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@@ -3,6 +3,7 @@
 
 using System.Collections.Generic;
 using Avalonia.Media;
+using Avalonia.Media.Immutable;
 using Avalonia.Platform;
 using Avalonia.VisualTree;
 
@@ -24,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
         public GeometryNode(
             Matrix transform,
             IBrush brush,
-            Pen pen,
+            IPen pen,
             IGeometryImpl geometry,
             IDictionary<IVisual, Scene> childScenes = null)
             : base(geometry.GetRenderBounds(pen), transform, null)
@@ -49,7 +50,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <summary>
         /// Gets the stroke pen.
         /// </summary>
-        public Pen Pen { get; }
+        public ImmutablePen Pen { get; }
 
         /// <summary>
         /// Gets the geometry to draw.
@@ -71,11 +72,11 @@ namespace Avalonia.Rendering.SceneGraph
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry)
+        public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry)
         {
             return transform == Transform &&
                 Equals(brush, Brush) && 
-                pen == Pen &&
+                Equals(Pen, pen) &&
                 Equals(geometry, Geometry);
         }
 

+ 5 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@@ -3,6 +3,7 @@
 
 using System.Collections.Generic;
 using Avalonia.Media;
+using Avalonia.Media.Immutable;
 using Avalonia.Platform;
 using Avalonia.VisualTree;
 
@@ -23,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
         public LineNode(
             Matrix transform,
-            Pen pen,
+            IPen pen,
             Point p1,
             Point p2,
             IDictionary<IVisual, Scene> childScenes = null)
@@ -44,7 +45,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <summary>
         /// Gets the stroke pen.
         /// </summary>
-        public Pen Pen { get; }
+        public ImmutablePen Pen { get; }
 
         /// <summary>
         /// Gets the start point of the line.
@@ -71,9 +72,9 @@ namespace Avalonia.Rendering.SceneGraph
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(Matrix transform, Pen pen, Point p1, Point p2)
+        public bool Equals(Matrix transform, IPen pen, Point p1, Point p2)
         {
-            return transform == Transform && pen == Pen && p1 == P1 && p2 == P2;
+            return transform == Transform && Equals(Pen, pen) && p1 == P1 && p2 == P2;
         }
 
         public override void Render(IDrawingContextImpl context)

+ 5 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@@ -3,6 +3,7 @@
 
 using System.Collections.Generic;
 using Avalonia.Media;
+using Avalonia.Media.Immutable;
 using Avalonia.Platform;
 using Avalonia.VisualTree;
 
@@ -25,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph
         public RectangleNode(
             Matrix transform,
             IBrush brush,
-            Pen pen,
+            IPen pen,
             Rect rect,
             float cornerRadius,
             IDictionary<IVisual, Scene> childScenes = null)
@@ -52,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <summary>
         /// Gets the stroke pen.
         /// </summary>
-        public Pen Pen { get; }
+        public ImmutablePen Pen { get; }
 
         /// <summary>
         /// Gets the rectangle to draw.
@@ -80,11 +81,11 @@ namespace Avalonia.Rendering.SceneGraph
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius)
+        public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, float cornerRadius)
         {
             return transform == Transform &&
                 Equals(brush, Brush) &&
-                pen == Pen &&
+                Equals(Pen, pen) &&
                 rect == Rect &&
                 cornerRadius == CornerRadius;
         }

+ 6 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -179,7 +179,12 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="scene">The scene that the node is a part of.</param>
         public void SortChildren(Scene scene)
         {
-            var keys = new List<long>();
+            if (_children == null || _children.Count <= 1)
+            {
+                return;
+            }
+
+            var keys = new List<long>(Visual.VisualChildren.Count);
 
             for (var i = 0; i < Visual.VisualChildren.Count; ++i)
             {

+ 1 - 1
src/Avalonia.X11/X11CursorFactory.cs

@@ -24,7 +24,7 @@ namespace Avalonia.X11
                 {StandardCursorType.No, CursorFontShape.XC_X_cursor},
                 {StandardCursorType.Wait, CursorFontShape.XC_watch},
                 {StandardCursorType.AppStarting, CursorFontShape.XC_watch},
-                {StandardCursorType.BottomSize, CursorFontShape.XC_bottom_side},
+                {StandardCursorType.BottomSide, CursorFontShape.XC_bottom_side},
                 {StandardCursorType.DragCopy, CursorFontShape.XC_center_ptr},
                 {StandardCursorType.DragLink, CursorFontShape.XC_fleur},
                 {StandardCursorType.DragMove, CursorFontShape.XC_diamond_cross},

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

@@ -77,18 +77,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);
-        }
     }
 }
 
@@ -99,6 +94,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; }

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor