Browse Source

Merge branch 'master' into x11-popup-no-resized

danwalmsley 5 years ago
parent
commit
be22d56141
42 changed files with 1801 additions and 339 deletions
  1. 1 1
      azure-pipelines.yml
  2. 6 1
      src/Avalonia.Controls/AutoCompleteBox.cs
  3. 6 1
      src/Avalonia.Controls/Calendar/DatePicker.cs
  4. 6 1
      src/Avalonia.Controls/ComboBox.cs
  5. 29 28
      src/Avalonia.Controls/Primitives/Popup.cs
  6. 33 0
      src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
  7. 30 9
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  8. 45 0
      src/Avalonia.Controls/ScrollChangedEventArgs.cs
  9. 45 4
      src/Avalonia.Controls/ScrollViewer.cs
  10. 24 0
      src/Avalonia.Controls/Shapes/Path.cs
  11. 10 0
      src/Avalonia.Controls/TextBox.cs
  12. 46 23
      src/Avalonia.Controls/Window.cs
  13. 43 5
      src/Avalonia.Controls/WindowBase.cs
  14. 1 1
      src/Avalonia.Dialogs/ManagedFileChooser.xaml
  15. 1 1
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  16. 100 4
      src/Avalonia.Layout/AttachedLayout.cs
  17. 45 0
      src/Avalonia.Layout/LayoutContextAdapter.cs
  18. 40 10
      src/Avalonia.Layout/LayoutHelper.cs
  19. 8 28
      src/Avalonia.Layout/NonVirtualizingLayout.cs
  20. 17 0
      src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
  21. 160 0
      src/Avalonia.Layout/NonVirtualizingStackLayout.cs
  22. 4 4
      src/Avalonia.Layout/StackLayout.cs
  23. 4 4
      src/Avalonia.Layout/UniformGridLayout.cs
  24. 42 0
      src/Avalonia.Layout/VirtualLayoutContextAdapter.cs
  25. 8 28
      src/Avalonia.Layout/VirtualizingLayout.cs
  26. 5 0
      src/Avalonia.Layout/VirtualizingLayoutContext.cs
  27. 13 4
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  28. 16 4
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs
  29. 34 9
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  30. 21 1
      src/Avalonia.X11/X11Window.cs
  31. 5 5
      src/Avalonia.X11/XI2Manager.cs
  32. 6 14
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  33. 4 3
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  34. 153 2
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  35. 72 9
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  36. 72 1
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  37. 18 0
      tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
  38. 39 0
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  39. 165 0
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  40. 3 105
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  41. 335 0
      tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs
  42. 86 29
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

+ 1 - 1
azure-pipelines.yml

@@ -68,7 +68,7 @@ jobs:
     inputs:
       script: |
         brew update
-        brew install castxml
+        brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
 
   - task: CmdLine@2
     displayName: 'Install Nuke'

+ 6 - 1
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -1630,7 +1630,7 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="sender">The source object.</param>
         /// <param name="e">The event data.</param>
-        private void DropDownPopup_Closed(object sender, EventArgs e)
+        private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
         {
             // Force the drop down dependency property to be false.
             if (IsDropDownOpen)
@@ -1638,6 +1638,11 @@ namespace Avalonia.Controls
                 IsDropDownOpen = false;
             }
 
+            if (e.CloseEvent is PointerEventArgs pointerEvent)
+            {
+                pointerEvent.Handled = true;
+            }
+
             // Fire the DropDownClosed event
             if (_popupHasOpened)
             {

+ 6 - 1
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -895,12 +895,17 @@ namespace Avalonia.Controls
                 _ignoreButtonClick = false;
             }
         }
-        private void PopUp_Closed(object sender, EventArgs e)
+        private void PopUp_Closed(object sender, PopupClosedEventArgs e)
         {
             IsDropDownOpen = false;
 
             if(!_isPopupClosing)
             {
+                if (e.CloseEvent is PointerEventArgs pointerEvent)
+                {
+                    pointerEvent.Handled = true;
+                }
+
                 _isPopupClosing = true;
                 Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
             }

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

@@ -242,11 +242,16 @@ namespace Avalonia.Controls
             }
         }
 
-        private void PopupClosed(object sender, EventArgs e)
+        private void PopupClosed(object sender, PopupClosedEventArgs e)
         {
             _subscriptionsOnOpen?.Dispose();
             _subscriptionsOnOpen = null;
 
+            if (e.CloseEvent is PointerEventArgs pointerEvent)
+            {
+                pointerEvent.Handled = true;
+            }
+
             if (CanFocus(this))
             {
                 Focus();

+ 29 - 28
src/Avalonia.Controls/Primitives/Popup.cs

@@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Raised when the popup closes.
         /// </summary>
-        public event EventHandler? Closed;
+        public event EventHandler<PopupClosedEventArgs>? Closed;
 
         /// <summary>
         /// Raised when the popup opens.
@@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives
 
                 if (parentPopupRoot?.Parent is Popup popup)
                 {
-                    DeferCleanup(SubscribeToEventHandler<Popup, EventHandler>(popup, ParentClosed,
+                    DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<PopupClosedEventArgs>>(popup, ParentClosed,
                         (x, handler) => x.Closed += handler,
                         (x, handler) => x.Closed -= handler));
                 }
@@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Closes the popup.
         /// </summary>
-        public void Close()
-        {
-            if (_openState is null)
-            {
-                using (BeginIgnoringIsOpen())
-                {
-                    IsOpen = false;
-                }
-
-                return;
-            }
-
-            _openState.Dispose();
-            _openState = null;
-
-            using (BeginIgnoringIsOpen())
-            {
-                IsOpen = false;
-            }
-
-            Closed?.Invoke(this, EventArgs.Empty);
-        }
+        public void Close() => CloseCore(null);
 
         /// <summary>
         /// Measures the control.
@@ -389,22 +368,44 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        private void CloseCore(EventArgs? closeEvent)
+        {
+            if (_openState is null)
+            {
+                using (BeginIgnoringIsOpen())
+                {
+                    IsOpen = false;
+                }
+
+                return;
+            }
+
+            _openState.Dispose();
+            _openState = null;
+
+            using (BeginIgnoringIsOpen())
+            {
+                IsOpen = false;
+            }
+
+            Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
+        }
+
         private void ListenForNonClientClick(RawInputEventArgs e)
         {
             var mouse = e as RawPointerEventArgs;
 
             if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
             {
-                Close();
+                CloseCore(e);
             }
         }
 
         private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
         {
-            if (!StaysOpen && !IsChildOrThis((IVisual)e.Source))
+            if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
             {
-                Close();
-                e.Handled = true;
+                CloseCore(e);
             }
         }
 

+ 33 - 0
src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs

@@ -0,0 +1,33 @@
+using System;
+using Avalonia.Interactivity;
+
+#nullable enable
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Holds data for the <see cref="Popup.Closed"/> event.
+    /// </summary>
+    public class PopupClosedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PopupClosedEventArgs"/> class.
+        /// </summary>
+        /// <param name="closeEvent"></param>
+        public PopupClosedEventArgs(EventArgs? closeEvent)
+        {
+            CloseEvent = closeEvent;
+        }
+
+        /// <summary>
+        /// Gets the event that closed the popup, if any.
+        /// </summary>
+        /// <remarks>
+        /// If <see cref="Popup.StaysOpen"/> is false, then this property will hold details of the
+        /// interaction that caused the popup to close if the close was caused by e.g. a pointer press
+        /// outside the popup. It can be used to mark the event as handled if the event should not
+        /// be propagated.
+        /// </remarks>
+        public EventArgs? CloseEvent { get; }
+    }
+}

+ 30 - 9
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -117,20 +117,41 @@ namespace Avalonia.Controls.Primitives
             });
         }
 
-        /// <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)
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            var measured = base.MeasureOverride(availableSize);
+            var width = measured.Width;
+            var height = measured.Height;
+            var widthCache = Width;
+            var heightCache = Height;
+
+            if (!double.IsNaN(widthCache))
+            {
+                width = widthCache;
+            }
+
+            width = Math.Min(width, MaxWidth);
+            width = Math.Max(width, MinWidth);
+
+            if (!double.IsNaN(heightCache))
+            {
+                height = heightCache;
+            }
+
+            height = Math.Min(height, MaxHeight);
+            height = Math.Max(height, MinHeight);
+
+            return new Size(width, height);
+        }
+
+        protected override sealed Size ArrangeSetBounds(Size size)
         {
             using (BeginAutoSizing())
             {
-                _positionerParameters.Size = finalSize;
+                _positionerParameters.Size = size;
                 UpdatePosition();
+                return ClientSize;
             }
-
-            return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
         }
     }
 }

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

@@ -0,0 +1,45 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Describes a change in scrolling state.
+    /// </summary>
+    public class ScrollChangedEventArgs : RoutedEventArgs
+    {
+        public ScrollChangedEventArgs(
+            Vector extentDelta,
+            Vector offsetDelta,
+            Vector viewportDelta)
+            : this(ScrollViewer.ScrollChangedEvent, extentDelta, offsetDelta, viewportDelta)
+        {
+        }
+
+        public ScrollChangedEventArgs(
+            RoutedEvent routedEvent,
+            Vector extentDelta,
+            Vector offsetDelta,
+            Vector viewportDelta)
+            : base(routedEvent)
+        {
+            ExtentDelta = extentDelta;
+            OffsetDelta = offsetDelta;
+            ViewportDelta = viewportDelta;
+        }
+
+        /// <summary>
+        /// Gets the change to the value of <see cref="ScrollViewer.Extent"/>.
+        /// </summary>
+        public Vector ExtentDelta { get; }
+
+        /// <summary>
+        /// Gets the change to the value of <see cref="ScrollViewer.Offset"/>.
+        /// </summary>
+        public Vector OffsetDelta { get; }
+
+        /// <summary>
+        /// Gets the change to the value of <see cref="ScrollViewer.Viewport"/>.
+        /// </summary>
+        public Vector ViewportDelta { get; }
+    }
+}

+ 45 - 4
src/Avalonia.Controls/ScrollViewer.cs

@@ -2,6 +2,7 @@ using System;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 
 namespace Avalonia.Controls
 {
@@ -165,6 +166,14 @@ namespace Avalonia.Controls
                 nameof(VerticalScrollBarVisibility),
                 ScrollBarVisibility.Auto);
 
+        /// <summary>
+        /// Defines the <see cref="ScrollChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<ScrollChangedEventArgs> ScrollChangedEvent =
+            RoutedEvent.Register<ScrollViewer, ScrollChangedEventArgs>(
+                nameof(ScrollChanged),
+                RoutingStrategies.Bubble);
+
         internal const double DefaultSmallChange = 16;
 
         private IDisposable _childSubscription;
@@ -191,6 +200,15 @@ namespace Avalonia.Controls
         {
         }
 
+        /// <summary>
+        /// Occurs when changes are detected to the scroll position, extent, or viewport size.
+        /// </summary>
+        public event EventHandler<ScrollChangedEventArgs> ScrollChanged
+        {
+            add => AddHandler(ScrollChangedEvent, value);
+            remove => RemoveHandler(ScrollChangedEvent, value);
+        }
+
         /// <summary>
         /// Gets the extent of the scrollable content.
         /// </summary>
