diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index bf177d64cd..9bc7ba9e2f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1630,7 +1630,7 @@ namespace Avalonia.Controls /// /// The source object. /// The event data. - private void DropDownPopup_Closed(object sender, EventArgs e) + private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e) { // Force the drop down dependency property to be false. if (IsDropDownOpen) @@ -1638,6 +1638,11 @@ namespace Avalonia.Controls IsDropDownOpen = false; } + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + // Fire the DropDownClosed event if (_popupHasOpened) { diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 07e42c64e4..b4e4ad1452 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -895,12 +895,17 @@ namespace Avalonia.Controls _ignoreButtonClick = false; } } - private void PopUp_Closed(object sender, EventArgs e) + private void PopUp_Closed(object sender, PopupClosedEventArgs e) { IsDropDownOpen = false; if(!_isPopupClosing) { + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + _isPopupClosing = true; Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false); } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 4b7d931d80..1daa6a5630 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -242,11 +242,16 @@ namespace Avalonia.Controls } } - private void PopupClosed(object sender, EventArgs e) + private void PopupClosed(object sender, PopupClosedEventArgs e) { _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + if (CanFocus(this)) { Focus(); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index f069903e05..66f2153b6c 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when the popup closes. /// - public event EventHandler? Closed; + public event EventHandler? Closed; /// /// Raised when the popup opens. @@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives if (parentPopupRoot?.Parent is Popup popup) { - DeferCleanup(SubscribeToEventHandler(popup, ParentClosed, + DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, (x, handler) => x.Closed += handler, (x, handler) => x.Closed -= handler)); } @@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives /// /// Closes the popup. /// - public void Close() - { - if (_openState is null) - { - using (BeginIgnoringIsOpen()) - { - IsOpen = false; - } - - return; - } - - _openState.Dispose(); - _openState = null; - - using (BeginIgnoringIsOpen()) - { - IsOpen = false; - } - - Closed?.Invoke(this, EventArgs.Empty); - } + public void Close() => CloseCore(null); /// /// Measures the control. @@ -389,22 +368,44 @@ namespace Avalonia.Controls.Primitives } } + private void CloseCore(EventArgs? closeEvent) + { + if (_openState is null) + { + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + + return; + } + + _openState.Dispose(); + _openState = null; + + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + + Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent)); + } + private void ListenForNonClientClick(RawInputEventArgs e) { var mouse = e as RawPointerEventArgs; if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { - Close(); + CloseCore(e); } } private void PointerPressedOutside(object sender, PointerPressedEventArgs e) { - if (!StaysOpen && !IsChildOrThis((IVisual)e.Source)) + if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v)) { - Close(); - e.Handled = true; + CloseCore(e); } } diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs new file mode 100644 index 0000000000..c51543438c --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Interactivity; + +#nullable enable + +namespace Avalonia.Controls.Primitives +{ + /// + /// Holds data for the event. + /// + public class PopupClosedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + public PopupClosedEventArgs(EventArgs? closeEvent) + { + CloseEvent = closeEvent; + } + + /// + /// Gets the event that closed the popup, if any. + /// + /// + /// If is false, then this property will hold details of the + /// interaction that caused the popup to close if the close was caused by e.g. a pointer press + /// outside the popup. It can be used to mark the event as handled if the event should not + /// be propagated. + /// + public EventArgs? CloseEvent { get; } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index b4bf4c799a..b2e827fa26 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// public class Scene : IDisposable { - private Dictionary _index; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. @@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned scene. public Scene CloneScene() { - var index = new Dictionary(); + var index = new Dictionary(_index.Count); var root = Clone((VisualNode)Root, null, index); var result = new Scene(root, index, Layers.Clone(), Generation + 1) @@ -162,9 +162,18 @@ namespace Avalonia.Rendering.SceneGraph index.Add(result.Visual, result); - foreach (var child in source.Children) + int childCount = source.Children.Count; + + if (childCount > 0) { - result.AddChild(Clone((VisualNode)child, result, index)); + Span children = result.AddChildrenSpan(childCount); + + for (var i = 0; i < childCount; i++) + { + var child = source.Children[i]; + + children[i] = Clone((VisualNode)child, result, index); + } } return result; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs index 5960b4f560..25f7383a1a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs @@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph public class SceneLayers : IEnumerable { private readonly IVisual _root; - private readonly List _inner = new List(); - private readonly Dictionary _index = new Dictionary(); + private readonly List _inner; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. /// /// The scene's root visual. - public SceneLayers(IVisual root) + public SceneLayers(IVisual root) : this(root, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The scene's root visual. + /// Initial layer capacity. + public SceneLayers(IVisual root, int capacity) { _root = root; + + _inner = new List(capacity); + _index = new Dictionary(capacity); } /// @@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned layers. public SceneLayers Clone() { - var result = new SceneLayers(_root); + var result = new SceneLayers(_root, Count); foreach (var src in _inner) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 82444a0c29..8cd1a47795 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -19,8 +20,8 @@ namespace Avalonia.Rendering.SceneGraph private Rect? _bounds; private double _opacity; - private List _children; - private List> _drawOperations; + private PooledList _children; + private PooledList> _drawOperations; private IRef _drawOperationsRefCounter; private bool _drawOperationsCloned; private Matrix transformRestore; @@ -349,6 +350,18 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = transformRestore; } + /// + /// Inserts default constructed children into collection and returns a span for the newly created range. + /// + /// Count of children that will be added. + /// + internal Span AddChildrenSpan(int count) + { + EnsureChildrenCreated(count); + + return _children.AddSpan(count); + } + private Rect CalculateBounds() { var result = new Rect(); @@ -362,11 +375,11 @@ namespace Avalonia.Rendering.SceneGraph return result; } - private void EnsureChildrenCreated() + private void EnsureChildrenCreated(int capacity = 0) { if (_children == null) { - _children = new List(); + _children = new PooledList(capacity); } } @@ -377,13 +390,21 @@ namespace Avalonia.Rendering.SceneGraph { if (_drawOperations == null) { - _drawOperations = new List>(); + _drawOperations = new PooledList>(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; } else if (_drawOperationsCloned) { - _drawOperations = new List>(_drawOperations.Select(op => op.Clone())); + var oldDrawOperations = _drawOperations; + + _drawOperations = new PooledList>(oldDrawOperations.Count); + + foreach (var drawOperation in oldDrawOperations) + { + _drawOperations.Add(drawOperation.Clone()); + } + _drawOperationsRefCounter.Dispose(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; @@ -397,14 +418,16 @@ namespace Avalonia.Rendering.SceneGraph /// /// Draw operations that need to be disposed. /// Disposable for given draw operations. - private static IDisposable CreateDisposeDrawOperations(List> drawOperations) + private static IDisposable CreateDisposeDrawOperations(PooledList> drawOperations) { - return Disposable.Create(() => + return Disposable.Create(drawOperations, operations => { - foreach (var operation in drawOperations) + foreach (var operation in operations) { operation.Dispose(); } + + operations.Dispose(); }); } @@ -414,6 +437,8 @@ namespace Avalonia.Rendering.SceneGraph { _drawOperationsRefCounter?.Dispose(); + _children?.Dispose(); + Disposed = true; } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 1a2c93ac22..0b9c94f850 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -298,13 +298,6 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - Window PreparedWindow(object content = null) - { - var w = new Window {Content = content}; - w.ApplyTemplate(); - return w; - } - [Fact] public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() { @@ -351,6 +344,56 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void StaysOpen_False_Should_Not_Handle_Closing_Click() + { + using (CreateServices()) + { + var window = PreparedWindow(); + var target = new Popup() + { + PlacementTarget = window , + StaysOpen = false, + }; + + target.Open(); + + var e = CreatePointerPressedEventArgs(window); + window.RaiseEvent(e); + + Assert.False(e.Handled); + } + } + + [Fact] + public void Should_Pass_Closing_Click_To_Closed_Event() + { + using (CreateServices()) + { + var window = PreparedWindow(); + var target = new Popup() + { + PlacementTarget = window, + StaysOpen = false, + }; + + target.Open(); + + var press = CreatePointerPressedEventArgs(window); + var raised = 0; + + target.Closed += (s, e) => + { + Assert.Same(press, e.CloseEvent); + ++raised; + }; + + window.RaiseEvent(press); + + Assert.Equal(1, raised); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: @@ -363,6 +406,26 @@ namespace Avalonia.Controls.UnitTests.Primitives }))); } + private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source) + { + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + return new PointerPressedEventArgs( + source, + pointer, + source, + default, + 0, + new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), + KeyModifiers.None); + } + + private Window PreparedWindow(object content = null) + { + var w = new Window { Content = content }; + w.ApplyTemplate(); + return w; + } + private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope) { return new Popup