Sfoglia il codice sorgente

Merge branch 'master' into fixes/macBuilds

Benedikt Stebner 6 anni fa
parent
commit
89702d1aa5
35 ha cambiato i file con 748 aggiunte e 109 eliminazioni
  1. 1 0
      src/Avalonia.Animation/Properties/AssemblyInfo.cs
  2. 1 1
      src/Avalonia.Animation/TransitionInstance.cs
  3. 23 0
      src/Avalonia.Base/EnumExtensions.cs
  4. 5 0
      src/Avalonia.Base/Utilities/MathUtilities.cs
  5. 6 6
      src/Avalonia.Base/ValueStore.cs
  6. 0 5
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  7. 9 1
      src/Avalonia.Controls/ItemsControl.cs
  8. 7 0
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  9. 13 2
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  10. 22 7
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  11. 5 1
      src/Avalonia.Controls/ToolTipService.cs
  12. 1 1
      src/Avalonia.Controls/WrapPanel.cs
  13. 11 2
      src/Avalonia.Input/MouseDevice.cs
  14. 1 1
      src/Avalonia.Input/Pointer.cs
  15. 6 3
      src/Avalonia.Input/PointerPoint.cs
  16. 18 3
      src/Avalonia.Input/TouchDevice.cs
  17. 0 1
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  18. 3 2
      src/Avalonia.Native/WindowImplBase.cs
  19. 1 1
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  20. 0 2
      src/Avalonia.X11/X11Platform.cs
  21. 8 2
      src/Avalonia.X11/X11Window.cs
  22. 6 6
      src/Avalonia.X11/XI2Manager.cs
  23. 0 8
      src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
  24. 20 19
      src/Windows/Avalonia.Win32/WindowImpl.cs
  25. 21 7
      tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
  26. 12 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
  27. 53 0
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  28. 187 2
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  29. 20 0
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  30. 109 0
      tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
  31. 53 25
      tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
  32. 39 0
      tests/Avalonia.Input.UnitTests/PointerTests.cs
  33. 1 1
      tests/Avalonia.UnitTests/MouseTestHelper.cs
  34. 11 0
      tests/Avalonia.UnitTests/TestRoot.cs
  35. 75 0
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

+ 1 - 0
src/Avalonia.Animation/Properties/AssemblyInfo.cs

@@ -10,3 +10,4 @@ using System.Runtime.CompilerServices;
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")]
 
 [assembly: InternalsVisibleTo("Avalonia.LeakTests")]
+[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")]

+ 1 - 1
src/Avalonia.Animation/TransitionInstance.cs

@@ -28,7 +28,7 @@ namespace Avalonia.Animation
 
         private void TimerTick(TimeSpan t)
         {
-            var interpVal = (double)t.Ticks / _duration.Ticks;
+            var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks;
 
             // Clamp interpolation value.
             if (interpVal >= 1d | interpVal < 0d)

+ 23 - 0
src/Avalonia.Base/EnumExtensions.cs

@@ -0,0 +1,23 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Provides extension methods for enums.
+    /// </summary>
+    public static class EnumExtensions
+    {
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static unsafe bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
+        {
+            var intValue = *(int*)&value;
+            var intFlag = *(int*)&flag;
+
+            return (intValue & intFlag) == intFlag;
+        }
+    }
+}

+ 5 - 0
src/Avalonia.Base/Utilities/MathUtilities.cs

@@ -159,6 +159,11 @@ namespace Avalonia.Utilities
         /// <returns>The clamped value.</returns>
         public static int Clamp(int val, int min, int max)
         {
+            if (min > max)
+            {
+                throw new ArgumentException($"{min} cannot be greater than {max}.");
+            }
+
             if (val < min)
             {
                 return min;

+ 6 - 6
src/Avalonia.Base/ValueStore.cs

@@ -57,7 +57,8 @@ namespace Avalonia
                 {
                     if (priority == (int)BindingPriority.LocalValue)
                     {
-                        _propertyValues.SetValue(property, Validate(property, value));
+                        Validate(property, ref value);
+                        _propertyValues.SetValue(property, value);
                         Changed(property, priority, v, value);
                         return;
                     }
@@ -78,7 +79,8 @@ namespace Avalonia
 
                 if (priority == (int)BindingPriority.LocalValue)
                 {
-                    _propertyValues.AddValue(property, Validate(property, value));
+                    Validate(property, ref value);
+                    _propertyValues.AddValue(property, value);
                     Changed(property, priority, AvaloniaProperty.UnsetValue, value);
                     return;
                 }
@@ -166,16 +168,14 @@ namespace Avalonia
                 validate2);
         }
 
-        private object Validate(AvaloniaProperty property, object value)
+        private void Validate(AvaloniaProperty property, ref object value)
         {
             var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
 
             if (validate != null && value != AvaloniaProperty.UnsetValue)
             {
-                return validate(_owner, value);
+                value = validate(_owner, value);
             }
-
-            return value;
         }
 
         private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)

+ 0 - 5
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@@ -48,11 +48,6 @@ namespace Avalonia.Controls.Generators
                 tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
             }
 
-            if (tabItem.Content == null)
-            {
-                tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty];
-            }
-
             return tabItem;
         }
     }

+ 9 - 1
src/Avalonia.Controls/ItemsControl.cs

@@ -359,6 +359,12 @@ namespace Avalonia.Controls
             UpdateItemCount();
             RemoveControlItemsFromLogicalChildren(oldValue);
             AddControlItemsToLogicalChildren(newValue);
+
+            if (Presenter != null)
+            {
+                Presenter.Items = newValue;
+            }
+
             SubscribeToItems(newValue);
         }
 