@@ -203,9 +221,11 @@ namespace Avalonia.Controls
 
             private set
             {
+                var old = _extent;
+
                 if (SetAndRaise(ExtentProperty, ref _extent, value))
                 {
-                    CalculatedPropertiesChanged();
+                    CalculatedPropertiesChanged(extentDelta: value - old);
                 }
             }
         }
@@ -222,11 +242,13 @@ namespace Avalonia.Controls
 
             set
             {
+                var old = _offset;
+
                 value = ValidateOffset(this, value);
 
                 if (SetAndRaise(OffsetProperty, ref _offset, value))
                 {
-                    CalculatedPropertiesChanged();
+                    CalculatedPropertiesChanged(offsetDelta: value - old);
                 }
             }
         }
@@ -243,9 +265,11 @@ namespace Avalonia.Controls
 
             private set
             {
+                var old = _viewport;
+
                 if (SetAndRaise(ViewportProperty, ref _viewport, value))
                 {
-                    CalculatedPropertiesChanged();
+                    CalculatedPropertiesChanged(viewportDelta: value - old);
                 }
             }
         }
@@ -525,7 +549,10 @@ namespace Avalonia.Controls
             }
         }
 
-        private void CalculatedPropertiesChanged()
+        private void CalculatedPropertiesChanged(
+            Size extentDelta = default,
+            Vector offsetDelta = default,
+            Size viewportDelta = default)
         {
             // Pass old values of 0 here because we don't have the old values at this point,
             // and it shouldn't matter as only the template uses these properies.
@@ -546,6 +573,20 @@ namespace Avalonia.Controls
                 SetAndRaise(SmallChangeProperty, ref _smallChange, new Size(DefaultSmallChange, DefaultSmallChange));
                 SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport);
             }
+
+            if (extentDelta != default || offsetDelta != default || viewportDelta != default)
+            {
+                using var route = BuildEventRoute(ScrollChangedEvent);
+
+                if (route.HasHandlers)
+                {
+                    var e = new ScrollChangedEventArgs(
+                        new Vector(extentDelta.Width, extentDelta.Height),
+                        offsetDelta,
+                        new Vector(viewportDelta.Width, viewportDelta.Height));
+                    route.RaiseEvent(this, e);
+                }
+            }
         }
 
         protected override void OnKeyDown(KeyEventArgs e)

+ 24 - 0
src/Avalonia.Controls/Shapes/Path.cs

@@ -1,3 +1,5 @@
+using System;
+using Avalonia.Data;
 using Avalonia.Media;
 
 namespace Avalonia.Controls.Shapes
@@ -10,6 +12,7 @@ namespace Avalonia.Controls.Shapes
         static Path()
         {
             AffectsGeometry<Path>(DataProperty);
+            DataProperty.Changed.AddClassHandler<Path>((o, e) => o.DataChanged(e));
         }
 
         public Geometry Data
@@ -19,5 +22,26 @@ namespace Avalonia.Controls.Shapes
         }
 
         protected override Geometry CreateDefiningGeometry() => Data;
+
+        private void DataChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var oldGeometry = (Geometry)e.OldValue;
+            var newGeometry = (Geometry)e.NewValue;
+
+            if (oldGeometry is object)
+            {
+                oldGeometry.Changed -= GeometryChanged;
+            }
+
+            if (newGeometry is object)
+            {
+                newGeometry.Changed += GeometryChanged;
+            }
+        }
+
+        private void GeometryChanged(object sender, EventArgs e)
+        {
+            InvalidateGeometry();
+        }
     }
 }

+ 10 - 0
src/Avalonia.Controls/TextBox.cs

@@ -473,8 +473,10 @@ namespace Avalonia.Controls
             {
                 if (!IsPasswordBox)
                 {
+                    _undoRedoHelper.Snapshot();
                     Copy();
                     DeleteSelection();
+                    _undoRedoHelper.Snapshot();
                 }
 
                 handled = true;
@@ -600,6 +602,7 @@ namespace Avalonia.Controls
                         break;
 
                     case Key.Back:
+                        _undoRedoHelper.Snapshot();
                         if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
                         {
                             SetSelectionForControlBackspace();
@@ -623,11 +626,13 @@ namespace Avalonia.Controls
                             CaretIndex -= removedCharacters;
                             SelectionStart = SelectionEnd = CaretIndex;
                         }
+                        _undoRedoHelper.Snapshot();
 
                         handled = true;
                         break;
 
                     case Key.Delete:
+                        _undoRedoHelper.Snapshot();
                         if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
                         {
                             SetSelectionForControlDelete();
@@ -649,6 +654,7 @@ namespace Avalonia.Controls
                             SetTextInternal(text.Substring(0, caretIndex) +
                                             text.Substring(caretIndex + removedCharacters));
                         }
+                        _undoRedoHelper.Snapshot();
 
                         handled = true;
                         break;
@@ -656,7 +662,9 @@ namespace Avalonia.Controls
                     case Key.Enter:
                         if (AcceptsReturn)
                         {
+                            _undoRedoHelper.Snapshot();
                             HandleTextInput(NewLine);
+                            _undoRedoHelper.Snapshot();
                             handled = true;
                         }
 
@@ -665,7 +673,9 @@ namespace Avalonia.Controls
                     case Key.Tab:
                         if (AcceptsTab)
                         {
+                            _undoRedoHelper.Snapshot();
                             HandleTextInput("\t");
+                            _undoRedoHelper.Snapshot();
                             handled = true;
                         }
                         else

+ 46 - 23
src/Avalonia.Controls/Window.cs

@@ -313,22 +313,7 @@ namespace Avalonia.Controls
         /// Should be called from left mouse button press event handler
         /// </summary>
         public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
-        
-        /// <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;
 
@@ -450,6 +435,19 @@ namespace Avalonia.Controls
 
             EnsureInitialized();
             IsVisible = true;
+
+            var initialSize = new Size(
+                double.IsNaN(Width) ? ClientSize.Width : Width,
+                double.IsNaN(Height) ? ClientSize.Height : Height);
+
+            if (initialSize != ClientSize)
+            {
+                using (BeginAutoSizing())
+                {
+                    PlatformImpl?.Resize(initialSize);
+                }
+            }
+
             LayoutManager.ExecuteInitialLayoutPass(this);
 
             using (BeginAutoSizing())
@@ -569,38 +567,60 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <inheritdoc/>
         protected override Size MeasureOverride(Size availableSize)
         {
             var sizeToContent = SizeToContent;
-            var clientSize = ClientSize;
             var constraint = availableSize;
+            var clientSize = ClientSize;
 
-            if ((sizeToContent & SizeToContent.Width) != 0)
+            if (sizeToContent.HasFlagCustom(SizeToContent.Width))
             {
                 constraint = constraint.WithWidth(double.PositiveInfinity);
             }
 
-            if ((sizeToContent & SizeToContent.Height) != 0)
+            if (sizeToContent.HasFlagCustom(SizeToContent.Height))
             {
                 constraint = constraint.WithHeight(double.PositiveInfinity);
             }
 
             var result = base.MeasureOverride(constraint);
 
-            if ((sizeToContent & SizeToContent.Width) == 0)
+            if (!sizeToContent.HasFlagCustom(SizeToContent.Width))
             {
-                result = result.WithWidth(clientSize.Width);
+                if (!double.IsInfinity(availableSize.Width))
+                {
+                    result = result.WithWidth(availableSize.Width);
+                }
+                else
+                {
+                    result = result.WithWidth(clientSize.Width);
+                }
             }
 
-            if ((sizeToContent & SizeToContent.Height) == 0)
+            if (!sizeToContent.HasFlagCustom(SizeToContent.Height))
             {
-                result = result.WithHeight(clientSize.Height);
+                if (!double.IsInfinity(availableSize.Height))
+                {
+                    result = result.WithHeight(availableSize.Height);
+                }
+                else
+                {
+                    result = result.WithHeight(clientSize.Height);
+                }
             }
 
             return result;
         }
 
+        protected sealed override Size ArrangeSetBounds(Size size)
+        {
+            using (BeginAutoSizing())
+            {
+                PlatformImpl?.Resize(size);
+                return ClientSize;
+            }
+        }
+
         protected sealed override void HandleClosed()
         {
             RaiseEvent(new RoutedEventArgs(WindowClosedEvent));
@@ -616,6 +636,9 @@ namespace Avalonia.Controls
                 SizeToContent = SizeToContent.Manual;
             }
 
+            Width = clientSize.Width;
+            Height = clientSize.Height;
+
             base.HandleResized(clientSize);
         }
 

+ 43 - 5
src/Avalonia.Controls/WindowBase.cs

@@ -224,16 +224,54 @@ namespace Avalonia.Controls
         /// <param name="clientSize">The new client size.</param>
         protected override void HandleResized(Size clientSize)
         {
-            if (!AutoSizing)
-            {
-                Width = clientSize.Width;
-                Height = clientSize.Height;
-            }
             ClientSize = clientSize;
             LayoutManager.ExecuteLayoutPass();
             Renderer?.Resized(clientSize);
         }
 
+        /// <summary>
+        /// Overrides the core measure logic for windows.
+        /// </summary>
+        /// <param name="availableSize">The available size.</param>
+        /// <returns>The measured size.</returns>
+        /// <remarks>
+        /// The layout logic for top-level windows is different than for other controls because
+        /// they don't have a parent, meaning that many layout properties handled by the default
+        /// MeasureCore (such as margins and alignment) make no sense.
+        /// </remarks>
+        protected override Size MeasureCore(Size availableSize)
+        {
+            ApplyStyling();
+            ApplyTemplate();
+
+            var constraint = LayoutHelper.ApplyLayoutConstraints(this, availableSize);
+
+            return MeasureOverride(constraint);
+        }
+
+        /// <summary>
+        /// Overrides the core arrange logic for windows.
+        /// </summary>
+        /// <param name="finalRect">The final arrange rect.</param>
+        /// <remarks>
+        /// The layout logic for top-level windows is different than for other controls because
+        /// they don't have a parent, meaning that many layout properties handled by the default
+        /// ArrangeCore (such as margins and alignment) make no sense.
+        /// </remarks>
+        protected override void ArrangeCore(Rect finalRect)
+        {
+            var constraint = ArrangeSetBounds(finalRect.Size);
+            var arrangeSize = ArrangeOverride(constraint);
+            Bounds = new Rect(arrangeSize);
+        }
+
+        /// <summary>
+        /// Called durung the arrange pass to set the size of the window.
+        /// </summary>
+        /// <param name="size">The requested size of the window.</param>
+        /// <returns>The actual size of the window.</returns>
+        protected virtual Size ArrangeSetBounds(Size size) => size;
+
         /// <summary>
         /// Handles a window position change notification from 
         /// <see cref="IWindowBaseImpl.PositionChanged"/>.

+ 1 - 1
src/Avalonia.Dialogs/ManagedFileChooser.xaml

@@ -58,7 +58,7 @@
             <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
                 <StackPanel.Styles>
                     <Style Selector="Button">
