From f29aeffecd7e95d2d63669b648c7a8580c58ffaa Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 3 Oct 2019 18:01:04 +0200 Subject: [PATCH 01/30] Avoid boxing mouse modifiers. --- src/Avalonia.Base/EnumExtensions.cs | 23 +++++++++++++++++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 12 ++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Base/EnumExtensions.cs diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs new file mode 100644 index 0000000000..a8306c2d69 --- /dev/null +++ b/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 +{ + /// + /// Provides extension methods for enums. + /// + public static class EnumExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum + { + var intValue = *(int*)&value; + var intFlag = *(int*)&flag; + + return (intValue & intFlag) == intFlag; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e8c3177ec5..6a733356b8 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -646,9 +646,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)), @@ -768,11 +768,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; } @@ -782,7 +782,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 { From 09e76184631125508d834b04f45e0c707a709ce5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 3 Oct 2019 18:24:28 +0200 Subject: [PATCH 02/30] Remove boxing from PointerPointProperties. --- src/Avalonia.Input/PointerPoint.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index d823a78090..55c1889e58 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/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 From 389d58f550cc5f5d4ba8e8883bd96921b0dddf23 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 20 Oct 2019 14:23:28 +0300 Subject: [PATCH 03/30] issue #3089 add failing tests --- .../Primitives/SelectingItemsControlTests.cs | 128 +++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..ce333f66d6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -23,7 +23,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 +168,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(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 +973,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(); From 2e70be023b4a5236d080be36b11572866acf0271 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 20 Oct 2019 14:36:23 +0300 Subject: [PATCH 04/30] fixed #3089 now set selecteditem/s before initialization should work properly --- .../Primitives/SelectingItemsControl.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..4b33b18475 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1088,9 +1088,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; + } } } } From 3b1005f04e78cee9928ae332a695d20f5b7bdd71 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 16:39:13 +0300 Subject: [PATCH 05/30] Removed pointer global state that caused trouble --- src/Avalonia.Input/MouseDevice.cs | 13 +++++++-- src/Avalonia.Input/TouchDevice.cs | 21 ++++++++++++--- src/Avalonia.Native/AvaloniaNativePlatform.cs | 1 - src/Avalonia.Native/WindowImplBase.cs | 5 ++-- src/Avalonia.X11/X11Platform.cs | 2 -- src/Avalonia.X11/X11Window.cs | 10 +++++-- src/Avalonia.X11/XI2Manager.cs | 12 ++++----- .../Input/WindowsMouseDevice.cs | 8 ------ src/Windows/Avalonia.Win32/WindowImpl.cs | 27 ++++++++++--------- 9 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index c84596b913..4dcf0eee53 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -14,13 +14,14 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - 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(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(); + } } } diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index b231c9fff4..d6ad836f37 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/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 /// /// - public class TouchDevice : IInputDevice + public class TouchDevice : IInputDevice, IDisposable { - Dictionary _pointers = new Dictionary(); - + private readonly Dictionary _pointers = new Dictionary(); + 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(); + } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index ddb71b61bb..fab3ce36b8 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -95,7 +95,6 @@ namespace Avalonia.Native .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) - .Bind().ToConstant(MouseDevice) .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index fe7458d583..7f1fab4b1c 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/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(); - _mouse = AvaloniaLocator.Current.GetService(); + _mouse = new MouseDevice(); _cursorFactory = AvaloniaLocator.Current.GetService(); } @@ -142,6 +142,7 @@ namespace Avalonia.Native { n?.Dispose(); } + _parent._mouse.Dispose(); } void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke(); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index d7a7bb97fd..6ba562bb69 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -19,9 +19,7 @@ namespace Avalonia.X11 class AvaloniaX11Platform : IWindowingPlatform { private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice()); - private Lazy _mouseDevice = new Lazy(() => new MouseDevice()); public KeyboardDevice KeyboardDevice => _keyboardDevice.Value; - public MouseDevice MouseDevice => _mouseDevice.Value; public Dictionary> Windows = new Dictionary>(); public XI2Manager XI2; public X11Info Info { get; private set; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 17471fad10..32460fed86 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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(); @@ -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); diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 6989d6d26d..e37ed39bee 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/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; } } } diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index e7c379ad89..8f060b1b81 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/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) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 04a9303d53..3777ce5e1e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/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 From d8ee7531aba3bb77dd07c60012ec117d6c287c5c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 17:29:18 +0300 Subject: [PATCH 06/30] Properly raise PointerCaptureLost on capture transfer --- src/Avalonia.Input/Pointer.cs | 2 +- .../Avalonia.Input.UnitTests/PointerTests.cs | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Input.UnitTests/PointerTests.cs diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 80d803abb1..819d231b31 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/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) diff --git a/tests/Avalonia.Input.UnitTests/PointerTests.cs b/tests/Avalonia.Input.UnitTests/PointerTests.cs new file mode 100644 index 0000000000..e639726dbd --- /dev/null +++ b/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(); + 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 })); + } + } +} From 66b3da044c7a8af634e14bdc4e15beb0071b5eb7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 18:20:01 +0300 Subject: [PATCH 07/30] Fixed MouseTestHelper --- tests/Avalonia.UnitTests/MouseTestHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index 48c4d73471..f6454a9cd2 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/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); From 58b79a7724f1d492758a60bc7723abbc65b3be3b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 18:47:18 +0100 Subject: [PATCH 08/30] Validate min/max in MathUtilities.Clamp. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 41b57b6e70..027028480c 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -159,6 +159,11 @@ namespace Avalonia.Utilities /// The clamped value. 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; From 0a8915b1cc9ffb2868a77e287527cb492dc272d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 19:55:23 +0100 Subject: [PATCH 09/30] Added failing test for #3148 --- .../Primitives/SelectingItemsControlTests.cs | 61 +++++++++++++++++++ tests/Avalonia.UnitTests/TestRoot.cs | 11 ++++ 2 files changed, 72 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..e5bbde2eba 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/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; @@ -980,6 +982,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((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 +1069,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 +1114,24 @@ namespace Avalonia.Controls.UnitTests.Primitives return base.MoveSelection(direction, wrap); } } + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(int itemCount) + { + AddRange(Enumerable.Range(0, itemCount).Select(x => $"Item{x}")); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } } } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 969d7bc821..56d7f028f2 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/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; } From 4f41a70455516f9ee73c87212ea7913a038deb97 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 21:45:06 +0100 Subject: [PATCH 10/30] Make sure ItemsPresenter is updated before selection change. Having a separate subscription to `Items.CollectionChanged` in both `ItemsControl` and `ItemsPresenter` meant that the `ItemsPresenter` sometimes doesn't update itself in time for a selection change, resulting in #3148. Make `ItemsControl` trigger `ItemsPresenter.ItemsChanged` rather than both controls listening to the event separately. --- src/Avalonia.Controls/ItemsControl.cs | 6 ++++-- .../Presenters/IItemsPresenter.cs | 4 ++++ .../Presenters/ItemsPresenterBase.cs | 15 +++++++++++++-- .../Primitives/SelectingItemsControl.cs | 17 +++++++++++++---- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b027da6d0c..b93346792d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -356,6 +356,7 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; + Presenter?.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); @@ -370,6 +371,9 @@ namespace Avalonia.Controls /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + UpdateItemCount(); + Presenter?.ItemsChanged(e); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -381,8 +385,6 @@ namespace Avalonia.Controls break; } - UpdateItemCount(); - var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 42311dc781..21a03402a0 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +1,16 @@ // 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.Specialized; + namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { IPanel Panel { get; } + void ItemsChanged(NotifyCollectionChangedEventArgs e); + void ScrollIntoView(object item); } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 0f0cdc37cf..ef1f277162 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/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; + /// public override sealed void ApplyTemplate() { @@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters { } + /// + void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) + { + if (Panel != null) + { + ItemsChanged(e); + } + } + /// /// Creates the for the control. /// @@ -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); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..abad53f0d6 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives /// 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; From 96619e44408aea5319a80b40327d67cee94c4615 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 14:49:19 +0100 Subject: [PATCH 11/30] Added failing test. `ItemsPresenter.Items` isn't correctly in sync with `ItemsControl.Items` when assigning new `Items`. --- .../ItemsControlTests.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index b2839360ee..c338d29e96 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -522,6 +522,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(e.Containers[0].Item); + }; + + target.Items = new[] + { + new Canvas() + }; + } + private class Item { public Item(string value) From 496fae20e55995017faf8f6f5672c507dc45fd15 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 14:59:47 +0100 Subject: [PATCH 12/30] Ensure Items are synchronised with Presenter. Ensure that the `ItemsControl`'s presenter is notified of an `Items` change at the appropriate time. --- src/Avalonia.Controls/ItemsControl.cs | 7 ++++++- src/Avalonia.Controls/Presenters/IItemsPresenter.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b93346792d..1203792559 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -356,10 +356,15 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; - Presenter?.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); + + if (Presenter != null) + { + Presenter.Items = newValue; + } + SubscribeToItems(newValue); } diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 21a03402a0..c4acf1ebef 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +1,15 @@ // 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); From 9107c0e96eea42756f7d6ea26013979986e7adfe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 23:01:18 +0100 Subject: [PATCH 13/30] Added another failing test. Adapted from failing test described by @MarchingCube in PR feedback: https://github.com/AvaloniaUI/Avalonia/pull/3177#issuecomment-547515972 --- .../ItemsControlTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index c338d29e96..227d783874 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/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(); + + 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() { From 68b655b5177175fc1b168b2130064992575c1fb6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Oct 2019 07:32:38 +0100 Subject: [PATCH 14/30] Add to logical children before notifying presenter. This makes sure that newly added control items have the correct logical parent. --- src/Avalonia.Controls/ItemsControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1203792559..bf22f0a08a 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -377,7 +377,6 @@ namespace Avalonia.Controls protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { UpdateItemCount(); - Presenter?.ItemsChanged(e); switch (e.Action) { @@ -390,6 +389,8 @@ namespace Avalonia.Controls break; } + Presenter?.ItemsChanged(e); + var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); From b7a3747da66b5d0feae56dc10636c0f10b695ed2 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 30 Oct 2019 23:26:34 +0100 Subject: [PATCH 15/30] Add failing test for https://github.com/AvaloniaUI/Avalonia/issues/3179. --- .../Rendering/DeferredRendererTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index b4743e900d..6063a382a0 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/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(); + + 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() { From d59d0904f43997a8cc4dcbb6064d141543045091 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 11:20:33 +0200 Subject: [PATCH 16/30] add test for tooltip --- .../ToolTipTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/ToolTipTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs new file mode 100644 index 0000000000..2a1bb78c2e --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -0,0 +1,45 @@ +// 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_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)); + } + } + } +} From 60a596bd82ebb387da5047f647e1388d3afb0b85 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 14:52:08 +0200 Subject: [PATCH 17/30] add another test for tooltip --- .../ToolTipTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 2a1bb78c2e..eba0d7cb3e 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -41,5 +41,51 @@ namespace Avalonia.Controls.UnitTests 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(x => x.CurrentThreadIsLoopThread == true); + + Mock.Get(pti) + .Setup(v => v.StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((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)); + } + } } } From 2288b7ab1d0463f84032c8907eb4b1fae671840b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 14:52:59 +0200 Subject: [PATCH 18/30] add failing test for tooltip for issue #3188 --- .../ToolTipTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index eba0d7cb3e..362dd6d111 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -16,6 +16,24 @@ namespace Avalonia.Controls.UnitTests { 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() { From a3dbf72947d4e971325cd2870b19e99cd4ae65c5 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 15:03:33 +0200 Subject: [PATCH 19/30] don't try open tooltip on detached control --- src/Avalonia.Controls/ToolTipService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 384a9db0cf..d90729e8a5 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/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) From f933fe53cfd8d5e029e930b81c8a7f36c5640e9a Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 14:13:58 +0100 Subject: [PATCH 20/30] Add failing test #2699 --- .../TabControlTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ddf7e7a0fa..b65a4d67d4 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/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 { tabItem } + }; + + ApplyTemplate(target); + + Assert.NotEqual(dataContext, tabItem.Content); + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => From e9335d6a2c4755bd120e7da06e2dec0029f93f54 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 14:15:11 +0100 Subject: [PATCH 21/30] Fix #2699 --- .../Generators/TabItemContainerGenerator.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index a6a64e570b..d99648a158 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/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; } } From a612ee5a3eb1f5d80dfd540e5c64f5d998139784 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 1 Nov 2019 18:04:12 +0200 Subject: [PATCH 22/30] fix WrapPanel issue #3165 item width/height should trigger new measure --- src/Avalonia.Controls/WrapPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 7c88401615..d06a71a9f8 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls /// static WrapPanel() { - AffectsMeasure(OrientationProperty); + AffectsMeasure(OrientationProperty, ItemWidthProperty, ItemHeightProperty); } /// From f3bb98756f694f45f0fd6b47c6164495721e5134 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 3 Nov 2019 01:31:24 +0200 Subject: [PATCH 23/30] add tests for #3165 --- .../WrapPanelTests.cs | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index fd93df46b8..82f94b4f3c 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/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); + } } } From e126874c80108e1e47c41cfafb1db9254dfcf256 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Nov 2019 11:25:32 +0100 Subject: [PATCH 24/30] Handle cross-root reparenting. When updating the scene for a window, moving a control to a different root should be the same as just removing the control. --- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 558e96e132..1689f2e964 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/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 && From dcf52fce844916f174cf89617f439c9f5935524e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 4 Nov 2019 14:35:58 +0100 Subject: [PATCH 25/30] Use a new macosx SDK --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 92e4afdca8..04e7262930 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -49,7 +49,7 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx10.14' + sdk: 'macosx10.15' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcodeVersion: 'default' # Options: 8, 9, default, specifyPath From bca8bc2fceb3915e74d3da316aab1cd614e6b476 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 4 Nov 2019 15:00:06 +0100 Subject: [PATCH 26/30] Update azure-pipelines.yml --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 04e7262930..4fe4d825bb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -49,10 +49,10 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx10.15' + sdk: 'macosx10.14' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: 'default' # Options: 8, 9, default, specifyPath + xcodeVersion: '10.3' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 From 08fdce729eef0cff19afeb365c1fc840170088fb Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 4 Nov 2019 15:09:11 +0100 Subject: [PATCH 27/30] Update azure-pipelines.yml --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4fe4d825bb..603308ef9a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -52,7 +52,7 @@ jobs: sdk: 'macosx10.14' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: '10.3' # Options: 8, 9, default, specifyPath + xcodeVersion: '10' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 From 5921d359c83a4149680265a9358172fe7e52b33d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Nov 2019 15:08:21 +0000 Subject: [PATCH 28/30] fix hit testing skia formatted text. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index eb7b65cdce..718d10a694 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -99,9 +99,17 @@ namespace Avalonia.Skia public TextHitTestResult HitTestPoint(Point point) { float y = (float)point.Y; - var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y); - if (!line.Equals(default(AvaloniaFormattedTextLine))) + AvaloniaFormattedTextLine line = default; + + int i = 0; + + while(_skiaLines[i].Top < y && i < _skiaLines.Count) + { + line = _skiaLines[i++]; + } + + if (!line.Equals(default)) { var rects = GetRects(); From 075aeecebf1694d4c35c27f75a164a9280e9f784 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Nov 2019 15:59:17 +0000 Subject: [PATCH 29/30] fix skia formatted text impl hit testing. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 28 +++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 718d10a694..dfdbfef136 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -102,12 +102,17 @@ namespace Avalonia.Skia AvaloniaFormattedTextLine line = default; - int i = 0; - - while(_skiaLines[i].Top < y && i < _skiaLines.Count) + foreach(var currentLine in _skiaLines) { - line = _skiaLines[i++]; - } + if(currentLine.Top <= y) + { + line = currentLine; + } + else + { + break; + } + } if (!line.Equals(default)) { @@ -135,12 +140,15 @@ namespace Avalonia.Skia line.Length : (line.Length - 1); } - return new TextHitTestResult + if (y < line.Top + line.Height) { - IsInside = false, - TextPosition = line.Start + offset, - IsTrailing = Text.Length == (line.Start + offset + 1) - }; + return new TextHitTestResult + { + IsInside = false, + TextPosition = line.Start + offset, + IsTrailing = Text.Length == (line.Start + offset + 1) + }; + } } bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height); From eb1241eea36d45b000792f373952e9e58df6e321 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Nov 2019 16:26:45 +0000 Subject: [PATCH 30/30] fix skia formatted text unit tests. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index dfdbfef136..a5e56b1d7a 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -102,14 +102,18 @@ namespace Avalonia.Skia AvaloniaFormattedTextLine line = default; + float nextTop = 0; + foreach(var currentLine in _skiaLines) { if(currentLine.Top <= y) { line = currentLine; + nextTop = currentLine.Top + currentLine.Height; } else { + nextTop = currentLine.Top; break; } } @@ -140,7 +144,7 @@ namespace Avalonia.Skia line.Length : (line.Length - 1); } - if (y < line.Top + line.Height) + if (y < nextTop) { return new TextHitTestResult {