@@ -370,6 +376,8 @@ namespace Avalonia.Controls
         /// <param name="e">The event args.</param>
         protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
+            UpdateItemCount();
+
             switch (e.Action)
             {
                 case NotifyCollectionChangedAction.Add:
@@ -381,7 +389,7 @@ namespace Avalonia.Controls
                     break;
             }
 
-            UpdateItemCount();
+            Presenter?.ItemsChanged(e);
 
             var collection = sender as ICollection;
             PseudoClasses.Set(":empty", collection == null || collection.Count == 0);

+ 7 - 0
src/Avalonia.Controls/Presenters/IItemsPresenter.cs

@@ -1,12 +1,19 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System.Collections;
+using System.Collections.Specialized;
+
 namespace Avalonia.Controls.Presenters
 {
     public interface IItemsPresenter : IPresenter
     {
+        IEnumerable Items { get; set; }
+
         IPanel Panel { get; }
 
+        void ItemsChanged(NotifyCollectionChangedEventArgs e);
+
         void ScrollIntoView(object item);
     }
 }

+ 13 - 2
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters
                 _itemsSubscription?.Dispose();
                 _itemsSubscription = null;
 
-                if (_createdPanel && value is INotifyCollectionChanged incc)
+                if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc)
                 {
                     _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
                 }
@@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters
             private set;
         }
 
+        protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
+
         /// <inheritdoc/>
         public override sealed void ApplyTemplate()
         {
@@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters
         {
         }
 
+        /// <inheritdoc/>
+        void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e)
+        {
+            if (Panel != null)
+            {
+                ItemsChanged(e);
+            }
+        }
+
         /// <summary>
         /// Creates the <see cref="ItemContainerGenerator"/> for the control.
         /// </summary>
@@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters
 
             _createdPanel = true;
 
-            if (_itemsSubscription == null && Items is INotifyCollectionChanged incc)
+            if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc)
             {
                 _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
             }

+ 22 - 7
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives
         /// <inheritdoc/>
         protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
-            base.ItemsCollectionChanged(sender, e);
-
             if (_updateCount > 0)
             {
+                base.ItemsCollectionChanged(sender, e);
                 return;
             }
 
+            switch (e.Action)
+            {
+                case NotifyCollectionChangedAction.Add:
+                    _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
+                    break;
+                case NotifyCollectionChangedAction.Remove:
+                    _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
+                    break;
+            }
+
+            base.ItemsCollectionChanged(sender, e);
+
             switch (e.Action)
             {
                 case NotifyCollectionChangedAction.Add:
@@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives
                     }
                     else
                     {
-                        _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
                         UpdateSelectedItem(_selection.First(), false);
                     }
 
                     break;
 
                 case NotifyCollectionChangedAction.Remove:
-                    _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
                     UpdateSelectedItem(_selection.First(), false);
                     ResetSelectedItems();
                     break;
@@ -1088,9 +1097,15 @@ namespace Avalonia.Controls.Primitives
                 }
                 else
                 {
-                    SelectedIndex = _updateSelectedIndex != int.MinValue ?
-                        _updateSelectedIndex :
-                        AlwaysSelected ? 0 : -1;
+                    if (_updateSelectedIndex != int.MinValue)
+                    {
+                        SelectedIndex = _updateSelectedIndex;
+                    }
+
+                    if (AlwaysSelected && SelectedIndex == -1)
+                    {
+                        SelectedIndex = 0;
+                    }
                 }
             }
         }

+ 5 - 1
src/Avalonia.Controls/ToolTipService.cs

@@ -1,6 +1,7 @@
 using System;
 using Avalonia.Input;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -79,7 +80,10 @@ namespace Avalonia.Controls
         {
             StopTimer();
 
-            ToolTip.SetIsOpen(control, true);
+            if ((control as IVisual).IsAttachedToVisualTree)
+            {
+                ToolTip.SetIsOpen(control, true);
+            }
         }
 
         private void Close(Control control)

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

@@ -42,7 +42,7 @@ namespace Avalonia.Controls
         /// </summary>
         static WrapPanel()
         {
-            AffectsMeasure<WrapPanel>(OrientationProperty);
+            AffectsMeasure<WrapPanel>(OrientationProperty, ItemWidthProperty, ItemHeightProperty);
         }
 
         /// <summary>

+ 11 - 2
src/Avalonia.Input/MouseDevice.cs

@@ -14,13 +14,14 @@ namespace Avalonia.Input
     /// <summary>
     /// Represents a mouse device.
     /// </summary>
-    public class MouseDevice : IMouseDevice
+    public class MouseDevice : IMouseDevice, IDisposable
     {
         private int _clickCount;
         private Rect _lastClickRect;
         private ulong _lastClickTime;
 
         private readonly Pointer _pointer;
+        private bool _disposed;
 
         public MouseDevice(Pointer pointer = null)
         {
@@ -126,7 +127,9 @@ namespace Avalonia.Input
         {
             Contract.Requires<ArgumentNullException>(e != null);
 
-            var mouse = (IMouseDevice)e.Device;
+            var mouse = (MouseDevice)e.Device;
+            if(mouse._disposed)
+                return;
 
             Position = e.Root.PointToScreen(e.Position);
             var props = CreateProperties(e);
@@ -441,5 +444,11 @@ namespace Avalonia.Input
                 el = (IInputElement)el.VisualParent;
             }
         }
+
+        public void Dispose()
+        {
+            _disposed = true;
+            _pointer?.Dispose();
+        }
     }
 }

+ 1 - 1
src/Avalonia.Input/Pointer.cs