-                        <Setter Property="Margin">4</Setter>
+                        <Setter Property="Margin" Value="4"/>
                     </Style>
                 </StackPanel.Styles>
                 <Button Command="{Binding Ok}">OK</Button>

+ 1 - 1
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw
         /// <summary>
         /// Gets the type of the event.
         /// </summary>
-        public RawPointerEventType Type { get; private set; }
+        public RawPointerEventType Type { get; set; }
 
         /// <summary>
         /// Gets the input modifiers.

+ 100 - 4
src/Avalonia.Layout/AttachedLayout.cs

@@ -46,7 +46,23 @@ namespace Avalonia.Layout
         /// <see cref="VirtualizingLayout.InitializeForContextCore"/> to provide the behavior for
         /// this method in a derived class.
         /// </remarks>
-        public abstract void InitializeForContext(LayoutContext context);
+        public void InitializeForContext(LayoutContext context)
+        {
+            if (this is VirtualizingLayout virtualizingLayout)
+            {
+                var virtualizingContext = GetVirtualizingLayoutContext(context);
+                virtualizingLayout.InitializeForContextCore(virtualizingContext);
+            }
+            else if (this is NonVirtualizingLayout nonVirtualizingLayout)
+            {
+                var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
+                nonVirtualizingLayout.InitializeForContextCore(nonVirtualizingContext);
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
+        }
 
         /// <summary>
         /// Removes any state the layout previously stored on the ILayoutable container.
@@ -55,7 +71,23 @@ namespace Avalonia.Layout
         /// The context object that facilitates communication between the layout and its host
         /// container.
         /// </param>
-        public abstract void UninitializeForContext(LayoutContext context);
+        public void UninitializeForContext(LayoutContext context)
+        {
+            if (this is VirtualizingLayout virtualizingLayout)
+            {
+                var virtualizingContext = GetVirtualizingLayoutContext(context);
+                virtualizingLayout.UninitializeForContextCore(virtualizingContext);
+            }
+            else if (this is NonVirtualizingLayout nonVirtualizingLayout)
+            {
+                var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
+                nonVirtualizingLayout.UninitializeForContextCore(nonVirtualizingContext);
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
+        }
 
         /// <summary>
         /// Suggests a DesiredSize for a container element. A container element that supports
@@ -73,7 +105,23 @@ namespace Avalonia.Layout
         /// if scrolling or other resize behavior is possible in that particular container.
         /// </param>
         /// <returns></returns>
-        public abstract Size Measure(LayoutContext context, Size availableSize);
+        public Size Measure(LayoutContext context, Size availableSize)
+        {
+            if (this is VirtualizingLayout virtualizingLayout)
+            {
+                var virtualizingContext = GetVirtualizingLayoutContext(context);
+                return virtualizingLayout.MeasureOverride(virtualizingContext, availableSize);
+            }
+            else if (this is NonVirtualizingLayout nonVirtualizingLayout)
+            {
+                var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
+                return nonVirtualizingLayout.MeasureOverride(nonVirtualizingContext, availableSize);
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
+        }
 
         /// <summary>
         /// Positions child elements and determines a size for a container UIElement. Container
@@ -88,7 +136,23 @@ namespace Avalonia.Layout
         /// The final size that the container computes for the child in layout.
         /// </param>
         /// <returns>The actual size that is used after the element is arranged in layout.</returns>
-        public abstract Size Arrange(LayoutContext context, Size finalSize);
+        public Size Arrange(LayoutContext context, Size finalSize)
+        {
+            if (this is VirtualizingLayout virtualizingLayout)
+            {
+                var virtualizingContext = GetVirtualizingLayoutContext(context);
+                return virtualizingLayout.ArrangeOverride(virtualizingContext, finalSize);
+            }
+            else if (this is NonVirtualizingLayout nonVirtualizingLayout)
+            {
+                var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
+                return nonVirtualizingLayout.ArrangeOverride(nonVirtualizingContext, finalSize);
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
+        }
 
         /// <summary>
         /// Invalidates the measurement state (layout) for all ILayoutable containers that reference
@@ -102,5 +166,37 @@ namespace Avalonia.Layout
         /// occurs asynchronously.
         /// </summary>
         protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
+
+        private VirtualizingLayoutContext GetVirtualizingLayoutContext(LayoutContext context)
+        {
+            if (context is VirtualizingLayoutContext virtualizingContext)
+            {
+                return virtualizingContext;
+            }
+            else if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
+            {
+                return nonVirtualizingContext.GetVirtualizingContextAdapter();
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
+        }
+
+        private NonVirtualizingLayoutContext GetNonVirtualizingLayoutContext(LayoutContext context)
+        {
+            if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
+            {
+                return nonVirtualizingContext;
+            }
+            else if (context is VirtualizingLayoutContext virtualizingContext)
+            {
+                return virtualizingContext.GetNonVirtualizingContextAdapter();
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
+        }
     }
 }

+ 45 - 0
src/Avalonia.Layout/LayoutContextAdapter.cs

@@ -0,0 +1,45 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Layout
+{
+    internal class LayoutContextAdapter : VirtualizingLayoutContext
+    {
+        private readonly NonVirtualizingLayoutContext _nonVirtualizingContext;
+
+        public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext)
+        {
+            _nonVirtualizingContext = nonVirtualizingContext;
+        }
+
+        protected override object LayoutStateCore 
+        { 
+            get => _nonVirtualizingContext.LayoutState;
+            set => _nonVirtualizingContext.LayoutState = value; 
+        }
+
+        protected override Point LayoutOriginCore 
+        {
+            get => default;
+            set 
+            { 
+                if (value != default)
+                {
+                    throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized.");
+                }
+            }
+        }
+
+        protected override Rect RealizationRectCore() => new Rect(Size.Infinity);
+
+        protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count;
+        protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index];
+        protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) =>
+            _nonVirtualizingContext.Children[index];
+        protected override void RecycleElementCore(ILayoutable element) { }
+    }
+}

+ 40 - 10
src/Avalonia.Layout/LayoutHelper.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Utilities;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Layout
@@ -19,16 +20,11 @@ namespace Avalonia.Layout
         /// <returns>The control's size.</returns>
         public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints)
         {
-            var controlWidth = control.Width;
-            var controlHeight = control.Height;
-
-            double width = (controlWidth > 0) ? controlWidth : constraints.Width;
-            double height = (controlHeight > 0) ? controlHeight : constraints.Height;
-            width = Math.Min(width, control.MaxWidth);
-            width = Math.Max(width, control.MinWidth);
-            height = Math.Min(height, control.MaxHeight);
-            height = Math.Max(height, control.MinHeight);
-            return new Size(width, height);
+            var minmax = new MinMax(control);
+
+            return new Size(
+                MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth),
+                MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight));
         }
 
         public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding,
@@ -85,5 +81,39 @@ namespace Avalonia.Layout
 
             InnerInvalidateMeasure(control);
         }
+
+        /// <summary>
+        /// Calculates the min and max height for a control. Ported from WPF.
+        /// </summary>
+        private readonly struct MinMax
+        {
+            public MinMax(ILayoutable e)
+            {
+                MaxHeight = e.MaxHeight;
+                MinHeight = e.MinHeight;
+                double l = e.Height;
+
+                double height = (double.IsNaN(l) ? double.PositiveInfinity : l);
+                MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight);
+
+                height = (double.IsNaN(l) ? 0 : l);
+                MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight);
+
+                MaxWidth = e.MaxWidth;
+                MinWidth = e.MinWidth;
+                l = e.Width;
+
+                double width = (double.IsNaN(l) ? double.PositiveInfinity : l);
+                MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth);
+
+                width = (double.IsNaN(l) ? 0 : l);
+                MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth);
+            }
+
+            public double MinWidth { get; }
+            public double MaxWidth { get; }
+            public double MinHeight { get; }
+            public double MaxHeight { get; }
+        }
     }
 }

+ 8 - 28
src/Avalonia.Layout/NonVirtualizingLayout.cs

@@ -17,30 +17,6 @@ namespace Avalonia.Layout
     /// </remarks>
     public abstract class NonVirtualizingLayout : AttachedLayout
     {
-        /// <inheritdoc/>
-        public sealed override void InitializeForContext(LayoutContext context)
-        {
-            InitializeForContextCore((NonVirtualizingLayoutContext)context);
-        }
-
-        /// <inheritdoc/>
-        public sealed override void UninitializeForContext(LayoutContext context)
-        {
-            UninitializeForContextCore((NonVirtualizingLayoutContext)context);
-        }
-
-        /// <inheritdoc/>
-        public sealed override Size Measure(LayoutContext context, Size availableSize)
-        {
-            return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
-        }
-
-        /// <inheritdoc/>
-        public sealed override Size Arrange(LayoutContext context, Size finalSize)
-        {
-            return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
-        }
-
         /// <summary>
         /// When overridden in a derived class, initializes any per-container state the layout
         /// requires when it is attached to an ILayoutable container.
@@ -49,7 +25,7 @@ namespace Avalonia.Layout
         /// The context object that facilitates communication between the layout and its host
         /// container.
         /// </param>
-        protected virtual void InitializeForContextCore(LayoutContext context)
+        protected internal virtual void InitializeForContextCore(LayoutContext context)
         {
         }
 
@@ -61,7 +37,7 @@ namespace Avalonia.Layout
         /// The context object that facilitates communication between the layout and its host
         /// container.
         /// </param>
-        protected virtual void UninitializeForContextCore(LayoutContext context)
+        protected internal virtual void UninitializeForContextCore(LayoutContext context)
         {
         }
 
@@ -83,7 +59,9 @@ namespace Avalonia.Layout
         /// of the allocated sizes for child objects or based on other considerations such as a
         /// fixed container size.
         /// </returns>
-        protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
+        protected internal abstract Size MeasureOverride(
+            NonVirtualizingLayoutContext context,
+            Size availableSize);
 
         /// <summary>
         /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -98,6 +76,8 @@ namespace Avalonia.Layout
         /// its children.
         /// </param>
         /// <returns>The actual size that is used after the element is arranged in layout.</returns>
-        protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize;
+        protected internal virtual Size ArrangeOverride(
+            NonVirtualizingLayoutContext context,
+            Size finalSize) => finalSize;
     }
 }

+ 17 - 0
src/Avalonia.Layout/NonVirtualizingLayoutContext.cs

@@ -3,6 +3,8 @@
 //
 // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 
+using System.Collections.Generic;
+
 namespace Avalonia.Layout
 {
     /// <summary>
@@ -10,5 +12,20 @@ namespace Avalonia.Layout
     /// </summary>
     public abstract class NonVirtualizingLayoutContext : LayoutContext
     {
+        private VirtualizingLayoutContext _contextAdapter;
+
+        /// <summary>
+        /// Gets the collection of child controls from the container that provides the context.
+        /// </summary>
+        public IReadOnlyList<ILayoutable> Children => ChildrenCore;
+
+        /// <summary>
+        /// Implements the behavior for getting the return value of <see cref="Children"/> in a
+        /// derived or custom <see cref="NonVirtualizingLayoutContext"/>.
+        /// </summary>
+        protected abstract IReadOnlyList<ILayoutable> ChildrenCore { get; }
+
+        internal VirtualizingLayoutContext GetVirtualizingContextAdapter() =>
+            _contextAdapter ?? (_contextAdapter = new LayoutContextAdapter(this));
     }
 }

