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();