@@ -37,7 +37,7 @@ namespace Avalonia.Input
         {
             if (Captured != null)
                 Captured.DetachedFromVisualTree -= OnCaptureDetached;
-            var oldCapture = control;
+            var oldCapture = Captured;
             Captured = control;
             PlatformCapture(control);
             if (oldCapture != null)

+ 6 - 3
src/Avalonia.Input/PointerPoint.cs

@@ -1,3 +1,6 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 namespace Avalonia.Input
 {
     public sealed class PointerPoint
@@ -27,9 +30,9 @@ namespace Avalonia.Input
         public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
         {
             PointerUpdateKind = kind;
-            IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton);
-            IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton);
-            IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton);
+            IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton);
+            IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton);
+            IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton);
 
             // The underlying input source might be reporting the previous state,
             // so make sure that we reflect the current state

+ 18 - 3
src/Avalonia.Input/TouchDevice.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Input.Raw;
@@ -11,10 +12,11 @@ namespace Avalonia.Input
     /// This class is supposed to be used on per-toplevel basis, don't use a shared one
     /// </remarks>
     /// </summary>
-    public class TouchDevice : IInputDevice
+    public class TouchDevice : IInputDevice, IDisposable
     {
-        Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
-
+        private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
+        private bool _disposed;
+        
         KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) =>
             (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
 
@@ -28,6 +30,8 @@ namespace Avalonia.Input
         
         public void ProcessRawEvent(RawInputEventArgs ev)
         {
+            if(_disposed)
+                return;
             var args = (RawTouchEventArgs)ev;
             if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
             {
@@ -82,6 +86,17 @@ namespace Avalonia.Input
 
             
         }
+
+        public void Dispose()
+        {
+            if(_disposed)
+                return;
+            var values = _pointers.Values.ToList();
+            _pointers.Clear();
+            _disposed = true;
+            foreach (var p in values)
+                p.Dispose();
+        }
         
     }
 }

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

@@ -95,7 +95,6 @@ namespace Avalonia.Native
                 .Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
                 .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
-                .Bind<IMouseDevice>().ToConstant(MouseDevice)
                 .Bind<IPlatformSettings>().ToConstant(this)
                 .Bind<IWindowingPlatform>().ToConstant(this)
                 .Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))

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

@@ -24,7 +24,7 @@ namespace Avalonia.Native
         private object _syncRoot = new object();
         private bool _deferredRendering = false;
         private bool _gpu = false;
-        private readonly IMouseDevice _mouse;
+        private readonly MouseDevice _mouse;
         private readonly IKeyboardDevice _keyboard;
         private readonly IStandardCursorFactory _cursorFactory;
         private Size _savedLogicalSize;
@@ -38,7 +38,7 @@ namespace Avalonia.Native
             _deferredRendering = opts.UseDeferredRendering;
 
             _keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
-            _mouse = AvaloniaLocator.Current.GetService<IMouseDevice>();
+            _mouse = new MouseDevice();
             _cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
         }
 
@@ -142,6 +142,7 @@ namespace Avalonia.Native
                 {
                     n?.Dispose();
                 }
+                _parent._mouse.Dispose();
             }
 
             void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke();

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

@@ -51,7 +51,7 @@ namespace Avalonia.Rendering.SceneGraph
                 UpdateSize(scene);
             }
 
-            if (visual.VisualRoot != null)
+            if (visual.VisualRoot == scene.Root.Visual)
             {
                 if (node?.Parent != null &&
                     visual.VisualParent != null &&

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

@@ -19,9 +19,7 @@ namespace Avalonia.X11
     class AvaloniaX11Platform : IWindowingPlatform
     {
         private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
-        private Lazy<MouseDevice> _mouseDevice = new Lazy<MouseDevice>(() => new MouseDevice());
         public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
-        public MouseDevice MouseDevice => _mouseDevice.Value;
         public Dictionary<IntPtr, Action<XEvent>> Windows = new Dictionary<IntPtr, Action<XEvent>>();
         public XI2Manager XI2;
         public X11Info Info { get; private set; }

+ 8 - 2
src/Avalonia.X11/X11Window.cs

@@ -32,7 +32,8 @@ namespace Avalonia.X11
         private PixelPoint? _configurePoint;
         private bool _triggeredExpose;
         private IInputRoot _inputRoot;
-        private readonly IMouseDevice _mouse;
+        private readonly MouseDevice _mouse;
+        private readonly TouchDevice _touch;
         private readonly IKeyboardDevice _keyboard;
         private PixelPoint? _position;
         private PixelSize _realSize;
@@ -57,7 +58,8 @@ namespace Avalonia.X11
             _platform = platform;
             _popup = popupParent != null;
             _x11 = platform.Info;
-            _mouse = platform.MouseDevice;
+            _mouse = new MouseDevice();
+            _touch = new TouchDevice();
             _keyboard = platform.KeyboardDevice;
 
             var glfeature = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
@@ -702,6 +704,8 @@ namespace Avalonia.X11
                 _platform.XI2?.OnWindowDestroyed(_handle);
                 _handle = IntPtr.Zero;
                 Closed?.Invoke();
+                _mouse.Dispose();
+                _touch.Dispose();
             }
             
             if (_useRenderWindow && _renderHandle != IntPtr.Zero)
@@ -830,6 +834,8 @@ namespace Avalonia.X11
         }
 
         public IMouseDevice MouseDevice => _mouse;
+        public TouchDevice TouchDevice => _touch;
+
         public IPopupImpl CreatePopup() 
             => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this);
 

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

@@ -92,8 +92,6 @@ namespace Avalonia.X11
         
         private PointerDeviceInfo _pointerDevice;
         private AvaloniaX11Platform _platform;