+ 160 - 0
src/Avalonia.Layout/NonVirtualizingStackLayout.cs

@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Data;
+
+namespace Avalonia.Layout
+{
+    public class NonVirtualizingStackLayout : NonVirtualizingLayout
+    {
+        /// <summary>
+        /// Defines the <see cref="Orientation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Orientation> OrientationProperty =
+            StackLayout.OrientationProperty.AddOwner<NonVirtualizingStackLayout>();
+
+        /// <summary>
+        /// Defines the <see cref="Spacing"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> SpacingProperty =
+            StackLayout.SpacingProperty.AddOwner<NonVirtualizingStackLayout>();
+
+        /// <summary>
+        /// Gets or sets the axis along which items are laid out.
+        /// </summary>
+        /// <value>
+        /// One of the enumeration values that specifies the axis along which items are laid out.
+        /// The default is Vertical.
+        /// </value>
+        public Orientation Orientation
+        {
+            get => GetValue(OrientationProperty);
+            set => SetValue(OrientationProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
+        /// direction of the StackLayout's Orientation.
+        /// </summary>
+        public double Spacing
+        {
+            get => GetValue(SpacingProperty);
+            set => SetValue(SpacingProperty, value);
+        }
+
+        protected internal override Size MeasureOverride(
+            NonVirtualizingLayoutContext context,
+            Size availableSize)
+        {
+            var extentU = 0.0;
+            var extentV = 0.0;
+            var childCount = context.Children.Count;
+            var isVertical = Orientation == Orientation.Vertical;
+            var spacing = Spacing;
+            var constraint = isVertical ?
+                availableSize.WithHeight(double.PositiveInfinity) :
+                availableSize.WithWidth(double.PositiveInfinity);
+
+            for (var i = 0; i < childCount; ++i)
+            {
+                var element = context.Children[i];
+
+                if (!element.IsVisible)
+                {
+                    continue;
+                }
+
+                element.Measure(constraint);
+                
+                if (isVertical)
+                {
+                    extentU += element.DesiredSize.Height;
+                    extentV = Math.Max(extentV, element.DesiredSize.Width);
+                }
+                else
+                {
+                    extentU += element.DesiredSize.Width;
+                    extentV = Math.Max(extentV, element.DesiredSize.Height);
+                }
+
+                if (i < childCount - 1)
+                {
+                    extentU += spacing;
+                }
+            }
+
+            return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV);
+        }
+
+        protected internal override Size ArrangeOverride(
+            NonVirtualizingLayoutContext context,
+            Size finalSize)
+        {
+            var u = 0.0;
+            var childCount = context.Children.Count;
+            var isVertical = Orientation == Orientation.Vertical;
+            var spacing = Spacing;
+            var bounds = new Rect();
+
+            for (var i = 0; i < childCount; ++i)
+            {
+                var element = context.Children[i];
+
+                if (!element.IsVisible)
+                {
+                    continue;
+                }
+
+                bounds = isVertical ?
+                    LayoutVertical(element, u, finalSize) :
+                    LayoutHorizontal(element, u, finalSize);
+                element.Arrange(bounds);
+                u = (isVertical ? bounds.Bottom : bounds.Right) + spacing;
+            }
+
+            return new Size(bounds.Right, bounds.Bottom);
+        }
+
+        private static Rect LayoutVertical(ILayoutable element, double y, Size constraint)
+        {
+            var x = 0.0;
+            var width = element.DesiredSize.Width;
+
+            switch (element.HorizontalAlignment)
+            {
+                case HorizontalAlignment.Center:
+                    x += (constraint.Width - element.DesiredSize.Width) / 2;
+                    break;
+                case HorizontalAlignment.Right:
+                    x += constraint.Width - element.DesiredSize.Width;
+                    break;
+                case HorizontalAlignment.Stretch:
+                    width = constraint.Width;
+                    break;
+            }
+
+            return new Rect(x, y, width, element.DesiredSize.Height);
+        }
+
+        private static Rect LayoutHorizontal(ILayoutable element, double x, Size constraint)
+        {
+            var y = 0.0;
+            var height = element.DesiredSize.Height;
+
+            switch (element.VerticalAlignment)
+            {
+                case VerticalAlignment.Center:
+                    y += (constraint.Height - element.DesiredSize.Height) / 2;
+                    break;
+                case VerticalAlignment.Bottom:
+                    y += constraint.Height - element.DesiredSize.Height;
+                    break;
+                case VerticalAlignment.Stretch:
+                    height = constraint.Height;
+                    break;
+            }
+
+            return new Rect(x, y, element.DesiredSize.Width, height);
+        }
+    }
+}

+ 4 - 4
src/Avalonia.Layout/StackLayout.cs

@@ -234,7 +234,7 @@ namespace Avalonia.Layout
             return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
         }
 
-        protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+        protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
         {
             var state = context.LayoutState;
             var stackState = state as StackLayoutState;
@@ -254,13 +254,13 @@ namespace Avalonia.Layout
             stackState.InitializeForContext(context, this);
         }
 
-        protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+        protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
         {
             var stackState = (StackLayoutState)context.LayoutState;
             stackState.UninitializeForContext(context);
         }
 
