diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index 6b79f6f33b..13b9724778 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -19,6 +19,12 @@
baseline/netstandard2.0/Avalonia.Base.dll
target/netstandard2.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect})
+ baseline/netstandard2.0/Avalonia.Controls.dll
+ target/netstandard2.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.Screens.#ctor(Avalonia.Platform.IScreenImpl)
@@ -43,6 +49,12 @@
baseline/netstandard2.0/Avalonia.Controls.dll
target/netstandard2.0/Avalonia.Controls.dll
+
+ CP0006
+ M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Controls.Primitives.PopupPositioning.PopupPositionRequest)
+ baseline/netstandard2.0/Avalonia.Controls.dll
+ target/netstandard2.0/Avalonia.Controls.dll
+
CP0009
T:Avalonia.Controls.Screens
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml
index c0521cd3ba..64a005c987 100644
--- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml
+++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml
@@ -222,7 +222,15 @@
-
+
@@ -267,7 +275,6 @@
-
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
index 8944151385..5897eb2cde 100644
--- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
@@ -1,5 +1,8 @@
+using System;
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
@@ -71,5 +74,25 @@ namespace ControlCatalog.Pages
"Then attach the flyout where you want it:\n" +
"";
}
+
+ public void CustomPlacementCallback(CustomPopupPlacement placement)
+ {
+ var r = new Random().Next();
+ placement.Anchor = (r % 4) switch
+ {
+ 1 => PopupAnchor.Top,
+ 2 => PopupAnchor.Left,
+ 3 => PopupAnchor.Right,
+ _ => PopupAnchor.Bottom,
+ };
+ placement.Gravity = (r % 4) switch
+ {
+ 1 => PopupGravity.Top,
+ 2 => PopupGravity.Left,
+ 3 => PopupGravity.Right,
+ _ => PopupGravity.Bottom,
+ };
+ placement.Offset = new Point(r % 20, r % 20);
+ }
}
}
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml
index 8aaed903f3..8b76cccbc9 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml
@@ -5,31 +5,24 @@
Spacing="4">
A control which pops up a hint when a control is hovered
-
+
-
+
+
Hover Here
-
ToolTip bottom placement
-
+ ToolTip custom placement
+
+ Moving offset
-
-
Nested ToolTips
-
-
ToolTip replaced on the fly
-
+
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs b/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
index 0e8bf3a181..7efdb99078 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
@@ -1,5 +1,8 @@
+using System;
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
@@ -19,6 +22,27 @@ namespace ControlCatalog.Pages
private void ToolTipOpening(object? sender, CancelRoutedEventArgs args)
{
((Control)args.Source!).SetValue(ToolTip.TipProperty, "New tip set from ToolTipOpening.");
- }
+ }
+
+ public void CustomPlacementCallback(CustomPopupPlacement placement)
+ {
+ var r = new Random().Next();
+
+ placement.Anchor = (r % 4) switch
+ {
+ 1 => PopupAnchor.Top,
+ 2 => PopupAnchor.Left,
+ 3 => PopupAnchor.Right,
+ _ => PopupAnchor.Bottom,
+ };
+ placement.Gravity = (r % 4) switch
+ {
+ 1 => PopupGravity.Top,
+ 2 => PopupGravity.Left,
+ 3 => PopupGravity.Right,
+ _ => PopupGravity.Bottom,
+ };
+ placement.Offset = new Point(r % 20, r % 20);
+ }
}
}
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index cb64cc27e6..940528bf6f 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -82,6 +82,10 @@ namespace Avalonia.Controls
public static readonly StyledProperty PlacementTargetProperty =
Popup.PlacementTargetProperty.AddOwner();
+ ///
+ public static readonly StyledProperty CustomPopupPlacementCallbackProperty =
+ Popup.CustomPopupPlacementCallbackProperty.AddOwner();
+
private Popup? _popup;
private List? _attachedControls;
private IInputElement? _previousFocus;
@@ -185,6 +189,13 @@ namespace Avalonia.Controls
set => SetValue(PlacementTargetProperty, value);
}
+ ///
+ public CustomPopupPlacementCallback? CustomPopupPlacementCallback
+ {
+ get => GetValue(CustomPopupPlacementCallbackProperty);
+ set => SetValue(CustomPopupPlacementCallbackProperty, value);
+ }
+
///
/// Occurs when the value of the
///
@@ -340,6 +351,7 @@ namespace Avalonia.Controls
_popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
_popup.PlacementGravity = PlacementGravity;
_popup.PlacementRect = PlacementRect;
+ _popup.CustomPopupPlacementCallback = CustomPopupPlacementCallback;
_popup.WindowManagerAddShadowHint = WindowManagerAddShadowHint;
IsOpen = true;
_popup.IsOpen = true;
diff --git a/src/Avalonia.Controls/ContextRequestedEventArgs.cs b/src/Avalonia.Controls/ContextRequestedEventArgs.cs
index ad5ffef267..fa0d5f9855 100644
--- a/src/Avalonia.Controls/ContextRequestedEventArgs.cs
+++ b/src/Avalonia.Controls/ContextRequestedEventArgs.cs
@@ -26,6 +26,13 @@ namespace Avalonia.Controls
_pointerEventArgs = pointerEventArgs;
}
+ ///
+ public ContextRequestedEventArgs(ContextRequestedEventArgs contextRequestedEventArgs)
+ : this()
+ {
+ _pointerEventArgs = contextRequestedEventArgs._pointerEventArgs;
+ }
+
///
/// Gets the x- and y-coordinates of the pointer position, optionally evaluated against a coordinate origin of a supplied .
///
diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
index 0c72d67154..82a243cd72 100644
--- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
+++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
@@ -30,11 +30,15 @@ namespace Avalonia.Controls.Primitives
///
public static readonly StyledProperty PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner();
-
+
///
public static readonly StyledProperty PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner();
+ ///
+ public static readonly StyledProperty CustomPopupPlacementCallbackProperty =
+ Popup.CustomPopupPlacementCallbackProperty.AddOwner();
+
///
/// Defines the property
///
@@ -112,6 +116,13 @@ namespace Avalonia.Controls.Primitives
set => SetValue(VerticalOffsetProperty, value);
}
+ ///
+ public CustomPopupPlacementCallback? CustomPopupPlacementCallback
+ {
+ get => GetValue(CustomPopupPlacementCallbackProperty);
+ set => SetValue(CustomPopupPlacementCallbackProperty, value);
+ }
+
///
/// Gets or sets the desired ShowMode
///
@@ -445,6 +456,7 @@ namespace Avalonia.Controls.Primitives
Popup.HorizontalOffset = HorizontalOffset;
Popup.PlacementAnchor = PlacementAnchor;
Popup.PlacementGravity = PlacementGravity;
+ Popup.CustomPopupPlacementCallback = CustomPopupPlacementCallback;
if (showAtPointer)
{
Popup.Placement = PlacementMode.Pointer;
diff --git a/src/Avalonia.Controls/PlacementMode.cs b/src/Avalonia.Controls/PlacementMode.cs
index 20e4e5470d..93116d8f1a 100644
--- a/src/Avalonia.Controls/PlacementMode.cs
+++ b/src/Avalonia.Controls/PlacementMode.cs
@@ -81,6 +81,11 @@ namespace Avalonia.Controls
///
/// Preferred location is to the right of the target element, with the bottom edge of popup aligned with bottom edge of the target element.
///
- RightEdgeAlignedBottom
+ RightEdgeAlignedBottom,
+
+ ///
+ /// A position and repositioning behavior that is defined by the property.
+ ///
+ Custom
}
}
diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs
index 0aad838b0f..4cb259db5f 100644
--- a/src/Avalonia.Controls/Primitives/IPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs
@@ -1,6 +1,7 @@
using System;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Metadata;
@@ -17,6 +18,7 @@ namespace Avalonia.Controls.Primitives
/// on an .
///
[NotClientImplementable]
+ [Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
public interface IPopupHost : IDisposable, IFocusScope
{
///
@@ -79,20 +81,7 @@ namespace Avalonia.Controls.Primitives
/// Configures the position of the popup according to a target control and a set of
/// placement parameters.
///
- /// The placement target.
- /// The placement mode.
- /// The offset, in device-independent pixels.
- /// The anchor point.
- /// The popup gravity.
- /// Defines how a popup position will be adjusted if the unadjusted position would result in the popup being partly constrained.
- ///
- /// The anchor rect. If null, the bounds of will be used.
- ///
- void ConfigurePosition(Visual target, PlacementMode placement, Point offset,
- PopupAnchor anchor = PopupAnchor.None,
- PopupGravity gravity = PopupGravity.None,
- PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
- Rect? rect = null);
+ void ConfigurePosition(PopupPositionRequest positionRequest);
///
/// Sets the control to display in the popup.
diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
index 43bb9b2947..3a602c15b7 100644
--- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Diagnostics;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Metadata;
-using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
@@ -19,9 +19,10 @@ namespace Avalonia.Controls.Primitives
private readonly OverlayLayer _overlayLayer;
private readonly ManagedPopupPositioner _positioner;
- private PopupPositionerParameters _positionerParameters;
private Point _lastRequestedPosition;
- private bool _shown;
+ private PopupPositionRequest? _popupPositionRequest;
+ private Size _popupSize;
+ private bool _shown, _needsUpdate;
public OverlayPopupHost(OverlayLayer overlayLayer)
{
@@ -73,36 +74,42 @@ namespace Avalonia.Controls.Primitives
}
///
+ [Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
public void ConfigurePosition(Visual target, PlacementMode placement, Point offset,
PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null)
{
- _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot()!, target, placement, offset, anchor,
- gravity, constraintAdjustment, rect, FlowDirection);
+ ((IPopupHost)this).ConfigurePosition(new PopupPositionRequest(target, placement, offset, anchor, gravity,
+ constraintAdjustment, rect, null));
+ }
+
+ ///
+ void IPopupHost.ConfigurePosition(PopupPositionRequest positionRequest)
+ {
+ _popupPositionRequest = positionRequest;
+ _needsUpdate = true;
UpdatePosition();
}
///
protected override Size ArrangeOverride(Size finalSize)
{
- if (_positionerParameters.Size != finalSize)
+ if (_popupSize != finalSize)
{
- _positionerParameters.Size = finalSize;
+ _popupSize = finalSize;
+ _needsUpdate = true;
UpdatePosition();
}
return base.ArrangeOverride(finalSize);
}
-
private void UpdatePosition()
{
- // Don't bother the positioner with layout system artifacts
- if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0)
- return;
- if (_shown)
+ if (_needsUpdate && _popupPositionRequest is not null)
{
- _positioner.Update(_positionerParameters);
+ _needsUpdate = false;
+ _positioner.Update(TopLevel.GetTopLevel(_overlayLayer)!, _popupPositionRequest, _popupSize, FlowDirection);
}
}
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 4c726e9183..28e2a51e06 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -22,6 +22,9 @@ namespace Avalonia.Controls.Primitives
///
public class Popup : Control, IPopupHostProvider
{
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty WindowManagerAddShadowHintProperty =
AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false);
@@ -89,9 +92,21 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty PlacementTargetProperty =
AvaloniaProperty.Register(nameof(PlacementTarget));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty CustomPopupPlacementCallbackProperty =
+ AvaloniaProperty.Register(nameof(CustomPopupPlacementCallback));
+
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty OverlayDismissEventPassThroughProperty =
AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough));
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty OverlayInputPassThroughElementProperty =
AvaloniaProperty.Register(nameof(OverlayInputPassThroughElement));
@@ -287,6 +302,15 @@ namespace Avalonia.Controls.Primitives
set => SetValue(PlacementTargetProperty, value);
}
+ ///
+ /// Gets or sets a delegate handler method that positions the Popup control, when is set to .
+ ///
+ public CustomPopupPlacementCallback? CustomPopupPlacementCallback
+ {
+ get => GetValue(CustomPopupPlacementCallbackProperty);
+ set => SetValue(CustomPopupPlacementCallbackProperty, value);
+ }
+
///
/// Gets or sets a value indicating whether the event that closes the popup is passed
/// through to the parent window.
@@ -603,14 +627,15 @@ namespace Avalonia.Controls.Primitives
private void UpdateHostPosition(IPopupHost popupHost, Control placementTarget)
{
- popupHost.ConfigurePosition(
+ popupHost.ConfigurePosition(new PopupPositionRequest(
placementTarget,
Placement,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
- PlacementRect ?? new Rect(default, placementTarget.Bounds.Size));
+ PlacementRect ?? new Rect(default, placementTarget.Bounds.Size),
+ CustomPopupPlacementCallback));
}
private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, Control placementTarget)
@@ -651,14 +676,15 @@ namespace Avalonia.Controls.Primitives
var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType();
if (placementTarget == null)
return;
- _openState.PopupHost.ConfigurePosition(
+ _openState.PopupHost.ConfigurePosition(new PopupPositionRequest(
placementTarget,
Placement,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
- PlacementRect);
+ PlacementRect,
+ CustomPopupPlacementCallback));
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/CustomPopupPlacement.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/CustomPopupPlacement.cs
new file mode 100644
index 0000000000..8be812b4ea
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/CustomPopupPlacement.cs
@@ -0,0 +1,57 @@
+namespace Avalonia.Controls.Primitives.PopupPositioning;
+
+///
+/// Defines custom placement parameters for a callback.
+///
+public record CustomPopupPlacement
+{
+ private PopupGravity _gravity;
+ private PopupAnchor _anchor;
+
+ internal CustomPopupPlacement(Size popupSize, Visual target)
+ {
+ PopupSize = popupSize;
+ Target = target;
+ }
+
+ ///
+ /// The of the control.
+ ///
+ public Size PopupSize { get; }
+
+ ///
+ /// Placement target of the popup.
+ ///
+ public Visual Target { get; }
+
+ ///
+ public Rect AnchorRectangle { get; set; }
+
+ ///
+ public PopupAnchor Anchor
+ {
+ get => _anchor;
+ set
+ {
+ PopupPositioningEdgeHelper.ValidateEdge(value);
+ _anchor = value;
+ }
+ }
+
+ ///
+ public PopupGravity Gravity
+ {
+ get => _gravity;
+ set
+ {
+ PopupPositioningEdgeHelper.ValidateGravity(value);
+ _gravity = value;
+ }
+ }
+
+ ///
+ public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
+
+ ///
+ public Point Offset { get; set; }
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/CustomPopupPlacementCallback.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/CustomPopupPlacementCallback.cs
new file mode 100644
index 0000000000..dbe90d8d32
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/CustomPopupPlacementCallback.cs
@@ -0,0 +1,6 @@
+namespace Avalonia.Controls.Primitives.PopupPositioning;
+
+///
+/// Represents a method that provides custom positioning for a control.
+///
+public delegate void CustomPopupPlacementCallback(CustomPopupPlacement parameters);
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
index 31c1e6054a..a0b853f2dc 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
@@ -45,6 +45,9 @@ Copyright © 2019 Nikita Tsukanov
*/
using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.VisualTree;
@@ -63,7 +66,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
/// requirement that a popup must intersect with or be at least partially adjacent to its parent
/// surface.
///
- [Unstable]
+ [Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
public record struct PopupPositionerParameters
{
private PopupGravity _gravity;
@@ -443,19 +446,35 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
void Update(PopupPositionerParameters parameters);
}
- [Unstable]
- static class PopupPositionerExtensions
+ internal static class PopupPositionerExtensions
{
- public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
+ public static void Update(
+ this IPopupPositioner positioner,
TopLevel topLevel,
- Visual target, PlacementMode placement, Point offset,
- PopupAnchor anchor, PopupGravity gravity,
- PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect,
+ PopupPositionRequest positionRequest,
+ Size popupSize,
FlowDirection flowDirection)
{
- positionerParameters.Offset = offset;
- positionerParameters.ConstraintAdjustment = constraintAdjustment;
- if (placement == PlacementMode.Pointer)
+ if (popupSize == default)
+ {
+ return;
+ }
+
+ var parameters = BuildParameters(topLevel, positionRequest, popupSize, flowDirection);
+ positioner.Update(parameters);
+ }
+
+ private static PopupPositionerParameters BuildParameters(
+ TopLevel topLevel,
+ PopupPositionRequest positionRequest,
+ Size popupSize,
+ FlowDirection flowDirection)
+ {
+ PopupPositionerParameters positionerParameters = default;
+ positionerParameters.Offset = positionRequest.Offset;
+ positionerParameters.Size = popupSize;
+ positionerParameters.ConstraintAdjustment = positionRequest.ConstraintAdjustment;
+ if (positionRequest.Placement == PlacementMode.Pointer)
{
// We need a better way for tracking the last pointer position
var position = topLevel.PointToClient(topLevel.LastPointerPosition ?? default);
@@ -464,39 +483,45 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
- else
+ else if (positionRequest.Placement == PlacementMode.Custom)
{
- if (target == null)
- throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
- Matrix? matrix;
- if (TryGetAdorner(target, out var adorned, out var adornerLayer))
- {
- matrix = adorned!.TransformToVisual(topLevel) * target.TransformToVisual(adornerLayer!);
- }
- else
- {
- matrix = target.TransformToVisual(topLevel);
- }
+ if (positionRequest.PlacementCallback is null)
+ throw new InvalidOperationException(
+ "CustomPopupPlacementCallback property must be set, when Placement=PlacementMode.Custom");
- if (matrix == null)
+ positionerParameters.AnchorRectangle = CalculateAnchorRect(topLevel, positionRequest);
+
+ var customPlacementParameters = new CustomPopupPlacement(
+ popupSize,
+ positionRequest.Target)
{
- if (target.GetVisualRoot() == null)
- throw new InvalidOperationException("Target control is not attached to the visual tree");
- throw new InvalidOperationException("Target control is not in the same tree as the popup parent");
- }
+ AnchorRectangle = positionerParameters.AnchorRectangle,
+ Anchor = positionerParameters.Anchor,
+ Gravity = positionerParameters.Gravity,
+ ConstraintAdjustment = positionerParameters.ConstraintAdjustment,
+ Offset = positionerParameters.Offset
+ };
- var bounds = new Rect(default, target.Bounds.Size);
- var anchorRect = rect ?? bounds;
- positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
+ positionRequest.PlacementCallback.Invoke(customPlacementParameters);
- var parameters = placement switch
+ positionerParameters.AnchorRectangle = customPlacementParameters.AnchorRectangle;
+ positionerParameters.Anchor = customPlacementParameters.Anchor;
+ positionerParameters.Gravity = customPlacementParameters.Gravity;
+ positionerParameters.ConstraintAdjustment = customPlacementParameters.ConstraintAdjustment;
+ positionerParameters.Offset = customPlacementParameters.Offset;
+ }
+ else
+ {
+ positionerParameters.AnchorRectangle = CalculateAnchorRect(topLevel, positionRequest);
+
+ var parameters = positionRequest.Placement switch
{
PlacementMode.Bottom => (PopupAnchor.Bottom, PopupGravity.Bottom),
PlacementMode.Right => (PopupAnchor.Right, PopupGravity.Right),
PlacementMode.Left => (PopupAnchor.Left, PopupGravity.Left),
PlacementMode.Top => (PopupAnchor.Top, PopupGravity.Top),
PlacementMode.Center => (PopupAnchor.None, PopupGravity.None),
- PlacementMode.AnchorAndGravity => (anchor, gravity),
+ PlacementMode.AnchorAndGravity => (positionRequest.Anchor, positionRequest.Gravity),
PlacementMode.TopEdgeAlignedRight => (PopupAnchor.TopRight, PopupGravity.TopLeft),
PlacementMode.TopEdgeAlignedLeft => (PopupAnchor.TopLeft, PopupGravity.TopRight),
PlacementMode.BottomEdgeAlignedLeft => (PopupAnchor.BottomLeft, PopupGravity.BottomRight),
@@ -505,7 +530,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
PlacementMode.LeftEdgeAlignedBottom => (PopupAnchor.BottomLeft, PopupGravity.TopLeft),
PlacementMode.RightEdgeAlignedTop => (PopupAnchor.TopRight, PopupGravity.BottomRight),
PlacementMode.RightEdgeAlignedBottom => (PopupAnchor.BottomRight, PopupGravity.TopRight),
- _ => throw new ArgumentOutOfRangeException(nameof(placement), placement,
+ _ => throw new ArgumentOutOfRangeException(nameof(positionRequest.Placement), positionRequest.Placement,
"Invalid value for Popup.PlacementMode")
};
positionerParameters.Anchor = parameters.Item1;
@@ -537,6 +562,35 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
positionerParameters.Gravity |= PopupGravity.Right;
}
}
+
+ return positionerParameters;
+ }
+
+ private static Rect CalculateAnchorRect(TopLevel topLevel, PopupPositionRequest positionRequest)
+ {
+ var target = positionRequest.Target;
+ if (target == null)
+ throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
+ Matrix? matrix;
+ if (TryGetAdorner(target, out var adorned, out var adornerLayer))
+ {
+ matrix = adorned!.TransformToVisual(topLevel) * target.TransformToVisual(adornerLayer!);
+ }
+ else
+ {
+ matrix = target.TransformToVisual(topLevel);
+ }
+
+ if (matrix == null)
+ {
+ if (target.GetVisualRoot() == null)
+ throw new InvalidOperationException("Target control is not attached to the visual tree");
+ throw new InvalidOperationException("Target control is not in the same tree as the popup parent");
+ }
+
+ var bounds = new Rect(default, target.Bounds.Size);
+ var anchorRect = positionRequest.AnchorRect ?? bounds;
+ return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
}
private static bool TryGetAdorner(Visual target, out Visual? adorned, out Visual? adornerLayer)
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/PopupPositionRequest.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/PopupPositionRequest.cs
new file mode 100644
index 0000000000..35e15e3057
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/PopupPositionRequest.cs
@@ -0,0 +1,35 @@
+using Avalonia.Diagnostics;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Primitives.PopupPositioning;
+
+[PrivateApi]
+[Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
+public class PopupPositionRequest
+{
+ internal PopupPositionRequest(Visual target, PlacementMode placement)
+ {
+ Target = target;
+ Placement = placement;
+ }
+
+ internal PopupPositionRequest(Visual target, PlacementMode placement, Point offset, PopupAnchor anchor, PopupGravity gravity, PopupPositionerConstraintAdjustment constraintAdjustment, Rect? anchorRect, CustomPopupPlacementCallback? placementCallback)
+ : this(target, placement)
+ {
+ Offset = offset;
+ Anchor = anchor;
+ Gravity = gravity;
+ ConstraintAdjustment = constraintAdjustment;
+ AnchorRect = anchorRect;
+ PlacementCallback = placementCallback;
+ }
+
+ public Visual Target { get; }
+ public PlacementMode Placement {get;}
+ public Point Offset {get;}
+ public PopupAnchor Anchor {get;}
+ public PopupGravity Gravity {get;}
+ public PopupPositionerConstraintAdjustment ConstraintAdjustment {get;}
+ public Rect? AnchorRect {get;}
+ public CustomPopupPlacementCallback? PlacementCallback {get;}
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index 723bdca1b2..b2905b9176 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -1,8 +1,10 @@
using System;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Diagnostics;
using Avalonia.Interactivity;
using Avalonia.Media;
+using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.VisualTree;
@@ -26,7 +28,9 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty WindowManagerAddShadowHintProperty =
Popup.WindowManagerAddShadowHintProperty.AddOwner();
- private PopupPositionerParameters _positionerParameters;
+ private PopupPositionRequest? _popupPositionRequest;
+ private Size _popupSize;
+ private bool _needsUpdate;
///
/// Initializes static members of the class.
@@ -124,20 +128,30 @@ namespace Avalonia.Controls.Primitives
private void UpdatePosition()
{
- PlatformImpl?.PopupPositioner?.Update(_positionerParameters);
+ if (_needsUpdate && _popupPositionRequest is not null)
+ {
+ _needsUpdate = false;
+ PlatformImpl?.PopupPositioner?
+ .Update(ParentTopLevel, _popupPositionRequest, _popupSize, FlowDirection);
+ }
}
+ [Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
public void ConfigurePosition(Visual target, PlacementMode placement, Point offset,
PopupAnchor anchor = PopupAnchor.None,
PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null)
{
- _positionerParameters.ConfigurePosition(ParentTopLevel, target,
- placement, offset, anchor, gravity, constraintAdjustment, rect, FlowDirection);
+ ((IPopupHost)this).ConfigurePosition(new PopupPositionRequest(target, placement, offset, anchor, gravity,
+ constraintAdjustment, rect, null));
+ }
- if (_positionerParameters.Size != default)
- UpdatePosition();
+ void IPopupHost.ConfigurePosition(PopupPositionRequest request)
+ {
+ _popupPositionRequest = request;
+ _needsUpdate = true;
+ UpdatePosition();
}
public void SetChild(Control? control) => Content = control;
@@ -184,10 +198,15 @@ namespace Avalonia.Controls.Primitives
return new Size(width, height);
}
- protected override sealed Size ArrangeSetBounds(Size size)
+ protected sealed override Size ArrangeSetBounds(Size size)
{
- _positionerParameters.Size = size;
- UpdatePosition();
+ if (_popupSize != size)
+ {
+ _popupSize = size;
+ _needsUpdate = true;
+ UpdatePosition();
+ }
+
return ClientSize;
}
diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs
index 8bf0adedee..fc8da13131 100644
--- a/src/Avalonia.Controls/ToolTip.cs
+++ b/src/Avalonia.Controls/ToolTip.cs
@@ -4,6 +4,7 @@ using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Reactive;
using Avalonia.Styling;
@@ -51,6 +52,10 @@ namespace Avalonia.Controls
public static readonly AttachedProperty VerticalOffsetProperty =
AvaloniaProperty.RegisterAttached("VerticalOffset", 20);
+ ///
+ public static readonly AttachedProperty CustomPopupPlacementCallbackProperty =
+ AvaloniaProperty.RegisterAttached("CustomPopupPlacementCallback");
+
///
/// Defines the ToolTip.ShowDelay property.
///
@@ -327,6 +332,22 @@ namespace Avalonia.Controls
public static void RemoveToolTipClosingHandler(Control element, EventHandler handler) =>
element.RemoveHandler(ToolTipClosingEvent, handler);
+ ///
+ /// Gets the value of the ToolTip.CustomPopupPlacementCallback attached property.
+ ///
+ public static CustomPopupPlacementCallback? GetCustomPopupPlacementCallback(Control element)
+ {
+ return element.GetValue(CustomPopupPlacementCallbackProperty);
+ }
+
+ ///
+ /// Sets the value of the ToolTip.CustomPopupPlacementCallback attached property.
+ ///
+ public static void SetCustomPopupPlacementCallback(Control element, CustomPopupPlacementCallback? value)
+ {
+ element.SetValue(CustomPopupPlacementCallbackProperty, value);
+ }
+
private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
@@ -397,7 +418,8 @@ namespace Avalonia.Controls
{
_popup.Bind(Popup.HorizontalOffsetProperty, control.GetBindingObservable(HorizontalOffsetProperty)),
_popup.Bind(Popup.VerticalOffsetProperty, control.GetBindingObservable(VerticalOffsetProperty)),
- _popup.Bind(Popup.PlacementProperty, control.GetBindingObservable(PlacementProperty))
+ _popup.Bind(Popup.PlacementProperty, control.GetBindingObservable(PlacementProperty)),
+ _popup.Bind(Popup.CustomPopupPlacementCallbackProperty, control.GetBindingObservable(CustomPopupPlacementCallbackProperty))
});
_popup.PlacementTarget = control;
@@ -415,14 +437,14 @@ namespace Avalonia.Controls
adornedControl.RaiseEvent(args);
}
- _subscriptions?.Dispose();
-
if (_popup is not null)
{
_popup.IsOpen = false;
_popup.SetPopupParent(null);
_popup.PlacementTarget = null;
}
+
+ _subscriptions?.Dispose();
}
private void OnPopupClosed(object? sender, EventArgs e)
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
index fb88c98343..38bbc875c5 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
@@ -262,6 +262,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
var target = CreateTarget(window, popupImpl.Object);
target.Content = child;
+ ((IPopupHost)target).ConfigurePosition(new PopupPositionRequest(window, PlacementMode.Top));
target.Show();
Assert.Equal(new Size(400, 1024), target.Bounds.Size);
@@ -290,6 +291,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Height = 100,
};
+ ((IPopupHost)target).ConfigurePosition(new PopupPositionRequest(window, PlacementMode.Top));
target.Show();
Assert.Equal(new Rect(0, 0, 400, 800), target.Bounds);
@@ -313,6 +315,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Width = 400;
target.Height = 800;
+ ((IPopupHost)target).ConfigurePosition(new PopupPositionRequest(window, PlacementMode.Top));
target.Show();
Assert.Equal(400, target.Width);
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
index e1ff48a3cc..1cad1304af 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
@@ -1206,6 +1206,50 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
+ [Fact]
+ public void Custom_Placement_Callback_Is_Executed()
+ {
+ using (CreateServices())
+ {
+ var callbackExecuted = 0;
+ var popupContent = new Border { Width = 100, Height = 100 };
+ var popup = new Popup
+ {
+ Child = popupContent,
+ Placement = PlacementMode.Custom,
+ HorizontalOffset = 42,
+ VerticalOffset = 21
+ };
+ var popupParent = new Border { Child = popup };
+ var root = PreparedWindow(popupParent);
+
+ popup.CustomPopupPlacementCallback = (parameters) =>
+ {
+ Assert.Equal(popupContent.Width, parameters.PopupSize.Width);
+ Assert.Equal(popupContent.Height, parameters.PopupSize.Height);
+
+ Assert.Equal(root.Width, parameters.AnchorRectangle.Width);
+ Assert.Equal(root.Height, parameters.AnchorRectangle.Height);
+
+ Assert.Equal(popup.HorizontalOffset, parameters.Offset.X);
+ Assert.Equal(popup.VerticalOffset, parameters.Offset.Y);
+
+ callbackExecuted++;
+
+ parameters.Anchor = PopupAnchor.Top;
+ parameters.Gravity = PopupGravity.Bottom;
+ };
+
+ root.LayoutManager.ExecuteInitialLayoutPass();
+ popup.Open();
+ root.LayoutManager.ExecuteLayoutPass();
+
+ // Ideally, callback should be executed only once for this test.
+ // But currently PlacementTargetLayoutUpdated triggers second update either way.
+ Assert.Equal(2, callbackExecuted);
+ }
+ }
+
private static PopupRoot CreateRoot(TopLevel popupParent, IPopupImpl impl = null)
{