-        private readonly TouchDevice _touchDevice = new TouchDevice();
-
 
         public bool Init(AvaloniaX11Platform platform)
         {
@@ -198,7 +196,7 @@ namespace Avalonia.X11
                     (ev.Type == XiEventType.XI_TouchUpdate ?
                         RawPointerEventType.TouchUpdate :
                         RawPointerEventType.TouchEnd);
-                client.ScheduleInput(new RawTouchEventArgs(_touchDevice,
+                client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
                     ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
                 return;
             }
@@ -232,10 +230,10 @@ namespace Avalonia.X11
                 }
 
                 if (scrollDelta != default)
-                    client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp,
+                    client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
                         client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
                 if (_pointerDevice.HasMotion(ev))
-                    client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
+                    client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
                         RawPointerEventType.Move, ev.Position, ev.Modifiers));
             }
 
@@ -248,7 +246,7 @@ namespace Avalonia.X11
                     : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp)
                     : (RawPointerEventType?)null;
                 if (type.HasValue)
-                    client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
+                    client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
                         type.Value, ev.Position, ev.Modifiers));
             }
             
@@ -310,5 +308,7 @@ namespace Avalonia.X11
     {
         IInputRoot InputRoot { get; }
         void ScheduleInput(RawInputEventArgs args);
+        IMouseDevice MouseDevice { get; }
+        TouchDevice TouchDevice { get; }
     }
 }

+ 0 - 8
src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs

@@ -11,19 +11,11 @@ namespace Avalonia.Win32.Input
 {
     class WindowsMouseDevice : MouseDevice
     {
-        public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice();
-
         public WindowsMouseDevice() : base(new WindowsMousePointer())
         {
             
         }
         
-        public WindowImpl CurrentWindow
-        {
-            get;
-            set;
-        }
-
         class WindowsMousePointer : Pointer
         {
             public WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true)

+ 20 - 19
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -30,6 +30,7 @@ namespace Avalonia.Win32
         private IntPtr _hwnd;
         private bool _multitouch;
         private TouchDevice _touchDevice = new TouchDevice();
+        private MouseDevice _mouseDevice = new WindowsMouseDevice();
         private IInputRoot _owner;
         private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock();
         private bool _trackingMouse;
@@ -205,7 +206,7 @@ namespace Avalonia.Win32
             }
         }
 
-        public IMouseDevice MouseDevice => WindowsMouseDevice.Instance;
+        public IMouseDevice MouseDevice => _mouseDevice;
 
         public WindowState WindowState
         {
@@ -333,7 +334,7 @@ namespace Avalonia.Win32
 
         public void BeginMoveDrag(PointerPressedEventArgs e)
         {
-            WindowsMouseDevice.Instance.Capture(null);
+            _mouseDevice.Capture(null);
             UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN,
                 new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero);
             e.Pointer.Capture(null);
@@ -356,7 +357,7 @@ namespace Avalonia.Win32
 #if USE_MANAGED_DRAG
             _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position));
 #else
-            WindowsMouseDevice.Instance.Capture(null);
+            _mouseDevice.Capture(null);
             UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN,
                 new IntPtr((int)EdgeDic[edge]), IntPtr.Zero);
 #endif
@@ -437,9 +438,7 @@ namespace Avalonia.Win32
             uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime());
 
             RawInputEventArgs e = null;
-
-            WindowsMouseDevice.Instance.CurrentWindow = this;
-
+            
             switch ((UnmanagedMethods.WindowsMessage)msg)
             {
                 case UnmanagedMethods.WindowsMessage.WM_ACTIVATE:
@@ -485,6 +484,8 @@ namespace Avalonia.Win32
                         _parent._disabledBy.Remove(this);
                         _parent.UpdateEnabled();
                     }
+                    _mouseDevice.Dispose();
+                    _touchDevice?.Dispose();
                     //Free other resources
                     Dispose();
                     return IntPtr.Zero;