-        protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+        protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
         {
             var desiredSize = GetFlowAlgorithm(context).Measure(
                 availableSize,
@@ -275,7 +275,7 @@ namespace Avalonia.Layout
             return new Size(desiredSize.Width, desiredSize.Height);
         }
 
-        protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+        protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
         {
             var value = GetFlowAlgorithm(context).Arrange(
                finalSize,

+ 4 - 4
src/Avalonia.Layout/UniformGridLayout.cs

@@ -392,7 +392,7 @@ namespace Avalonia.Layout
         {
         }
 
-        protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+        protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
         {
             var state = context.LayoutState;
             var gridState = state as UniformGridLayoutState;
@@ -412,13 +412,13 @@ namespace Avalonia.Layout
             gridState.InitializeForContext(context, this);
         }
 
-        protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+        protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
         {
             var gridState = (UniformGridLayoutState)context.LayoutState;
             gridState.UninitializeForContext(context);
         }
 
-        protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+        protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
         {
             // Set the width and height on the grid state. If the user already set them then use the preset. 
             // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
@@ -442,7 +442,7 @@ namespace Avalonia.Layout
             return new Size(desiredSize.Width, desiredSize.Height);
         }
 
-        protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+        protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
         {
             var value = GetFlowAlgorithm(context).Arrange(
                finalSize,

+ 42 - 0
src/Avalonia.Layout/VirtualLayoutContextAdapter.cs

@@ -0,0 +1,42 @@
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Avalonia.Layout
+{
+    public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext
+    {
+        private readonly VirtualizingLayoutContext _virtualizingContext;
+        private ChildrenCollection _children;
+
+        public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext)
+        {
+            _virtualizingContext = virtualizingContext;
+        }
+
+        protected override object LayoutStateCore
+        {
+            get => _virtualizingContext.LayoutState;
+            set => _virtualizingContext.LayoutState = value;
+        }
+
+        protected override IReadOnlyList<ILayoutable> ChildrenCore =>
+            _children ?? (_children = new ChildrenCollection(_virtualizingContext));
+
+        private class ChildrenCollection : IReadOnlyList<ILayoutable>
+        {
+            private readonly VirtualizingLayoutContext _context;
+            public ChildrenCollection(VirtualizingLayoutContext context) => _context = context;
+            public ILayoutable this[int index] => _context.GetOrCreateElementAt(index);
+            public int Count => _context.ItemCount;
+            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+            public IEnumerator<ILayoutable> GetEnumerator()
+            {
+                for (var i = 0; i < Count; ++i)
+                {
+                    yield return this[i];
+                }
+            }
+        }
+    }
+}

+ 8 - 28
src/Avalonia.Layout/VirtualizingLayout.cs

@@ -19,30 +19,6 @@ namespace Avalonia.Layout
     /// </remarks>
     public abstract class VirtualizingLayout : AttachedLayout
     {
-        /// <inheritdoc/>
-        public sealed override void InitializeForContext(LayoutContext context)
-        {
-            InitializeForContextCore((VirtualizingLayoutContext)context);
-        }
-
-        /// <inheritdoc/>
-        public sealed override void UninitializeForContext(LayoutContext context)
-        {
-            UninitializeForContextCore((VirtualizingLayoutContext)context);
-        }
-
-        /// <inheritdoc/>
-        public sealed override Size Measure(LayoutContext context, Size availableSize)
-        {
-            return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
-        }
-
-        /// <inheritdoc/>
-        public sealed override Size Arrange(LayoutContext context, Size finalSize)
-        {
-            return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
-        }
-
         /// <summary>
         /// Notifies the layout when the data collection assigned to the container element (Items)
         /// has changed.
@@ -70,7 +46,7 @@ namespace Avalonia.Layout
         /// The context object that facilitates communication between the layout and its host
         /// container.
         /// </param>
-        protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
+        protected internal virtual void InitializeForContextCore(VirtualizingLayoutContext context)
         {
         }
 
@@ -82,7 +58,7 @@ namespace Avalonia.Layout
         /// The context object that facilitates communication between the layout and its host
         /// container.
         /// </param>
-        protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
+        protected internal virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
         {
         }
 
@@ -104,7 +80,9 @@ namespace Avalonia.Layout
         /// of the allocated sizes for child objects or based on other considerations such as a
         /// fixed container size.
         /// </returns>
-        protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+        protected internal abstract Size MeasureOverride(
+            VirtualizingLayoutContext context,
+            Size availableSize);
 
         /// <summary>
         /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -119,7 +97,9 @@ namespace Avalonia.Layout
         /// its children.
         /// </param>
         /// <returns>The actual size that is used after the element is arranged in layout.</returns>
-        protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+        protected internal virtual Size ArrangeOverride(
+            VirtualizingLayoutContext context,
+            Size finalSize) => finalSize;
 
         /// <summary>
         /// Notifies the layout when the data collection assigned to the container element (Items)

+ 5 - 0
src/Avalonia.Layout/VirtualizingLayoutContext.cs

@@ -43,6 +43,8 @@ namespace Avalonia.Layout
     /// </summary>
     public abstract class VirtualizingLayoutContext : LayoutContext
     {
+        private NonVirtualizingLayoutContext _contextAdapter;
+
         /// <summary>
         /// Gets the number of items in the data.
         /// </summary>
@@ -186,5 +188,8 @@ namespace Avalonia.Layout
         /// </summary>
         /// <param name="element">The element to clear.</param>
         protected abstract void RecycleElementCore(ILayoutable element);
+
+        internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() =>
+            _contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this));
     }
 }

+ 13 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     public class Scene : IDisposable
     {
-        private Dictionary<IVisual, IVisualNode> _index;
+        private readonly Dictionary<IVisual, IVisualNode> _index;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Scene"/> class.
@@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <returns>The cloned scene.</returns>
         public Scene CloneScene()
         {
-            var index = new Dictionary<IVisual, IVisualNode>();
+            var index = new Dictionary<IVisual, IVisualNode>(_index.Count);
             var root = Clone((VisualNode)Root, null, index);
 
             var result = new Scene(root, index, Layers.Clone(), Generation + 1)
@@ -162,9 +162,18 @@ namespace Avalonia.Rendering.SceneGraph
 
             index.Add(result.Visual, result);
 
-            foreach (var child in source.Children)
+            int childCount = source.Children.Count;
+
+            if (childCount > 0)
             {
-                result.AddChild(Clone((VisualNode)child, result, index));
+                Span<IVisualNode> children = result.AddChildrenSpan(childCount);
+
+                for (var i = 0; i < childCount; i++)
+                {
+                    var child = source.Children[i];
+
+                    children[i] = Clone((VisualNode)child, result, index);
+                }
             }
 
             return result;

+ 16 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs

@@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph
     public class SceneLayers : IEnumerable<SceneLayer>
     {
         private readonly IVisual _root;
-        private readonly List<SceneLayer> _inner = new List<SceneLayer>();
-        private readonly Dictionary<IVisual, SceneLayer> _index = new Dictionary<IVisual, SceneLayer>();
+        private readonly List<SceneLayer> _inner;
+        private readonly Dictionary<IVisual, SceneLayer> _index;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SceneLayers"/> class.
         /// </summary>
         /// <param name="root">The scene's root visual.</param>
-        public SceneLayers(IVisual root)
+        public SceneLayers(IVisual root) : this(root, 0)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SceneLayers"/> class.
+        /// </summary>
+        /// <param name="root">The scene's root visual.</param>
+        /// <param name="capacity">Initial layer capacity.</param>
+        public SceneLayers(IVisual root, int capacity)
         {
             _root = root;
+
+            _inner = new List<SceneLayer>(capacity);
+            _index = new Dictionary<IVisual, SceneLayer>(capacity);
         }
 
         /// <summary>
@@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <returns>The cloned layers.</returns>
         public SceneLayers Clone()
         {
-            var result = new SceneLayers(_root);
+            var result = new SceneLayers(_root, Count);
 
             foreach (var src in _inner)
             {

+ 34 - 9
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Reactive.Disposables;
+using Avalonia.Collections.Pooled;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Utilities;
@@ -19,8 +20,8 @@ namespace Avalonia.Rendering.SceneGraph
 
         private Rect? _bounds;
         private double _opacity;
-        private List<IVisualNode> _children;
-        private List<IRef<IDrawOperation>> _drawOperations;
+        private PooledList<IVisualNode> _children;
+        private PooledList<IRef<IDrawOperation>> _drawOperations;
         private IRef<IDisposable> _drawOperationsRefCounter;
         private bool _drawOperationsCloned;
         private Matrix transformRestore;
@@ -349,6 +350,18 @@ namespace Avalonia.Rendering.SceneGraph
             context.Transform = transformRestore;
         }
 
+        /// <summary>
+        /// Inserts default constructed children into collection and returns a span for the newly created range.
+        /// </summary>
+        /// <param name="count">Count of children that will be added.</param>
+        /// <returns></returns>
+        internal Span<IVisualNode> AddChildrenSpan(int count)
+        {
+            EnsureChildrenCreated(count);
+
+            return _children.AddSpan(count);
+        }
+
         private Rect CalculateBounds()
         {
             var result = new Rect();
@@ -362,11 +375,11 @@ namespace Avalonia.Rendering.SceneGraph
             return result;
         }
 
-        private void EnsureChildrenCreated()
+        private void EnsureChildrenCreated(int capacity = 0)
         {
             if (_children == null)
             {
-                _children = new List<IVisualNode>();
+                _children = new PooledList<IVisualNode>(capacity);
             }
         }
 
@@ -377,13 +390,21 @@ namespace Avalonia.Rendering.SceneGraph
         {
             if (_drawOperations == null)
             {
-                _drawOperations = new List<IRef<IDrawOperation>>();
+                _drawOperations = new PooledList<IRef<IDrawOperation>>();
                 _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
                 _drawOperationsCloned = false;
             }
             else if (_drawOperationsCloned)
             {
-                _drawOperations = new List<IRef<IDrawOperation>>(_drawOperations.Select(op => op.Clone()));
+                var oldDrawOperations = _drawOperations;
+
+                _drawOperations = new PooledList<IRef<IDrawOperation>>(oldDrawOperations.Count);
+
+                foreach (var drawOperation in oldDrawOperations)
+                {
+                    _drawOperations.Add(drawOperation.Clone());
+                }
+
                 _drawOperationsRefCounter.Dispose();
                 _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
                 _drawOperationsCloned = false;
@@ -397,14 +418,16 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         /// <param name="drawOperations">Draw operations that need to be disposed.</param>
         /// <returns>Disposable for given draw operations.</returns>
-        private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> drawOperations)
+        private static IDisposable CreateDisposeDrawOperations(PooledList<IRef<IDrawOperation>> drawOperations)
         {
-            return Disposable.Create(() =>
+            return Disposable.Create(drawOperations, operations =>
             {
-                foreach (var operation in drawOperations)
+                foreach (var operation in operations)
                 {
                     operation.Dispose();
                 }
+
+                operations.Dispose();
             });
         }
 
@@ -414,6 +437,8 @@ namespace Avalonia.Rendering.SceneGraph
         {
             _drawOperationsRefCounter?.Dispose();
 
+            _children?.Dispose();
+
             Disposed = true;
         }
     }

+ 21 - 1
src/Avalonia.X11/X11Window.cs

@@ -649,7 +649,27 @@ namespace Avalonia.X11
             ScheduleInput(args);
         }
 
-        public void ScheduleInput(RawInputEventArgs args)
+        public void ScheduleXI2Input(RawInputEventArgs args)
+        {
+            if (args is RawPointerEventArgs pargs)
+            {
+                if ((pargs.Type == RawPointerEventType.TouchBegin
+                     || pargs.Type == RawPointerEventType.TouchUpdate
+                     || pargs.Type == RawPointerEventType.LeftButtonDown
+                     || pargs.Type == RawPointerEventType.RightButtonDown
+                     || pargs.Type == RawPointerEventType.MiddleButtonDown
+                     || pargs.Type == RawPointerEventType.NonClientLeftButtonDown)
+                    && ActivateTransientChildIfNeeded())
+                    return;
+                if (pargs.Type == RawPointerEventType.TouchEnd
+                    && ActivateTransientChildIfNeeded())
+                    pargs.Type = RawPointerEventType.TouchCancel;
+            }
+
+            ScheduleInput(args);
+        }
+        
+        private void ScheduleInput(RawInputEventArgs args)
         {
             if (args is RawPointerEventArgs mouse)
                 mouse.Position = mouse.Position / Scaling;

+ 5 - 5
src/Avalonia.X11/XI2Manager.cs

@@ -196,7 +196,7 @@ namespace Avalonia.X11
                     (ev.Type == XiEventType.XI_TouchUpdate ?
                         RawPointerEventType.TouchUpdate :
                         RawPointerEventType.TouchEnd);
-                client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
+                client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice,
                     ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
                 return;
             }
@@ -230,10 +230,10 @@ namespace Avalonia.X11
                 }
 
                 if (scrollDelta != default)
-                    client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
+                    client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
                         client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
                 if (_pointerDevice.HasMotion(ev))
-                    client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
+                    client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
                         RawPointerEventType.Move, ev.Position, ev.Modifiers));
             }
 
@@ -250,7 +250,7 @@ namespace Avalonia.X11
                     _ => (RawPointerEventType?)null
                 };
                 if (type.HasValue)
-                    client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
+                    client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
                         type.Value, ev.Position, ev.Modifiers));
             }
             
@@ -313,7 +313,7 @@ namespace Avalonia.X11
     interface IXI2Client
     {
         IInputRoot InputRoot { get; }
-        void ScheduleInput(RawInputEventArgs args);
+        void ScheduleXI2Input(RawInputEventArgs args);
         IMouseDevice MouseDevice { get; }
         TouchDevice TouchDevice { get; }
     }

+ 6 - 14
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -140,25 +140,17 @@ namespace Avalonia.Skia
 
         public Rect HitTestTextPosition(int index)
         {
+            if (string.IsNullOrEmpty(Text))
+            {
+                var alignmentOffset = TransformX(0, 0, _paint.TextAlign);
+                return new Rect(alignmentOffset, 0, 0, _lineHeight);
+            }
             var rects = GetRects();
-
-            if (index < 0 || index >= rects.Count)
+            if (index >= Text.Length || index < 0)
             {
                 var r = rects.LastOrDefault();
                 return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
             }
-
-            if (rects.Count == 0)
-            {
-                return new Rect(0, 0, 1, _lineHeight);
-            }
-
-            if (index == rects.Count)
-            {
-                var lr = rects[rects.Count - 1];
-                return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size);
-            }
-
             return rects[index];
         }
 

+ 4 - 3
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@@ -209,16 +209,17 @@ namespace Avalonia.Controls.UnitTests
             screenImpl.Setup(x => x.ScreenCount).Returns(1);
             screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(1, screen, screen, true) });
 
-            popupImpl = MockWindowingPlatform.CreatePopupMock();
+            var windowImpl = MockWindowingPlatform.CreateWindowMock();
+            popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object);
             popupImpl.SetupGet(x => x.Scaling).Returns(1);
+            windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object);
 
-            var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object);
             windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
 
             var services = TestServices.StyledWindow.With(
                                         inputManager: new InputManager(),
                                         windowImpl: windowImpl.Object,
-                                        windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, () => popupImpl.Object));
+                                        windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, x => popupImpl.Object));
 
             return UnitTestApplication.Start(services);
         }

+ 153 - 2
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@@ -2,11 +2,14 @@ using System;
 using System.Linq;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Controls.Templates;
 using Avalonia.LogicalTree;
+using Avalonia.Platform;
 using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests.Primitives
@@ -172,9 +175,146 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
 
