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