@@ -542,7 +543,7 @@ namespace Avalonia.Win32
                     if(ShouldIgnoreTouchEmulatedMessage())
                         break;
                     e = new RawPointerEventArgs(
-                        WindowsMouseDevice.Instance,
+                        _mouseDevice,
                         timestamp,
                         _owner,
                         msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN
@@ -559,7 +560,7 @@ namespace Avalonia.Win32
                     if(ShouldIgnoreTouchEmulatedMessage())
                         break;
                     e = new RawPointerEventArgs(
-                        WindowsMouseDevice.Instance,
+                        _mouseDevice,
                         timestamp,
                         _owner,
                         msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP
@@ -587,7 +588,7 @@ namespace Avalonia.Win32
                     }
 
                     e = new RawPointerEventArgs(
-                        WindowsMouseDevice.Instance,
+                        _mouseDevice,
                         timestamp,
                         _owner,
                         RawPointerEventType.Move,
@@ -597,7 +598,7 @@ namespace Avalonia.Win32
 
                 case UnmanagedMethods.WindowsMessage.WM_MOUSEWHEEL:
                     e = new RawMouseWheelEventArgs(
-                        WindowsMouseDevice.Instance,
+                        _mouseDevice,
                         timestamp,
                         _owner,
                         PointToClient(PointFromLParam(lParam)),
@@ -606,7 +607,7 @@ namespace Avalonia.Win32
 
                 case UnmanagedMethods.WindowsMessage.WM_MOUSEHWHEEL:
                     e = new RawMouseWheelEventArgs(
-                        WindowsMouseDevice.Instance,
+                        _mouseDevice,
                         timestamp,
                         _owner,
                         PointToClient(PointFromLParam(lParam)),
@@ -616,7 +617,7 @@ namespace Avalonia.Win32
                 case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE:
                     _trackingMouse = false;
                     e = new RawPointerEventArgs(
-                        WindowsMouseDevice.Instance,
+                        _mouseDevice,
                         timestamp,
                         _owner,
                         RawPointerEventType.LeaveWindow,
@@ -627,7 +628,7 @@ namespace Avalonia.Win32
                 case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN:
                 case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN:
                     e = new RawPointerEventArgs(
-                        WindowsMouseDevice.Instance,
+                        _mouseDevice,
                         timestamp,
                         _owner,
                         msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN
@@ -649,9 +650,9 @@ namespace Avalonia.Win32
                         {
                             Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
                                 _owner,
-                                touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ?
+                                touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
                                     RawPointerEventType.TouchEnd :
-                                    touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ?
+                                    touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
                                         RawPointerEventType.TouchBegin :
                                         RawPointerEventType.TouchUpdate,
                                 PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
@@ -771,11 +772,11 @@ namespace Avalonia.Win32
         {
             var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam);
             var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
-            if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
+            if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
                 modifiers |= RawInputModifiers.LeftMouseButton;
-            if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
+            if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
                 modifiers |= RawInputModifiers.RightMouseButton;
-            if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
+            if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
                 modifiers |= RawInputModifiers.MiddleMouseButton;
             return modifiers;
         }
@@ -785,7 +786,7 @@ namespace Avalonia.Win32
             // Ensure that the delegate doesn't get garbage collected by storing it as a field.
             _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc);
 
-            _className = "Avalonia-" + Guid.NewGuid();
+            _className = $"Avalonia-{Guid.NewGuid().ToString()}";
 
             UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX
             {

+ 21 - 7
tests/Avalonia.Animation.UnitTests/TransitionsTests.cs

@@ -1,14 +1,7 @@
 using System;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Avalonia.Animation;
 using Avalonia.Controls;
-using Avalonia.Styling;
 using Avalonia.UnitTests;
-using Avalonia.Data;
 using Xunit;
-using Avalonia.Animation.Easings;
 
 namespace Avalonia.Animation.UnitTests
 {
@@ -69,5 +62,26 @@ namespace Avalonia.Animation.UnitTests
                 Assert.Equal(0, border.Opacity);
             }
         }
+
+        [Fact]
+        public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick()
+        {
+            var clock = new MockGlobalClock();
+
+            using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
+            {
+                int i = 0;
+                var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue =>
+                {
+                    switch (i++)
+                    {
+                        case 0: Assert.Equal(0, nextValue); break;
+                        case 1: Assert.Equal(1d, nextValue); break;
+                    }
+                });
+
+                clock.Pulse(TimeSpan.FromMilliseconds(10));
+            }
+        }
     }
 }

+ 12 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

@@ -78,6 +78,18 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal(10, target.GetValue(Class1.AttachedProperty));
         }
 
+        [Fact]
+        public void PropertyChanged_Event_Uses_Coerced_Value()
+        {
+            var inst = new Class1();
+            inst.PropertyChanged += (sender, e) =>
+            {
+                Assert.Equal(10, e.NewValue);
+            };
+
+            inst.SetValue(Class1.QuxProperty, 15);
+        }
+
         private class Class1 : AvaloniaObject
         {
             public static readonly StyledProperty<int> QuxProperty =

+ 53 - 0
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -12,6 +12,7 @@ using Xunit;
 using System.Collections.ObjectModel;
 using Avalonia.UnitTests;
 using Avalonia.Input;
+using System.Collections.Generic;
 
 namespace Avalonia.Controls.UnitTests
 {
@@ -104,6 +105,28 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(new[] { child }, target.GetLogicalChildren());
         }
 
+        [Fact]
+        public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl()
+        {
+            var item = new Border();
+            var items = new ObservableCollection<Border>();
+
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                Items = items,
+            };
+
+            var root = new TestRoot(true, target);
+
+            root.Measure(new Size(100, 100));
+            root.Arrange(new Rect(0, 0, 100, 100));
+
+            items.Add(item);
+
+            Assert.Equal(target, item.Parent);
+        }
+
         [Fact]
         public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate()
         {
@@ -522,6 +545,36 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Presenter_Items_Should_Be_In_Sync()
+        {
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                Items = new object[]
+                {
+                    new Button(),
+                    new Button(),
+                },
+            };
+
+            var root = new TestRoot { Child = target };
+            var otherPanel = new StackPanel();
+
+            target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
+            
+            target.ItemContainerGenerator.Materialized += (s, e) =>
+            {
+                Assert.IsType<Canvas>(e.Containers[0].Item);
+            };
+
+            target.Items = new[]
+            {
+                new Canvas()
+            };
+        }
+
         private class Item
         {
             public Item(string value)

+ 187 - 2
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
@@ -14,6 +15,7 @@ using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Data;
+using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -23,7 +25,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
     public class SelectingItemsControlTests
     {
         private MouseTestHelper _helper = new MouseTestHelper();
-        
+
         [Fact]
         public void SelectedIndex_Should_Initially_Be_Minus_1()
         {
@@ -168,6 +170,130 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.Equal("B", listBox.SelectedItem);
         }
 
+        [Fact]
+        public void Setting_SelectedIndex_Before_Initialize_Should_Retain()
+        {
+            var listBox = new ListBox
+            {
+                SelectionMode = SelectionMode.Single,
+                Items = new[] { "foo", "bar", "baz" },
+                SelectedIndex = 1
+            };
+
+            listBox.BeginInit();
+
+            listBox.EndInit();
+
+            Assert.Equal(1, listBox.SelectedIndex);
+            Assert.Equal("bar", listBox.SelectedItem);
+        }
+
+        [Fact]
+        public void Setting_SelectedIndex_During_Initialize_Should_Take_Priority_Over_Previous_Value()
+        {
+            var listBox = new ListBox
+            {
+                SelectionMode = SelectionMode.Single,
+                Items = new[] { "foo", "bar", "baz" },
+                SelectedIndex = 2
+            };
+
+            listBox.BeginInit();
+
+            listBox.SelectedIndex = 1;
+
+            listBox.EndInit();
+
+            Assert.Equal(1, listBox.SelectedIndex);
+            Assert.Equal("bar", listBox.SelectedItem);
+        }
+
+        [Fact]
+        public void Setting_SelectedItem_Before_Initialize_Should_Retain()
+        {
+            var listBox = new ListBox
+            {
+                SelectionMode = SelectionMode.Single,
+                Items = new[] { "foo", "bar", "baz" },
+                SelectedItem = "bar"
+            };
+
+            listBox.BeginInit();
+
+            listBox.EndInit();
+
+            Assert.Equal(1, listBox.SelectedIndex);
+            Assert.Equal("bar", listBox.SelectedItem);
+        }
+
+
+        [Fact]
+        public void Setting_SelectedItems_Before_Initialize_Should_Retain()
+        {
+            var listBox = new ListBox
+            {
+                SelectionMode = SelectionMode.Multiple,
+                Items = new[] { "foo", "bar", "baz" },
+            };
+
+            var selected = new[] { "foo", "bar" };
+
+            foreach (var v in selected)
+            {
+                listBox.SelectedItems.Add(v);
+            }
+
+            listBox.BeginInit();
+
+            listBox.EndInit();
+
+            Assert.Equal(selected, listBox.SelectedItems);
+        }
+
+        [Fact]
+        public void Setting_SelectedItems_During_Initialize_Should_Take_Priority_Over_Previous_Value()
+        {
+            var listBox = new ListBox
+            {
+                SelectionMode = SelectionMode.Multiple,
+                Items = new[] { "foo", "bar", "baz" },
+            };
+
+            var selected = new[] { "foo", "bar" };
+
+            foreach (var v in new[] { "bar", "baz" })
+            {
+                listBox.SelectedItems.Add(v);
+            }
+
+            listBox.BeginInit();
+
+            listBox.SelectedItems = new AvaloniaList<object>(selected);
+
+            listBox.EndInit();
+
+            Assert.Equal(selected, listBox.SelectedItems);
+        }
+
+        [Fact]
+        public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain()
+        {
+            var listBox = new ListBox
+            {
+                SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected,
+
+                Items = new[] { "foo", "bar", "baz" },
+                SelectedIndex = 1
+            };
+
+            listBox.BeginInit();
+
+            listBox.EndInit();
+
+            Assert.Equal(1, listBox.SelectedIndex);
+            Assert.Equal("bar", listBox.SelectedItem);
+        }
+
         [Fact]
         public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True()
         {
@@ -849,7 +975,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             var target = new ListBox
             {
                 Template = Template(),
-                Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"},
+                Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
             };
 
             target.ApplyTemplate();
@@ -980,6 +1106,45 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.True(raised);
         }
 
+        [Fact]
+        public void AutoScrollToSelectedItem_On_Reset_Works()
+        {
+            // Issue #3148
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var items = new ResettingCollection(100);
+
+                var target = new ListBox
+                {
+                    Items = items,
+                    ItemTemplate = new FuncDataTemplate<string>((x, _) =>
+                        new TextBlock
+                        {
+                            Text = x,
+                            Width = 100,
+                            Height = 10
+                        }),
+                    AutoScrollToSelectedItem = true,
+                    VirtualizationMode = ItemVirtualizationMode.Simple,
+                };
+
+                var root = new TestRoot(true, target);
+                root.Measure(new Size(100, 100));
+                root.Arrange(new Rect(0, 0, 100, 100));
+
+                Assert.True(target.Presenter.Panel.Children.Count > 0);
+                Assert.True(target.Presenter.Panel.Children.Count < 100);
+
+                target.SelectedItem = "Item99";
+
+                // #3148 triggered here.
+                items.Reset(new[] { "Item99" });
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(1, target.Presenter.Panel.Children.Count);
+            }
+        }
+
         [Fact]
         public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization()
         {
@@ -1028,6 +1193,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                     Name = "itemsPresenter",
                     [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
                     [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
+                    [~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty],
                 }.RegisterInNameScope(scope));
         }
 
@@ -1072,5 +1238,24 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 return base.MoveSelection(direction, wrap);
             }
         }
