diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs
index bb4daf38e1..18fb45fd5a 100644
--- a/src/Avalonia.Controls/Primitives/IPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs
@@ -45,10 +45,14 @@ namespace Avalonia.Controls.Primitives
/// The placement mode.
/// The offset, in device-independent pixels.
/// The anchor point.
- /// The anchor gravity.
+ /// The popup gravity.
+ ///
+ /// The anchor rect. If null, the bounds of will be used.
+ ///
void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
- PopupPositioningEdge anchor = PopupPositioningEdge.None,
- PopupPositioningEdge gravity = PopupPositioningEdge.None);
+ PopupAnchor anchor = PopupAnchor.None,
+ PopupGravity gravity = PopupGravity.None,
+ Rect? rect = null);
///
/// Shows the popup.
diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
index 3dc9d302db..15b09f29f1 100644
--- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
@@ -71,10 +71,11 @@ namespace Avalonia.Controls.Primitives
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
- PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None)
+ PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None,
+ Rect? rect = null)
{
_positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor,
- gravity);
+ gravity, rect);
UpdatePosition();
}
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 66f2153b6c..ff1201286c 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
@@ -34,12 +35,36 @@ namespace Avalonia.Controls.Primitives
o => o.IsOpen,
(o, v) => o.IsOpen = v);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementAnchorProperty =
+ AvaloniaProperty.Register(nameof(PlacementAnchor));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementGravityProperty =
+ AvaloniaProperty.Register(nameof(PlacementGravity));
+
///
/// Defines the property.
///
public static readonly StyledProperty PlacementModeProperty =
AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementRectProperty =
+ AvaloniaProperty.Register(nameof(PlacementRect));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementTargetProperty =
+ AvaloniaProperty.Register(nameof(PlacementTarget));
+
#pragma warning disable 618
///
/// Defines the property.
@@ -60,12 +85,6 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty VerticalOffsetProperty =
AvaloniaProperty.Register(nameof(VerticalOffset));
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty PlacementTargetProperty =
- AvaloniaProperty.Register(nameof(PlacementTarget));
-
///
/// Defines the property.
///
@@ -136,6 +155,26 @@ namespace Avalonia.Controls.Primitives
set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
+ ///
+ /// Gets or sets the anchor point on the when
+ /// is .
+ ///
+ public PopupAnchor PlacementAnchor
+ {
+ get { return GetValue(PlacementAnchorProperty); }
+ set { SetValue(PlacementAnchorProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value which defines in what direction the popup should open
+ /// when is .
+ ///
+ public PopupGravity PlacementGravity
+ {
+ get { return GetValue(PlacementGravityProperty); }
+ set { SetValue(PlacementGravityProperty, value); }
+ }
+
///
/// Gets or sets the placement mode of the popup in relation to the .
///
@@ -145,6 +184,32 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
+ ///
+ /// Gets or sets the the anchor rectangle within the parent that the popup will be placed
+ /// relative to when is .
+ ///
+ ///
+ /// The placement rect defines a rectangle relative to around
+ /// which the popup will be opened, with determining which edge
+ /// of the placement target is used.
+ ///
+ /// If unset, the anchor rectangle will be the bounds of the .
+ ///
+ public Rect? PlacementRect
+ {
+ get { return GetValue(PlacementRectProperty); }
+ set { SetValue(PlacementRectProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the control that is used to determine the popup's position.
+ ///
+ public Control? PlacementTarget
+ {
+ get { return GetValue(PlacementTargetProperty); }
+ set { SetValue(PlacementTargetProperty, value); }
+ }
+
[Obsolete("This property has no effect")]
public bool ObeyScreenEdges
{
@@ -153,7 +218,7 @@ namespace Avalonia.Controls.Primitives
}
///
- /// Gets or sets the Horizontal offset of the popup in relation to the
+ /// Gets or sets the Horizontal offset of the popup in relation to the .
///
public double HorizontalOffset
{
@@ -162,7 +227,7 @@ namespace Avalonia.Controls.Primitives
}
///
- /// Gets or sets the Vertical offset of the popup in relation to the
+ /// Gets or sets the Vertical offset of the popup in relation to the .
///
public double VerticalOffset
{
@@ -170,15 +235,6 @@ namespace Avalonia.Controls.Primitives
set { SetValue(VerticalOffsetProperty, value); }
}
- ///
- /// Gets or sets the control that is used to determine the popup's position.
- ///
- public Control? PlacementTarget
- {
- get { return GetValue(PlacementTargetProperty); }
- set { SetValue(PlacementTargetProperty, value); }
- }
-
///
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
@@ -251,8 +307,11 @@ namespace Avalonia.Controls.Primitives
popupHost.ConfigurePosition(
placementTarget,
- PlacementMode,
- new Point(HorizontalOffset, VerticalOffset));
+ PlacementMode,
+ new Point(HorizontalOffset, VerticalOffset),
+ PlacementAnchor,
+ PlacementGravity,
+ PlacementRect);
DeferCleanup(SubscribeToEventHandler>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
index d2a403b602..d6e54b80c1 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
@@ -62,8 +62,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
///
public struct PopupPositionerParameters
{
- private PopupPositioningEdge _gravity;
- private PopupPositioningEdge _anchor;
+ private PopupGravity _gravity;
+ private PopupAnchor _anchor;
///
/// Set the size of the popup that is to be positioned with the positioner object, in device-
@@ -91,7 +91,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
/// centered on the specified edge, or in the center of the anchor rectangle if no edge is
/// specified.
///
- public PopupPositioningEdge Anchor
+ public PopupAnchor Anchor
{
get => _anchor;
set
@@ -110,12 +110,12 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
/// be placed towards the specified gravity; otherwise, the popup will be centered over the
/// anchor point on any axis that had no gravity specified.
///
- public PopupPositioningEdge Gravity
+ public PopupGravity Gravity
{
get => _gravity;
set
{
- PopupPositioningEdgeHelper.ValidateEdge(value);
+ PopupPositioningEdgeHelper.ValidateGravity(value);
_gravity = value;
}
}
@@ -235,18 +235,23 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
static class PopupPositioningEdgeHelper
{
- public static void ValidateEdge(this PopupPositioningEdge edge)
+ public static void ValidateEdge(this PopupAnchor edge)
{
- if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0)
+ if (((edge & PopupAnchor.Left) != 0 && (edge & PopupAnchor.Right) != 0)
||
- ((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0))
+ ((edge & PopupAnchor.Top) != 0 && (edge & PopupAnchor.Bottom) != 0))
throw new ArgumentException("Opposite edges specified");
}
- public static PopupPositioningEdge Flip(this PopupPositioningEdge edge)
+ public static void ValidateGravity(this PopupGravity gravity)
+ {
+ ValidateEdge((PopupAnchor)gravity);
+ }
+
+ public static PopupAnchor Flip(this PopupAnchor edge)
{
- var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right;
- var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom;
+ var hmask = PopupAnchor.Left | PopupAnchor.Right;
+ var vmask = PopupAnchor.Top | PopupAnchor.Bottom;
if ((edge & hmask) != 0)
edge ^= hmask;
if ((edge & vmask) != 0)
@@ -254,28 +259,36 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
return edge;
}
- public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge)
+ public static PopupAnchor FlipX(this PopupAnchor edge)
{
- if ((edge & PopupPositioningEdge.HorizontalMask) != 0)
- edge ^= PopupPositioningEdge.HorizontalMask;
+ if ((edge & PopupAnchor.HorizontalMask) != 0)
+ edge ^= PopupAnchor.HorizontalMask;
return edge;
}
- public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge)
+ public static PopupAnchor FlipY(this PopupAnchor edge)
{
- if ((edge & PopupPositioningEdge.VerticalMask) != 0)
- edge ^= PopupPositioningEdge.VerticalMask;
+ if ((edge & PopupAnchor.VerticalMask) != 0)
+ edge ^= PopupAnchor.VerticalMask;
return edge;
}
-
+
+ public static PopupGravity FlipX(this PopupGravity gravity)
+ {
+ return (PopupGravity)FlipX((PopupAnchor)gravity);
+ }
+
+ public static PopupGravity FlipY(this PopupGravity gravity)
+ {
+ return (PopupGravity)FlipY((PopupAnchor)gravity);
+ }
}
///
- /// Defines the popup position edge for and
- /// .
+ /// Defines the edges around an anchor rectangle on which a popup will open.
///
[Flags]
- public enum PopupPositioningEdge
+ public enum PopupAnchor
{
///
/// The center of the anchor rectangle.
@@ -338,6 +351,58 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
AllMask = VerticalMask|HorizontalMask
}
+ ///
+ /// Defines the direction in which a popup will open.
+ ///
+ [Flags]
+ public enum PopupGravity
+ {
+ ///
+ /// The popup will be centered over the anchor edge.
+ ///
+ None,
+
+ ///
+ /// The popup will be positioned above the anchor edge
+ ///
+ Top = 1,
+
+ ///
+ /// The popup will be positioned below the anchor edge
+ ///
+ Bottom = 2,
+
+ ///
+ /// The popup will be positioned to the left of the anchor edge
+ ///
+ Left = 4,
+
+ ///
+ /// The popup will be positioned to the right of the anchor edge
+ ///
+ Right = 8,
+
+ ///
+ /// The popup will be positioned to the top-left of the anchor edge
+ ///
+ TopLeft = Top | Left,
+
+ ///
+ /// The popup will be positioned to the top-right of the anchor edge
+ ///
+ TopRight = Top | Right,
+
+ ///
+ /// The popup will be positioned to the bottom-left of the anchor edge
+ ///
+ BottomLeft = Bottom | Left,
+
+ ///
+ /// The popup will be positioned to the bottom-right of the anchor edge
+ ///
+ BottomRight = Bottom | Right,
+ }
+
///
/// Positions an .
///
@@ -363,7 +428,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
TopLevel topLevel,
IVisual target, PlacementMode placement, Point offset,
- PopupPositioningEdge anchor, PopupPositioningEdge gravity)
+ PopupAnchor anchor, PopupGravity gravity, Rect? rect)
{
// We need a better way for tracking the last pointer position
var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position);
@@ -373,8 +438,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
if (placement == PlacementMode.Pointer)
{
positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1));
- positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
- positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ positionerParameters.Anchor = PopupAnchor.TopLeft;
+ positionerParameters.Gravity = PopupGravity.BottomRight;
}
else
{
@@ -384,32 +449,33 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
if (matrix == null)
{
if (target.GetVisualRoot() == null)
- throw new InvalidCastException("Target control is not attached to the visual tree");
- throw new InvalidCastException("Target control is not in the same tree as the popup parent");
+ 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");
}
- positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size)
- .TransformToAABB(matrix.Value);
+ var bounds = new Rect(default, target.Bounds.Size);
+ var anchorRect = rect ?? bounds;
+ positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
if (placement == PlacementMode.Right)
{
- positionerParameters.Anchor = PopupPositioningEdge.TopRight;
- positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ positionerParameters.Anchor = PopupAnchor.TopRight;
+ positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Bottom)
{
- positionerParameters.Anchor = PopupPositioningEdge.BottomLeft;
- positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ positionerParameters.Anchor = PopupAnchor.BottomLeft;
+ positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Left)
{
- positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
- positionerParameters.Gravity = PopupPositioningEdge.BottomLeft;
+ positionerParameters.Anchor = PopupAnchor.TopLeft;
+ positionerParameters.Gravity = PopupGravity.BottomLeft;
}
else if (placement == PlacementMode.Top)
{
- positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
- positionerParameters.Gravity = PopupPositioningEdge.TopRight;
+ positionerParameters.Anchor = PopupAnchor.TopLeft;
+ positionerParameters.Gravity = PopupGravity.TopRight;
}
else if (placement == PlacementMode.AnchorAndGravity)
{
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
index bd23a13e7f..04167bbd15 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
@@ -39,38 +39,38 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
- private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge)
+ private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge)
{
double x, y;
- if ((edge & PopupPositioningEdge.Left) != 0)
+ if ((edge & PopupAnchor.Left) != 0)
x = anchorRect.X;
- else if ((edge & PopupPositioningEdge.Right) != 0)
+ else if ((edge & PopupAnchor.Right) != 0)
x = anchorRect.Right;
else
x = anchorRect.X + anchorRect.Width / 2;
- if ((edge & PopupPositioningEdge.Top) != 0)
+ if ((edge & PopupAnchor.Top) != 0)
y = anchorRect.Y;
- else if ((edge & PopupPositioningEdge.Bottom) != 0)
+ else if ((edge & PopupAnchor.Bottom) != 0)
y = anchorRect.Bottom;
else
y = anchorRect.Y + anchorRect.Height / 2;
return new Point(x, y);
}
- private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity)
+ private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity)
{
double x, y;
- if ((gravity & PopupPositioningEdge.Left) != 0)
+ if ((gravity & PopupGravity.Left) != 0)
x = -size.Width;
- else if ((gravity & PopupPositioningEdge.Right) != 0)
+ else if ((gravity & PopupGravity.Right) != 0)
x = 0;
else
x = -size.Width / 2;
- if ((gravity & PopupPositioningEdge.Top) != 0)
+ if ((gravity & PopupGravity.Top) != 0)
y = -size.Height;
- else if ((gravity & PopupPositioningEdge.Bottom) != 0)
+ else if ((gravity & PopupGravity.Bottom) != 0)
y = 0;
else
y = -size.Height / 2;
@@ -89,7 +89,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
private void Update(Size translatedSize, Size originalSize,
- Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity,
+ Rect anchorRect, PopupAnchor anchor, PopupGravity gravity,
PopupPositionerConstraintAdjustment constraintAdjustment, Point offset)
{
var parentGeometry = _popup.ParentClientAreaScreenGeometry;
@@ -116,28 +116,28 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
var bounds = GetBounds();
- bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask)
+ bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask)
{
- if ((edge & PopupPositioningEdge.Left) != 0
+ if ((edge & PopupAnchor.Left) != 0
&& rc.X < bounds.X)
return false;
- if ((edge & PopupPositioningEdge.Top) != 0
+ if ((edge & PopupAnchor.Top) != 0
&& rc.Y < bounds.Y)
return false;
- if ((edge & PopupPositioningEdge.Right) != 0
+ if ((edge & PopupAnchor.Right) != 0
&& rc.Right > bounds.Right)
return false;
- if ((edge & PopupPositioningEdge.Bottom) != 0
+ if ((edge & PopupAnchor.Bottom) != 0
&& rc.Bottom > bounds.Bottom)
return false;
return true;
}
- Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) =>
+ Rect GetUnconstrained(PopupAnchor a, PopupGravity g) =>
new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize);
@@ -145,11 +145,11 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
// If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is
- if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask)
+ if (!FitsInBounds(geo, PopupAnchor.HorizontalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0)
{
var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
- if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask))
+ if (FitsInBounds(flipped, PopupAnchor.HorizontalMask))
geo = geo.WithX(flipped.X);
}
@@ -163,11 +163,11 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
// If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is
- if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask)
+ if (!FitsInBounds(geo, PopupAnchor.VerticalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0)
{
var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
- if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask))
+ if (FitsInBounds(flipped, PopupAnchor.VerticalMask))
geo = geo.WithY(flipped.Y);
}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index 788fe03162..3b167d52e1 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -82,11 +82,12 @@ namespace Avalonia.Controls.Primitives
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
- PopupPositioningEdge anchor = PopupPositioningEdge.None,
- PopupPositioningEdge gravity = PopupPositioningEdge.None)
+ PopupAnchor anchor = PopupAnchor.None,
+ PopupGravity gravity = PopupGravity.None,
+ Rect? rect = null)
{
_positionerParameters.ConfigurePosition(_parent, target,
- placement, offset, anchor, gravity);
+ placement, offset, anchor, gravity, rect);
if (_positionerParameters.Size != default)
UpdatePosition();