-        private PopupRoot CreateTarget(TopLevel popupParent)
+        [Fact]
+        public void Child_Should_Be_Measured_With_Infinity()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var window = new Window();
+                var target = CreateTarget(window);
+                
+                target.Content = child;
+                target.Show();
+
+                Assert.Equal(Size.Infinity, child.MeasureSize);
+            }
+        }
+
+        [Fact]
+        public void Child_Should_Be_Measured_With_Width_Height_When_Set()
         {
-            var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup())
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var window = new Window();
+                var target = CreateTarget(window);
+
+                target.Width = 500;
+                target.Height = 600;
+                target.Content = child;
+                target.Show();
+
+                Assert.Equal(new Size(500, 600), child.MeasureSize);
+            }
+        }
+
+        [Fact]
+        public void Child_Should_Be_Measured_With_MaxWidth_MaxHeight_When_Set()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var window = new Window();
+                var target = CreateTarget(window);
+
+                target.MaxWidth = 500;
+                target.MaxHeight = 600;
+                target.Content = child;
+                target.Show();
+
+                Assert.Equal(new Size(500, 600), child.MeasureSize);
+            }
+        }
+
+        [Fact]
+        public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
+        {
+            // Issue #3784.
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
+
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 1344,
+                };
+
+                var target = CreateTarget(window, popupImpl.Object);
+                target.Content = child;
+
+                target.Show();
+
+                Assert.Equal(new Size(400, 1024), target.Bounds.Size);
+
+                // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
+                // parent control to be offset against.
+                Assert.Equal(new Point(0, 0), target.Bounds.Position);
+            }
+        }
+
+        [Fact]
+        public void MinWidth_MinHeight_Should_Be_Respected()
+        {
+            // Issue #3796
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
+
+                var target = CreateTarget(window, popupImpl.Object);
+                target.MinWidth = 400;
+                target.MinHeight = 800;
+                target.Content = new Border
+                {
+                    Width = 100,
+                    Height = 100,
+                };
+
+                target.Show();
+
+                Assert.Equal(new Rect(0, 0, 400, 800), target.Bounds);
+                Assert.Equal(new Size(400, 800), target.ClientSize);
+                Assert.Equal(new Size(400, 800), target.PlatformImpl.ClientSize);
+            }
+        }
+
+        [Fact]
+        public void Setting_Width_Should_Resize_WindowImpl()
+        {
+            // Issue #3796
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
+                var positioner = new Mock<IPopupPositioner>();
+                popupImpl.Setup(x => x.PopupPositioner).Returns(positioner.Object);
+
+                var target = CreateTarget(window, popupImpl.Object);
+                target.Width = 400;
+                target.Height = 800;
+
+                target.Show();
+
+                Assert.Equal(400, target.Width);
+                Assert.Equal(800, target.Height);
+
+                target.Width = 410;
+                target.LayoutManager.ExecuteLayoutPass();
+
+                positioner.Verify(x => 
+                    x.Update(It.Is<PopupPositionerParameters>(x => x.Size.Width == 410)));
+                Assert.Equal(410, target.Width);
+            }
+        }
+
+        private PopupRoot CreateTarget(TopLevel popupParent, IPopupImpl impl = null)
+        {
+            impl ??= popupParent.PlatformImpl.CreatePopup();
+
+            var result = new PopupRoot(popupParent, impl)
             {
                 Template = new FuncControlTemplate<PopupRoot>((parent, scope) =>
                     new ContentPresenter
@@ -217,5 +357,16 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Popup = (Popup)this.GetVisualChildren().Single();
             }
         }
+
+        private class ChildControl : Control
+        {
+            public Size MeasureSize { get; private set; }
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                MeasureSize = availableSize;
+                return base.MeasureOverride(availableSize);
+            }
+        }
     }
 }

+ 72 - 9
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -298,13 +298,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
 
-        Window PreparedWindow(object content = null)
-        {
-            var w = new Window {Content = content};
-            w.ApplyTemplate();
-            return w;
-        }
-
         [Fact]
         public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit()
         {
@@ -351,18 +344,88 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
 
+        [Fact]
+        public void StaysOpen_False_Should_Not_Handle_Closing_Click()
+        {
+            using (CreateServices())
+            {
+                var window = PreparedWindow();
+                var target = new Popup() 
+                { 
+                    PlacementTarget = window ,
+                    StaysOpen = false,
+                };
+
+                target.Open();
+
+                var e = CreatePointerPressedEventArgs(window);
+                window.RaiseEvent(e);
+
+                Assert.False(e.Handled);
+            }
+        }
+
+        [Fact]
+        public void Should_Pass_Closing_Click_To_Closed_Event()
+        {
+            using (CreateServices())
+            {
+                var window = PreparedWindow();
+                var target = new Popup()
+                {
+                    PlacementTarget = window,
+                    StaysOpen = false,
+                };
+
+                target.Open();
+
+                var press = CreatePointerPressedEventArgs(window);
+                var raised = 0;
+
+                target.Closed += (s, e) =>
+                {
+                    Assert.Same(press, e.CloseEvent);
+                    ++raised;
+                };
+
+                window.RaiseEvent(press);
+
+                Assert.Equal(1, raised);
+            }
+        }
+
         private IDisposable CreateServices()
         {
             return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
                 new MockWindowingPlatform(null,
-                    () =>
+                    x =>
                     {
                         if(UsePopupHost)
                             return null;
-                        return MockWindowingPlatform.CreatePopupMock().Object;
+                        return MockWindowingPlatform.CreatePopupMock(x).Object;
                     })));
         }
 
+        private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source)
+        {
+            var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
+            return new PointerPressedEventArgs(
+                source,
+                pointer,
+                source,
+                default,
+                0,
+                new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
+                KeyModifiers.None);
+        }
+
+        private Window PreparedWindow(object content = null)
+        {
+            var w = new Window { Content = content };
+            w.ApplyTemplate();
+            return w;
+        }
+
         private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope)
         {
             return new Popup

+ 72 - 1
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@@ -4,7 +4,6 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Layout;
-using Avalonia.LogicalTree;
 using Moq;
 using Xunit;
 
@@ -147,6 +146,78 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(new Size(45, 67), target.LargeChange);
         }
 
+        [Fact]
+        public void Changing_Extent_Should_Raise_ScrollChanged()
+        {
+            var target = new ScrollViewer();
+            var raised = 0;
+
+            target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
+            target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
+            target.Offset = new Vector(10, 10);
+
+            target.ScrollChanged += (s, e) =>
+            {
+                Assert.Equal(new Vector(11, 12), e.ExtentDelta);
+                Assert.Equal(default, e.OffsetDelta);
+                Assert.Equal(default, e.ViewportDelta);
+                ++raised;
+            };
+
+            target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112));
+
+            Assert.Equal(1, raised);
+
+        }
+
+        [Fact]
+        public void Changing_Offset_Should_Raise_ScrollChanged()
+        {
+            var target = new ScrollViewer();
+            var raised = 0;
+
+            target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
+            target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
+            target.Offset = new Vector(10, 10);
+
+            target.ScrollChanged += (s, e) =>
+            {
+                Assert.Equal(default, e.ExtentDelta);
+                Assert.Equal(new Vector(12, 14), e.OffsetDelta);
+                Assert.Equal(default, e.ViewportDelta);
+                ++raised;
+            };
+
+            target.Offset = new Vector(22, 24);
+
+            Assert.Equal(1, raised);
+
+        }
+
+        [Fact]
+        public void Changing_Viewport_Should_Raise_ScrollChanged()
+        {
+            var target = new ScrollViewer();
+            var raised = 0;
+
+            target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
+            target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
+            target.Offset = new Vector(10, 10);
+
+            target.ScrollChanged += (s, e) =>
+            {
+                Assert.Equal(default, e.ExtentDelta);
+                Assert.Equal(default, e.OffsetDelta);
+                Assert.Equal(new Vector(6, 8), e.ViewportDelta);
+                ++raised;
+            };
+
+            target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58));
+
+            Assert.Equal(1, raised);
+
+        }
+
         private Control CreateTemplate(ScrollViewer control, INameScope scope)
         {
             return new Grid

+ 18 - 0
tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs

@@ -1,4 +1,6 @@
 using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.UnitTests;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests.Shapes
@@ -12,5 +14,21 @@ namespace Avalonia.Controls.UnitTests.Shapes
 
             target.Measure(Size.Infinity);
         }
+
+        [Fact]
+        public void Subscribes_To_Geometry_Changes()
+        {
+            using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+            var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
+            var target = new Path { Data = geometry };
+
+            target.Measure(Size.Infinity);
+            Assert.True(target.IsMeasureValid);
+
+            geometry.Rect = new Rect(0, 0, 20, 20);
+
+            Assert.False(target.IsMeasureValid);
+        }
     }
 }

+ 39 - 0
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -1,10 +1,12 @@
 using System;
 using System.Reactive.Linq;
+using System.Threading.Tasks;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
@@ -554,6 +556,34 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Theory]
+        [InlineData(Key.X, KeyModifiers.Control)]
+        [InlineData(Key.Back, KeyModifiers.None)]
+        [InlineData(Key.Delete, KeyModifiers.None)]
+        [InlineData(Key.Tab, KeyModifiers.None)]
+        [InlineData(Key.Enter, KeyModifiers.None)]
+        public void Keys_Allow_Undo(Key key, KeyModifiers modifiers)
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                var target = new TextBox
+                {
+                    Template = CreateTemplate(),
+                    Text = "0123",
+                    AcceptsReturn = true,
+                    AcceptsTab = true
+                };
+                target.SelectionStart = 1;
+                target.SelectionEnd = 3;
+                AvaloniaLocator.CurrentMutable
+                    .Bind<Input.Platform.IClipboard>().ToSingleton<ClipboardStub>();
+
+                RaiseKeyEvent(target, key, modifiers);
+                RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
+                Assert.True(target.Text == "0123");
+            }
+        }
+
         private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
             focusManager: new FocusManager(),
             keyboardDevice: () => new KeyboardDevice(),
@@ -616,5 +646,14 @@ namespace Avalonia.Controls.UnitTests
                 set { _bar = value; RaisePropertyChanged(); }
             }
         }
+
+        private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
+        {
+            public Task<string> GetTextAsync() => Task.FromResult("");
+
+            public Task SetTextAsync(string text) => Task.CompletedTask;
+
+            public Task ClearAsync() => Task.CompletedTask;
+        }
     }
 }