+
+        private class ResettingCollection : List<string>, INotifyCollectionChanged
+        {
+            public ResettingCollection(int itemCount)
+            {
+                AddRange(Enumerable.Range(0, itemCount).Select(x => $"Item{x}"));
+            }
+
+            public void Reset(IEnumerable<string> items)
+            {
+                Clear();
+                AddRange(items);
+                CollectionChanged?.Invoke(
+                    this,
+                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+            }
+
+            public event NotifyCollectionChangedEventHandler CollectionChanged;
+        }
     }
 }

+ 20 - 0
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Collections.ObjectModel;
 using System.Linq;
+using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
@@ -287,6 +288,25 @@ namespace Avalonia.Controls.UnitTests
             Assert.Single(target.GetLogicalChildren(), content);
         }
 
+        [Fact]
+        public void Should_Not_Propagate_DataContext_To_TabItem_Content()
+        {
+            var dataContext = "DataContext";
+
+            var tabItem = new TabItem();
+
+            var target = new TabControl
+            {
+                Template = TabControlTemplate(),
+                DataContext = dataContext,
+                Items = new AvaloniaList<object> { tabItem }
+            };
+
+            ApplyTemplate(target);
+
+            Assert.NotEqual(dataContext, tabItem.Content);
+        }
+
         private IControlTemplate TabControlTemplate()
         {
             return new FuncControlTemplate<TabControl>((parent, scope) =>

+ 109 - 0
tests/Avalonia.Controls.UnitTests/ToolTipTests.cs

@@ -0,0 +1,109 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Disposables;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class TolTipTests
+    {
+        private MouseTestHelper _mouseHelper = new MouseTestHelper();
+
+        [Fact]
+        public void Should_Not_Open_On_Detached_Control()
+        {
+            //issue #3188
+            var control = new Decorator()
+            {
+                [ToolTip.TipProperty] = "Tip",
+                [ToolTip.ShowDelayProperty] = 0
+            };
+
+            Assert.False((control as IVisual).IsAttachedToVisualTree);
+
+            //here in issue #3188 exception is raised
+            _mouseHelper.Enter(control);
+
+            Assert.False(ToolTip.GetIsOpen(control));
+        }
+
+        [Fact]
+        public void Should_Open_On_Pointer_Enter()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+
+                var target = new Decorator()
+                {
+                    [ToolTip.TipProperty] = "Tip",
+                    [ToolTip.ShowDelayProperty] = 0
+                };
+
+                window.Content = target;
+
+                window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
+
+                Assert.True((target as IVisual).IsAttachedToVisualTree);
+
+                _mouseHelper.Enter(target);
+
+                Assert.True(ToolTip.GetIsOpen(target));
+            }
+        }
+
+        [Fact]
+        public void Should_Open_On_Pointer_Enter_With_Delay()
+        {
+            Action timercallback = null;
+            var delay = TimeSpan.Zero;
+
+            var pti = Mock.Of<IPlatformThreadingInterface>(x => x.CurrentThreadIsLoopThread == true);
+
+            Mock.Get(pti)
+                .Setup(v => v.StartTimer(It.IsAny<DispatcherPriority>(), It.IsAny<TimeSpan>(), It.IsAny<Action>()))
+                .Callback<DispatcherPriority, TimeSpan, Action>((priority, interval, tick) =>
+                {
+                    delay = interval;
+                    timercallback = tick;
+                })
+                .Returns(Disposable.Empty);
+
+            using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti)))
+            {
+                var window = new Window();
+
+                var target = new Decorator()
+                {
+                    [ToolTip.TipProperty] = "Tip",
+                    [ToolTip.ShowDelayProperty] = 1
+                };
+
+                window.Content = target;
+
+                window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
+
+                Assert.True((target as IVisual).IsAttachedToVisualTree);
+
+                _mouseHelper.Enter(target);
+
+                Assert.Equal(TimeSpan.FromMilliseconds(1), delay);
+                Assert.NotNull(timercallback);
+                Assert.False(ToolTip.GetIsOpen(target));
+
+                timercallback();
+
+                Assert.True(ToolTip.GetIsOpen(target));
+            }
+        }
+    }
+}