+ 165 - 0
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
+using Avalonia.Layout;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.UnitTests;
@@ -355,6 +356,27 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var windowImpl = MockWindowingPlatform.CreateWindowMock();
+                windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
+
+                var child = new ChildControl();
+                var target = new Window(windowImpl.Object)
+                {
+                    SizeToContent = SizeToContent.Manual,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(new Size(550, 450), child.MeasureSize);
+            }
+        }
+
         [Fact]
         public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
         {
@@ -375,6 +397,149 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
+        {
+            // Issue #3784.
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var windowImpl = MockWindowingPlatform.CreateWindowMock();
+                var clientSize = new Size(200, 200);
+                var maxClientSize = new Size(480, 480);
+
+                windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(size =>
+                {
+                    clientSize = size.Constrain(maxClientSize);
+                    windowImpl.Object.Resized?.Invoke(clientSize);
+                });
+
+                windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+                var target = new Window(windowImpl.Object)
+                {
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(new Size(400, 480), target.Bounds.Size);
+
+                // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
+                // parent control to be offset against.
+                Assert.Equal(new Point(0, 0), target.Bounds.Position);
+            }
+        }
+
+        [Fact]
+        public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+
+                var target = new Window()
+                {
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(400, target.Width);
+                Assert.Equal(800, target.Height);
+            }
+        }
+
+        [Fact]
+        public void SizeToContent_Should_Not_Be_Lost_On_Show()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+
+                var target = new Window()
+                {
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+            }
+        }
+
+        [Fact]
+        public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+
+                var target = new Window()
+                {
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(400, target.Width);
+                Assert.Equal(800, target.Height);
+
+                child.Width = 410;
+                target.LayoutManager.ExecuteLayoutPass();
+
+                Assert.Equal(410, target.Width);
+                Assert.Equal(800, target.Height);
+                Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+            }
+        }
+
+        [Fact]
+        public void Setting_Width_Should_Resize_WindowImpl()
+        {
+            // Issue #3796
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var target = new Window()
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+
+                target.Show();
+
+                Assert.Equal(400, target.Width);
+                Assert.Equal(800, target.Height);
+
+                target.Width = 410;
+                target.LayoutManager.ExecuteLayoutPass();
+
+                var windowImpl = Mock.Get(target.PlatformImpl);
+                windowImpl.Verify(x => x.Resize(new Size(410, 800)));
+                Assert.Equal(410, target.Width);
+            }
+        }
+
         private IWindowImpl CreateImpl(Mock<IRenderer> renderer)
         {
             return Mock.Of<IWindowImpl>(x =>

+ 3 - 105
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@@ -1,25 +1,12 @@
-using System.Diagnostics;
-using System.IO;
 using System.Linq;
-using Moq;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
-using Avalonia.Diagnostics;
-using Avalonia.Input;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using Avalonia.Shared.PlatformSupport;
 using Avalonia.Styling;
-using Avalonia.Themes.Default;
+using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
-using Avalonia.Media;
-using System;
-using System.Collections.Generic;
-using Avalonia.Controls.UnitTests;
-using Avalonia.UnitTests;
 
 namespace Avalonia.Layout.UnitTests
 {
@@ -28,10 +15,8 @@ namespace Avalonia.Layout.UnitTests
         [Fact]
         public void Grandchild_Size_Changed()
         {
-            using (var context = AvaloniaLocator.EnterScope())
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                RegisterServices();
-
                 Border border;
                 TextBlock textBlock;
 
@@ -55,7 +40,6 @@ namespace Avalonia.Layout.UnitTests
                 };
 
                 window.Show();
-                window.LayoutManager.ExecuteInitialLayoutPass(window);
 
                 Assert.Equal(new Size(400, 400), border.Bounds.Size);
                 textBlock.Width = 200;
@@ -68,10 +52,8 @@ namespace Avalonia.Layout.UnitTests
         [Fact]
         public void Test_ScrollViewer_With_TextBlock()
         {
-            using (var context = AvaloniaLocator.EnterScope())
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                RegisterServices();
-
                 ScrollViewer scrollViewer;
                 TextBlock textBlock;
 
@@ -79,7 +61,6 @@ namespace Avalonia.Layout.UnitTests
                 {
                     Width = 800,
                     Height = 600,
-                    SizeToContent = SizeToContent.WidthAndHeight,
                     Content = scrollViewer = new ScrollViewer
                     {
                         Width = 200,
@@ -99,7 +80,6 @@ namespace Avalonia.Layout.UnitTests
                 window.Resources["ScrollBarThickness"] = 10.0;
 
                 window.Show();
-                window.LayoutManager.ExecuteInitialLayoutPass(window);
 
                 Assert.Equal(new Size(800, 600), window.Bounds.Size);
                 Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size);
@@ -131,87 +111,5 @@ namespace Avalonia.Layout.UnitTests
         {
             return v.Bounds.Position;
         }
-
-        class FormattedTextMock : IFormattedTextImpl
-        {
-            public FormattedTextMock(string text)
-            {
-                Text = text;
-            }
-
-            public Size Constraint { get; set; }
-
-            public string Text { get; }
-
-            public Rect Bounds => Rect.Empty;
-
-            public void Dispose()
-            {
-            }
-
-            public IEnumerable<FormattedTextLine> GetLines() => new FormattedTextLine[0];
-
-            public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult();
-
-            public Rect HitTestTextPosition(int index) => new Rect();
-
-            public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
-
-            public Size Measure() => Constraint;
-        }
-
-        private void RegisterServices()
-        {
-            var globalStyles = new Mock<IGlobalStyles>();
-            var globalStylesResources = globalStyles.As<IResourceNode>();
-            var outObj = (object)10;
-            globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true);
-
-            var renderInterface = new Mock<IPlatformRenderInterface>();
-            renderInterface.Setup(x =>
-                x.CreateFormattedText(
-                    It.IsAny<string>(),
-                    It.IsAny<Typeface>(),
-                    It.IsAny<double>(),
-                    It.IsAny<TextAlignment>(),
-                    It.IsAny<TextWrapping>(),
-                    It.IsAny<Size>(),
-                    It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
-                .Returns(new FormattedTextMock("TEST"));
-
-            var streamGeometry = new Mock<IStreamGeometryImpl>();
-            streamGeometry.Setup(x =>
-                    x.Open())
-                .Returns(new Mock<IStreamGeometryContextImpl>().Object);
-
-            renderInterface.Setup(x =>
-                    x.CreateStreamGeometry())
-                .Returns(streamGeometry.Object);
-
-            var windowImpl = new Mock<IWindowImpl>();
-
-            Size clientSize = default(Size);
-
-            windowImpl.SetupGet(x => x.ClientSize).Returns(() => clientSize);
-            windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(s => clientSize = s);
-            windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1024, 1024));
-            windowImpl.SetupGet(x => x.Scaling).Returns(1);
-
-            AvaloniaLocator.CurrentMutable
-                .Bind<IStandardCursorFactory>().ToConstant(new CursorFactoryMock())
-                .Bind<IAssetLoader>().ToConstant(new AssetLoader())
-                .Bind<IInputManager>().ToConstant(new Mock<IInputManager>().Object)
-                .Bind<IGlobalStyles>().ToConstant(globalStyles.Object)
-                .Bind<IRuntimePlatform>().ToConstant(new AppBuilder().RuntimePlatform)
-                .Bind<IPlatformRenderInterface>().ToConstant(renderInterface.Object)
-                .Bind<IStyler>().ToConstant(new Styler())
-                .Bind<IFontManagerImpl>().ToConstant(new MockFontManagerImpl())
-                .Bind<ITextShaperImpl>().ToConstant(new MockTextShaperImpl())
-                .Bind<IWindowingPlatform>().ToConstant(new Avalonia.Controls.UnitTests.WindowingPlatformMock(() => windowImpl.Object));
-
-            var theme = new DefaultTheme();
-            globalStyles.Setup(x => x.IsStylesInitialized).Returns(true);
-            globalStyles.Setup(x => x.Styles).Returns(theme);
-        }
     }
 }

+ 335 - 0
tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs

@@ -0,0 +1,335 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Xunit;
+
+namespace Avalonia.Layout.UnitTests
+{
+    public class NonVirtualizingStackLayoutTests
+    {
+        [Fact]
+        public void Lays_Out_Children_Vertically()
+        {
+            var target = new NonVirtualizingStackLayout { Orientation = Orientation.Vertical };
+            var context = CreateContext(new[]
+            {
+                new Border { Height = 20, Width = 120 },
+                new Border { Height = 30 },
+                new Border { Height = 50 },
+            });
+
+            var desiredSize = target.Measure(context, Size.Infinity);
+            var arrangeSize = target.Arrange(context, desiredSize);
+
+            Assert.Equal(new Size(120, 100), desiredSize);
+            Assert.Equal(new Size(120, 100), arrangeSize);
+            Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
+            Assert.Equal(new Rect(0, 20, 120, 30), context.Children[1].Bounds);
+            Assert.Equal(new Rect(0, 50, 120, 50), context.Children[2].Bounds);
+        }
+
+        [Fact]
+        public void Lays_Out_Children_Horizontally()
+        {
+            var target = new NonVirtualizingStackLayout { Orientation = Orientation.Horizontal };
+            var context = CreateContext(new[]
+            {
+                new Border { Width = 20, Height = 120 },
+                new Border { Width = 30 },
+                new Border { Width = 50 },
+            });
+
+            var desiredSize = target.Measure(context, Size.Infinity);
+            var arrangeSize = target.Arrange(context, desiredSize);
+
+            Assert.Equal(new Size(100, 120), desiredSize);
+            Assert.Equal(new Size(100, 120), arrangeSize);
+            Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
+            Assert.Equal(new Rect(20, 0, 30, 120), context.Children[1].Bounds);
+            Assert.Equal(new Rect(50, 0, 50, 120), context.Children[2].Bounds);
+        }
+
+        [Fact]
+        public void Lays_Out_Children_Vertically_With_Spacing()
+        {
+            var target = new NonVirtualizingStackLayout 
+            { 
+                Orientation = Orientation.Vertical,
+                Spacing = 10,
+            };
+
+            var context = CreateContext(new[]
+            {
+                new Border { Height = 20, Width = 120 },
+                new Border { Height = 30 },
+                new Border { Height = 50 },
+            });
+
+            var desiredSize = target.Measure(context, Size.Infinity);
+            var arrangeSize = target.Arrange(context, desiredSize);
+
+            Assert.Equal(new Size(120, 120), desiredSize);
+            Assert.Equal(new Size(120, 120), arrangeSize);
+            Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
+            Assert.Equal(new Rect(0, 30, 120, 30), context.Children[1].Bounds);
+            Assert.Equal(new Rect(0, 70, 120, 50), context.Children[2].Bounds);
+        }
+
+        [Fact]
+        public void Lays_Out_Children_Horizontally_With_Spacing()
+        {
+            var target = new NonVirtualizingStackLayout 
+            { 
+                Orientation = Orientation.Horizontal,
+                Spacing = 10,
+            };
+
+            var context = CreateContext(new[]
+            {
+                new Border { Width = 20, Height = 120 },
+                new Border { Width = 30 },
+                new Border { Width = 50 },
+            });
+
+            var desiredSize = target.Measure(context, Size.Infinity);
+            var arrangeSize = target.Arrange(context, desiredSize);
+
+            Assert.Equal(new Size(120, 120), desiredSize);
+            Assert.Equal(new Size(120, 120), arrangeSize);
+            Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
+            Assert.Equal(new Rect(30, 0, 30, 120), context.Children[1].Bounds);
+            Assert.Equal(new Rect(70, 0, 50, 120), context.Children[2].Bounds);
+        }
+
+        [Fact]
+        public void Arranges_Vertical_Children_With_Correct_Bounds()
+        {
+            var target = new NonVirtualizingStackLayout
+            {
+                Orientation = Orientation.Vertical
+            };
+            
+            var context = CreateContext(new[]
+            {
+                new TestControl
+                {
+                    HorizontalAlignment = HorizontalAlignment.Left,
+                    MeasureSize = new Size(50, 10),
+                },
+                new TestControl
+                {
+                    HorizontalAlignment = HorizontalAlignment.Left,
+                    MeasureSize = new Size(150, 10),
+                },
+                new TestControl
+                {
+                    HorizontalAlignment = HorizontalAlignment.Center,
+                    MeasureSize = new Size(50, 10),
+                },
+                new TestControl
+                {
+                    HorizontalAlignment = HorizontalAlignment.Center,
+                    MeasureSize = new Size(150, 10),
+                },
+                new TestControl
+                {
+                    HorizontalAlignment = HorizontalAlignment.Right,
+                    MeasureSize = new Size(50, 10),
+                },
+                new TestControl
+                {
+                    HorizontalAlignment = HorizontalAlignment.Right,
+                    MeasureSize = new Size(150, 10),
+                },
+                new TestControl
+                {
+                    HorizontalAlignment = HorizontalAlignment.Stretch,
+                    MeasureSize = new Size(50, 10),
+                },
+                new TestControl
+                {
+                    HorizontalAlignment = HorizontalAlignment.Stretch,
+                    MeasureSize = new Size(150, 10),
+                },
+            });
+
+            var desiredSize = target.Measure(context, new Size(100, 150));
+            Assert.Equal(new Size(100, 80), desiredSize);
+
+            target.Arrange(context, desiredSize);
+
+            var bounds = context.Children.Select(x => x.Bounds).ToArray();
+
+            Assert.Equal(
+                new[]
+                {
+                    new Rect(0, 0, 50, 10),
+                    new Rect(0, 10, 100, 10),
+                    new Rect(25, 20, 50, 10),
+                    new Rect(0, 30, 100, 10),
+                    new Rect(50, 40, 50, 10),
+                    new Rect(0, 50, 100, 10),
+                    new Rect(0, 60, 100, 10),
+                    new Rect(0, 70, 100, 10),
+
+                }, bounds);
+        }
+
+        [Fact]
+        public void Arranges_Horizontal_Children_With_Correct_Bounds()
+        {
+            var target = new NonVirtualizingStackLayout
+            {
+                Orientation = Orientation.Horizontal
+            };
+
+            var context = CreateContext(new[]
+            {
+                new TestControl
+                {
+                    VerticalAlignment = VerticalAlignment.Top,
+                    MeasureSize = new Size(10, 50),
+                },
+                new TestControl
+                {
+                    VerticalAlignment = VerticalAlignment.Top,
+                    MeasureSize = new Size(10, 150),
+                },
+                new TestControl
+                {
+                    VerticalAlignment = VerticalAlignment.Center,
+                    MeasureSize = new Size(10, 50),
+                },
+                new TestControl
+                {
+                    VerticalAlignment = VerticalAlignment.Center,
+                    MeasureSize = new Size(10, 150),
+                },
+                new TestControl
+                {
+                    VerticalAlignment = VerticalAlignment.Bottom,
+                    MeasureSize = new Size(10, 50),
+                },
+                new TestControl
+                {
+                    VerticalAlignment = VerticalAlignment.Bottom,
+                    MeasureSize = new Size(10, 150),
+                },
+                new TestControl
+                {
+                    VerticalAlignment = VerticalAlignment.Stretch,
+                    MeasureSize = new Size(10, 50),
+                },
+                new TestControl
+                {
+                    VerticalAlignment = VerticalAlignment.Stretch,
+                    MeasureSize = new Size(10, 150),
+                },
+            });
+
+            var desiredSize = target.Measure(context, new Size(150, 100));
+            Assert.Equal(new Size(80, 100), desiredSize);
+
+            target.Arrange(context, desiredSize);
+
+            var bounds = context.Children.Select(x => x.Bounds).ToArray();
+
+            Assert.Equal(
+                new[]
+                {
+                    new Rect(0, 0, 10, 50),
+                    new Rect(10, 0, 10, 100),
+                    new Rect(20, 25, 10, 50),
+                    new Rect(30, 0, 10, 100),
+                    new Rect(40, 50, 10, 50),
+                    new Rect(50, 0, 10, 100),
+                    new Rect(60, 0, 10, 100),
+                    new Rect(70, 0, 10, 100),
+                }, bounds);
+        }
+
+        [Theory]
+        [InlineData(Orientation.Horizontal)]
+        [InlineData(Orientation.Vertical)]
+        public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation)
+        {
+            var targetThreeChildrenOneInvisble = new NonVirtualizingStackLayout
+            {
+                Orientation = orientation,
+                Spacing = 40,
+            };
+
+            var contextThreeChildrenOneInvisble = CreateContext(new[]
+            {
+                new StackPanel { Width = 10, Height= 10, IsVisible = false },
+                new StackPanel { Width = 10, Height= 10 },
+                new StackPanel { Width = 10, Height= 10 },
+            });
+
+            var targetTwoChildrenNoneInvisible = new NonVirtualizingStackLayout
+            {
+                Spacing = 40,
+                Orientation = orientation,
+            };
+
+            var contextTwoChildrenNoneInvisible = CreateContext(new[]
+            {
+                new StackPanel { Width = 10, Height = 10 },
+                new StackPanel { Width = 10, Height = 10 }
+            });
+
+            var desiredSize1 = targetThreeChildrenOneInvisble.Measure(contextThreeChildrenOneInvisble, Size.Infinity);
+            var desiredSize2 = targetTwoChildrenNoneInvisible.Measure(contextTwoChildrenNoneInvisible, Size.Infinity);
+ 
+            Assert.Equal(desiredSize2, desiredSize1);
+        }
+
+        [Theory]
+        [InlineData(Orientation.Horizontal)]
+        [InlineData(Orientation.Vertical)]
+        public void Only_Arrange_Visible_Children(Orientation orientation)
+        {
+            var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false };
+            var panel = new Panel { Width = 10, Height = 10 };
+
+            var target = new NonVirtualizingStackLayout
+            {
+                Spacing = 40,
+                Orientation = orientation,
+            };
+
+            var context = CreateContext(new[]
+            {
+                hiddenPanel,
+                panel
+            });
+
+            var desiredSize = target.Measure(context, Size.Infinity);
+            var arrangeSize = target.Arrange(context, desiredSize);
+            Assert.Equal(new Size(10, 10), arrangeSize);
+        }
+
+        private NonVirtualizingLayoutContext CreateContext(Control[] children)
+        {
+            return new TestLayoutContext(children);
+        }
+
+        private class TestLayoutContext : NonVirtualizingLayoutContext
+        {
+            public TestLayoutContext(Control[] children) => ChildrenCore = children;
+            protected override IReadOnlyList<ILayoutable> ChildrenCore { get; }
+        }
+
+        private class TestControl : Control
+        {
+            public Size MeasureConstraint { get; private set; }
+            public Size MeasureSize { get; set; }
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                MeasureConstraint = availableSize;
+                return MeasureSize;
+            }
+        }
+    }
+}

+ 86 - 29
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@@ -8,65 +8,122 @@ namespace Avalonia.UnitTests
 {
     public class MockWindowingPlatform : IWindowingPlatform
     {
+        private static readonly Size s_screenSize = new Size(1280, 1024);
         private readonly Func<IWindowImpl> _windowImpl;
-        private readonly Func<IPopupImpl> _popupImpl;
+        private readonly Func<IWindowBaseImpl, IPopupImpl> _popupImpl;
 
-        public MockWindowingPlatform(Func<IWindowImpl> windowImpl = null, Func<IPopupImpl> popupImpl = null )
+        public MockWindowingPlatform(
+            Func<IWindowImpl> windowImpl = null,
+            Func<IWindowBaseImpl, IPopupImpl> popupImpl = null )
         {
             _windowImpl = windowImpl;
             _popupImpl = popupImpl;
         }
 
-        public static Mock<IWindowImpl> CreateWindowMock(Func<IPopupImpl> popupImpl = null)
+        public static Mock<IWindowImpl> CreateWindowMock()
         {
-            var win = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
-            var mock = Mock.Get(win);
-            mock.Setup(x => x.Show()).Callback(() =>
+            var windowImpl = new Mock<IWindowImpl>();
+            var position = new PixelPoint();
+            var clientSize = new Size(800, 600);
+
+            windowImpl.SetupAllProperties();
+            windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+            windowImpl.Setup(x => x.Scaling).Returns(1);
+            windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object);
+            windowImpl.Setup(x => x.Position).Returns(() => position);
+            SetupToplevel(windowImpl);
+
+            windowImpl.Setup(x => x.CreatePopup()).Returns(() =>
             {
-                mock.Object.Activated?.Invoke();
+                return CreatePopupMock(windowImpl.Object).Object;
             });
-            mock.Setup(x => x.CreatePopup()).Returns(() =>
+
+            windowImpl.Setup(x => x.Dispose()).Callback(() =>
             {
-                if (popupImpl != null)
-                    return popupImpl();
-                return CreatePopupMock().Object;
+                windowImpl.Object.Closed?.Invoke();
+            });
+
+            windowImpl.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback<PixelPoint>(x =>
+            {
+                position = x;
+                windowImpl.Object.PositionChanged?.Invoke(x);
+            });
 
+            windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(x =>
+            {
+                clientSize = x.Constrain(s_screenSize);
+                windowImpl.Object.Resized?.Invoke(clientSize);
             });
-            mock.Setup(x => x.Dispose()).Callback(() =>
+
+            windowImpl.Setup(x => x.Show()).Callback(() =>
             {
-                mock.Object.Closed?.Invoke();
+                windowImpl.Object.Activated?.Invoke();
             });
-            PixelPoint pos = default;
-            mock.SetupGet(x => x.Position).Returns(() => pos);
-            mock.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));
-            SetupToplevel(mock);
-            return mock;
+
+            return windowImpl;
         }
 
-        static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
+        public static Mock<IPopupImpl> CreatePopupMock(IWindowBaseImpl parent)
         {
-            mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
+            var popupImpl = new Mock<IPopupImpl>();
+            var clientSize = new Size();
+
+            var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) =>
+            {
+                clientSize = size.Constrain(s_screenSize);
+                popupImpl.Object.PositionChanged?.Invoke(pos);
+                popupImpl.Object.Resized?.Invoke(clientSize);
+            });
+            
+            var positioner = new ManagedPopupPositioner(positionerHelper);
+
+            popupImpl.SetupAllProperties();
+            popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+            popupImpl.Setup(x => x.Scaling).Returns(1);
+            popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);
+            
+            SetupToplevel(popupImpl);
+            
+            return popupImpl;
         }
 
-        public static Mock<IPopupImpl> CreatePopupMock()
+        public static Mock<IScreenImpl> CreateScreenMock()
         {
-            var positioner = Mock.Of<IPopupPositioner>();
-            var popup = Mock.Of<IPopupImpl>(x => x.Scaling == 1);
-            var mock = Mock.Get(popup);
-            mock.SetupGet(x => x.PopupPositioner).Returns(positioner);
-            SetupToplevel(mock);
-            
-            return mock;
+            var screenImpl = new Mock<IScreenImpl>();
+            var bounds = new PixelRect(0, 0, (int)s_screenSize.Width, (int)s_screenSize.Height);
+            var screen = new Screen(96, bounds, bounds, true);
+            screenImpl.Setup(x => x.AllScreens).Returns(new[] { screen });
+            screenImpl.Setup(x => x.ScreenCount).Returns(1);
+            return screenImpl;
         }
 
         public IWindowImpl CreateWindow()
         {
-            return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object;
+            if (_windowImpl is object)
+            {
+                return _windowImpl();
+            }
+            else
+            {
+                var mock = CreateWindowMock();
+
+                if (_popupImpl is object)
+                {
+                    mock.Setup(x => x.CreatePopup()).Returns(() => _popupImpl(mock.Object));
+                }
+
+                return mock.Object;
+            }
         }
 
         public IEmbeddableWindowImpl CreateEmbeddableWindow()
         {
             throw new NotImplementedException();
         }
+
+        private static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
+        {
+            mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
+        }
     }
 }