+ 53 - 25
tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

@@ -12,14 +12,14 @@ namespace Avalonia.Controls.UnitTests
         public void Lays_Out_Horizontally_On_Separate_Lines()
         {
             var target = new WrapPanel()
-                         {
-                            Width = 100,
-                            Children =
+            {
+                Width = 100,
+                Children =
                             {
                                 new Border { Height = 50, Width = 100 },
                                 new Border { Height = 50, Width = 100 },
                             }
-                         };
+            };
 
             target.Measure(Size.Infinity);
             target.Arrange(new Rect(target.DesiredSize));
@@ -33,14 +33,14 @@ namespace Avalonia.Controls.UnitTests
         public void Lays_Out_Horizontally_On_A_Single_Line()
         {
             var target = new WrapPanel()
-                         {
-                            Width = 200,
-                            Children =
+            {
+                Width = 200,
+                Children =
                             {
                                 new Border { Height = 50, Width = 100 },
                                 new Border { Height = 50, Width = 100 },
                             }
-                         };
+            };
 
             target.Measure(Size.Infinity);
             target.Arrange(new Rect(target.DesiredSize));
@@ -54,15 +54,15 @@ namespace Avalonia.Controls.UnitTests
         public void Lays_Out_Vertically_Children_On_A_Single_Line()
         {
             var target = new WrapPanel()
-                         {
-                            Orientation = Orientation.Vertical,
-                            Height = 120,
-                            Children =
+            {
+                Orientation = Orientation.Vertical,
+                Height = 120,
+                Children =
                             {
                                 new Border { Height = 50, Width = 100 },
                                 new Border { Height = 50, Width = 100 },
                             }
-                         };
+            };
 
             target.Measure(Size.Infinity);
             target.Arrange(new Rect(target.DesiredSize));
@@ -76,15 +76,15 @@ namespace Avalonia.Controls.UnitTests
         public void Lays_Out_Vertically_On_Separate_Lines()
         {
             var target = new WrapPanel()
-                         {
-                            Orientation = Orientation.Vertical,
-                            Height = 60,
-                            Children =
+            {
+                Orientation = Orientation.Vertical,
+                Height = 60,
+                Children =
                             {
                                 new Border { Height = 50, Width = 100 },
                                 new Border { Height = 50, Width = 100 },
                             }
-                         };
+            };
 
             target.Measure(Size.Infinity);
             target.Arrange(new Rect(target.DesiredSize));
@@ -98,17 +98,17 @@ namespace Avalonia.Controls.UnitTests
         public void Applies_ItemWidth_And_ItemHeight_Properties()
         {
             var target = new WrapPanel()
-                        {
-                            Orientation = Orientation.Horizontal,
-                            Width = 50,
-                            ItemWidth = 20,
-                            ItemHeight = 15,
-                            Children =
+            {
+                Orientation = Orientation.Horizontal,
+                Width = 50,
+                ItemWidth = 20,
+                ItemHeight = 15,
+                Children =
                             {
                                 new Border(),
                                 new Border(),
                             }
-                        };
+            };
 
             target.Measure(Size.Infinity);
             target.Arrange(new Rect(target.DesiredSize));
@@ -117,5 +117,33 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(new Rect(0, 0, 20, 15), target.Children[0].Bounds);
             Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds);
         }
+
+        [Fact]
+        void ItemWidth_Trigger_InvalidateMeasure()
+        {
+            var target = new WrapPanel();
+
+            target.Measure(new Size(10, 10));
+
+            Assert.True(target.IsMeasureValid);
+
+            target.ItemWidth = 1;
+
+            Assert.False(target.IsMeasureValid);
+        }
+
+        [Fact]
+        void ItemHeight_Trigger_InvalidateMeasure()
+        {
+            var target = new WrapPanel();
+
+            target.Measure(new Size(10, 10));
+
+            Assert.True(target.IsMeasureValid);
+
+            target.ItemHeight = 1;
+
+            Assert.False(target.IsMeasureValid);
+        }
     }
 }

+ 39 - 0
tests/Avalonia.Input.UnitTests/PointerTests.cs

@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Xunit;
+
+namespace Avalonia.Input.UnitTests
+{
+    public class PointerTests
+    {
+        [Fact]
+        public void On_Capture_Transfer_PointerCaptureLost_Should_Propagate_Up_To_The_Common_Parent()
+        {
+            Border initialParent, initialCapture, newParent, newCapture;
+            var el = new StackPanel
+            {
+                Children =
+                {
+                    (initialParent = new Border { Child = initialCapture = new Border() }),
+                    (newParent = new Border { Child = newCapture = new Border() })
+                }
+            };
+            var receivers = new List<object>();
+            var root = new TestRoot(el);
+            foreach (InputElement d in root.GetSelfAndVisualDescendants())
+                d.PointerCaptureLost += (s, e) => receivers.Add(s);
+            var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
+            
+            pointer.Capture(initialCapture);
+            pointer.Capture(newCapture);
+            Assert.True(receivers.SequenceEqual(new[] { initialCapture, initialParent }));
+            
+            receivers.Clear();
+            pointer.Capture(null);
+            Assert.True(receivers.SequenceEqual(new object[] { newCapture, newParent, el, root }));
+        }
+    }
+}

+ 1 - 1
tests/Avalonia.UnitTests/MouseTestHelper.cs

@@ -84,9 +84,9 @@ namespace Avalonia.UnitTests
             );
             if (ButtonCount(props) == 0)
             {
-                _pointer.Capture(null);
                 target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position,
                     Timestamp(), props, GetModifiers(modifiers), _pressedButton));
+                _pointer.Capture(null);
             }
             else
                 Move(target, source, position);

+ 11 - 0
tests/Avalonia.UnitTests/TestRoot.cs

@@ -24,8 +24,19 @@ namespace Avalonia.UnitTests
         }
 
         public TestRoot(IControl child)
+            : this(false, child)
+        {
+            Child = child;
+        }
+
+        public TestRoot(bool useGlobalStyles, IControl child)
             : this()
         {
+            if (useGlobalStyles)
+            {
+                StylingParent = UnitTestApplication.Current;
+            }
+
             Child = child;
         }
 

+ 75 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@@ -369,6 +369,81 @@ namespace Avalonia.Visuals.UnitTests.Rendering
 
         }
 
+        [Fact]
+        public void Should_Update_VisualNodes_When_Child_Moved_To_New_Parent_And_New_Root()
+        {
+            var dispatcher = new ImmediateDispatcher();
+            var loop = new Mock<IRenderLoop>();
+
+            Decorator moveFrom;
+            Decorator moveTo;
+            Canvas moveMe;
+
+            var root = new TestRoot
+            {
+                Child = new StackPanel
+                {
+                    Children =
+                    {
+                        (moveFrom = new Decorator
+                        {
+                            Child = moveMe = new Canvas(),
+                        })
+                    }
+                }
+            };
+
+            var otherRoot = new TestRoot
+            {
+                Child = new StackPanel
+                {
+                    Children =
+                    {
+                        (moveTo = new Decorator())
+                    }
+                }
+            };
+
+            var sceneBuilder = new SceneBuilder();
+            var target = new DeferredRenderer(
+                root,
+                loop.Object,
+                sceneBuilder: sceneBuilder,
+                dispatcher: dispatcher);
+
+            var otherSceneBuilder = new SceneBuilder();
+            var otherTarget = new DeferredRenderer(
+                otherRoot,
+                loop.Object,
+                sceneBuilder: otherSceneBuilder,
+                dispatcher: dispatcher);
+
+            root.Renderer = target;
+            otherRoot.Renderer = otherTarget;
+
+            target.Start();
+            otherTarget.Start();
+
+            RunFrame(target);
+            RunFrame(otherTarget);
+
+            moveFrom.Child = null;
+            moveTo.Child = moveMe;
+
+            RunFrame(target);
+            RunFrame(otherTarget);
+
+            var scene = target.UnitTestScene();
+            var otherScene = otherTarget.UnitTestScene();
+
+            var moveFromNode = (VisualNode)scene.FindNode(moveFrom);
+            var moveToNode = (VisualNode)otherScene.FindNode(moveTo);
+
+            Assert.Empty(moveFromNode.Children);
+            Assert.Equal(1, moveToNode.Children.Count);
+            Assert.Same(moveMe, moveToNode.Children[0].Visual);
+        }
+
         [Fact]
         public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity()
         {