@ -50,46 +50,48 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
/// <summary>
///
/// The IPopupPositioner provides a collection of rules for the placement of a
/// a popup relative to its parent. Rules can be defined to ensure
/// the popup remains within the visible area's borders, and to
/// specify how the popup changes its position, such as sliding along
/// an axis, or flipping around a rectangle. These positioner-created rules are
/// constrained by the requirement that a popup must intersect with or
/// be at least partially adjacent to its parent surface.
/// Provides positioning parameters to <see cref="IPopupPositioner"/>.
/// </summary>
/// <remarks>
/// The IPopupPositioner provides a collection of rules for the placement of a a popup relative
/// to its parent. Rules can be defined to ensure the popup remains within the visible area's
/// borders, and to specify how the popup changes its position, such as sliding along an axis,
/// or flipping around a rectangle. These positioner-created rules are constrained by the
/// requirement that a popup must intersect with or be at least partially adjacent to its parent
/// surface.
/// </remarks>
public struct PopupPositionerParameters
{
private PopupPositioningEdge _ gravity ;
private PopupPositioningEdge _ anchor ;
private PopupGravity _ gravity ;
private PopupAnchor _ anchor ;
/// <summary>
/// Set the size of the popup that is to be positioned with the positioner
/// object. The size is in scaled coordinates.
/// Set the size of the popup that is to be positioned with the positioner object, in device-
/// independent pixel s.
/// </summary>
public Size Size { get ; set ; }
/// <summary>
/// Specify the anchor rectangle within the parent that the popup
/// will be placed relative to. The rectangle is relative to the
/// parent geometry
///
/// The anchor rectangle may not extend outside the window geometry of the
/// popup's parent. The anchor rectangle is in scaled coordinates
/// Specifies the anchor rectangle within the parent that the popup will be placed relative
/// to, in device-independent pixels.
/// </summary>
/// <remarks>
/// The rectangle is relative to the parent geometry and may not extend outside the window
/// geometry of the popup's parent.
/// </remarks>
public Rect AnchorRectangle { get ; set ; }
/// <summary>
/// Defines the anchor point for the anchor rectangle. The specified anchor
/// is used derive an anchor point that the popup will be
/// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or
/// 'BottomRight'), the anchor point will be at the specified corner;
/// otherwise, the derived anchor point will be centered on the specified
/// edge, or in the center of the anchor rectangle if no edge is specified.
/// Defines the anchor point for the anchor rectangle.
/// </summary>
public PopupPositioningEdge Anchor
/// <remarks>
/// The specified anchor is used derive an anchor point that the popup will be positioned
/// relative to. If a corner anchor is set (e.g. 'TopLeft' or 'BottomRight'), the anchor
/// point will be at the specified corner; otherwise, the derived anchor point will be
/// centered on the specified edge, or in the center of the anchor rectangle if no edge is
/// specified.
/// </remarks>
public PopupAnchor Anchor
{
get = > _ anchor ;
set
@ -100,66 +102,70 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
/// <summary>
/// Defines in what direction a popup should be positioned, relative to
/// the anchor point of the parent. If a corner gravity is
/// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup
/// will be placed towards the specified gravity; otherwise, the popup
/// will be centered over the anchor point on any axis that had no
/// gravity specified.
/// Defines in what direction a popup should be positioned, relative to the anchor point of
/// the parent.
/// </summary>
public PopupPositioningEdge Gravity
/// <remarks>
/// If a corner gravity is specified (e.g. 'BottomRight' or 'TopLeft'), then the popup will
/// be placed towards the specified gravity; otherwise, the popup will be centered over the
/// anchor point on any axis that had no gravity specified.
/// </remarks>
public PopupGravity Gravity
{
get = > _ gravity ;
set
{
PopupPositioningEdgeHelper . ValidateEdge ( value ) ;
PopupPositioningEdgeHelper . ValidateGravity ( value ) ;
_ gravity = value ;
}
}
/// <summary>
/// Specify how the popup should be positioned if the originally intended
/// position caused the popup to be constrained, meaning at least
/// partially outside positioning boundaries set by the positioner. The
/// adjustment is set by constructing a bitmask describing the adjustment to
/// be made when the popup is constrained on that axis.
/// Specify how the popup should be positioned if the originally intended position caused
/// the popup to be constrained.
/// </summary>
/// <remarks>
/// Adjusts the popup position if the intended position caused the popup to be constrained;
/// meaning at least partially outside positioning boundaries set by the positioner. The
/// adjustment is set by constructing a bitmask describing the adjustment to be made when
/// the popup is constrained on that axis.
///
/// If no bit for one axis is set, the positioner will assume that the child
/// surface should not change its position on that axis when constrained.
/// If no bit for one axis is set, the positioner will assume that the child surface should
/// not change its position on that axis when constrained.
///
/// If more than one bit for one axis is set, the order of how adjustments
/// are applied is specified in the corresponding adjustment descriptions.
/// If more than one bit for one axis is set, the order of how adjustments are applied is
/// specified in the corresponding adjustment descriptions.
///
/// The default adjustment is none.
/// </summary >
/// </remarks >
public PopupPositionerConstraintAdjustment ConstraintAdjustment { get ; set ; }
/// <summary>
/// Specify the popup position offset relative to the position of the
/// anchor on the anchor rectangle and the anchor on the popup. For
/// example if the anchor of the anchor rectangle is at (x, y), the popup
/// has the gravity bottom|right, and the offset is (ox, oy), the calculated
/// surface position will be (x + ox, y + oy). The offset position of the
/// surface is the one used for constraint testing. See
/// set_constraint_adjustment.
///
/// An example use case is placing a popup menu on top of a user interface
/// element, while aligning the user interface element of the parent surface
/// with some user interface element placed somewhere in the popup.
/// anchor on the anchor rectangle and the anchor on the popup.
/// </summary>
/// <remarks>
/// For example if the anchor of the anchor rectangle is at (x, y), the popup has the
/// gravity bottom|right, and the offset is (ox, oy), the calculated surface position will
/// be (x + ox, y + oy). The offset position of the surface is the one used for constraint
/// testing. See set_constraint_adjustment.
///
/// An example use case is placing a popup menu on top of a user interface element, while
/// aligning the user interface element of the parent surface with some user interface
/// element placed somewhere in the popup.
/// </remarks>
public Point Offset { get ; set ; }
}
/// <summary>
/// The constraint adjustment value define ways how popup position will
/// be adjusted if the unadjusted position would result in the popup
/// being partly constrained.
///
/// Whether a popup is considered 'constrained' is left to the positioner
/// to determine. For example, the popup may be partly outside the
/// target platform defined 'work area', thus necessitating the popup's
/// position be adjusted until it is entirely inside the work area.
/// Defines how a popup position will be adjusted if the unadjusted position would result in
/// the popup being partly constrained.
/// </summary>
/// <remarks>
/// Whether a popup is considered 'constrained' is left to the positioner to determine. For
/// example, the popup may be partly outside the target platform defined 'work area', thus
/// necessitating the popup's position be adjusted until it is entirely inside the work area.
/// </remarks>
[Flags]
public enum PopupPositionerConstraintAdjustment
{
@ -171,79 +177,97 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
/// <summary>
/// Slide the surface along the x axis until it is no longer constrained.
/// First try to slide towards the direction of the gravity on the x axis
/// until either the edge in the opposite direction of the gravity is
/// unconstrained or the edge in the direction of the gravity is
/// constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the
/// x axis until either the edge in the direction of the gravity is
/// unconstrained or the edge in the opposite direction of the gravity is
/// constrained.
/// </summary>
/// <remarks>
/// First try to slide towards the direction of the gravity on the x axis until either the
/// edge in the opposite direction of the gravity is unconstrained or the edge in the
/// direction of the gravity is constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the x axis until
/// either the edge in the direction of the gravity is unconstrained or the edge in the
/// opposite direction of the gravity is constrained.
/// </remarks>
SlideX = 1 ,
/// <summary>
/// Slide the surface along the y axis until it is no longer constrained.
///
/// First try to slide towards the direction of the gravity on the y axis
/// until either the edge in the opposite direction of the gravity is
/// unconstrained or the edge in the direction of the gravity is
/// constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the
/// y axis until either the edge in the direction of the gravity is
/// unconstrained or the edge in the opposite direction of the gravity is
/// constrained.
/// */
/// Slide the surface along the y axis until it is no longer constrained.
/// </summary>
/// <remarks>
/// First try to slide towards the direction of the gravity on the y axis until either the
/// edge in the opposite direction of the gravity is unconstrained or the edge in the
/// direction of the gravity is constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the y axis until
/// either the edge in the direction of the gravity is unconstrained or the edge in the
/// opposite direction of the gravity is constrained.
/// </remarks>
SlideY = 2 ,
/// <summary>
/// Invert the anchor and gravity on the x axis if the surface is
/// constrained on the x axis. For example, if the left edge of the
/// surface is constrained, the gravity is 'left' and the anchor is
/// 'left', change the gravity to 'right' and the anchor to 'right'.
///
/// If the adjusted position also ends up being constrained, the resulting
/// position of the flip_x adjustment will be the one before the
/// adjustment.
/// Invert the anchor and gravity on the x axis if the surface is constrained on the x axis.
/// </summary>
/// <remarks>
/// For example, if the left edge of the surface is constrained, the gravity is 'left' and
/// the anchor is 'left', change the gravity to 'right' and the anchor to 'right'.
///
/// If the adjusted position also ends up being constrained, the resulting position of the
/// FlipX adjustment will be the one before the adjustment.
/// /// </remarks>
FlipX = 4 ,
/// <summary>
/// Invert the anchor and gravity on the y axis if the surface is
/// constrained on the y axis. For example, if the bottom edge of the
/// surface is constrained, the gravity is 'bottom' and the anchor is
/// 'bottom', change the gravity to 'top' and the anchor to 'top'.
/// Invert the anchor and gravity on the y axis if the surface is constrained on the y axis.
/// </summary>
/// <remarks>
/// For example, if the bottom edge of the surface is constrained, the gravity is 'bottom'
/// and the anchor is 'bottom', change the gravity to 'top' and the anchor to 'top'.
///
/// The adjusted position is calculated given the original anchor
/// rectangle and offset, but with the new flipped anchor and gravity
/// values.
/// The adjusted position is calculated given the original anchor rectangle and offset, but
/// with the new flipped anchor and gravity values.
///
/// If the adjusted position also ends up being constrained, the resulting
/// position of the flip_y adjustment will be the one before the
/// adjustment.
/// </summary>
/// If the adjusted position also ends up being constrained, the resulting position of the
/// FlipY adjustment will be the one before the adjustment.
/// </remarks>
FlipY = 8 ,
All = SlideX | SlideY | FlipX | FlipY
/// <summary>
/// Horizontally resize the surface
/// </summary>
/// <remarks>
/// Resize the surface horizontally so that it is completely unconstrained.
/// </remarks>
ResizeX = 1 6 ,
/// <summary>
/// Vertically resize the surface
/// </summary>
/// <remarks>
/// Resize the surface vertically so that it is completely unconstrained.
/// </remarks>
ResizeY = 1 6 ,
All = SlideX | SlideY | FlipX | FlipY | ResizeX | ResizeY
}
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 )
@ -251,43 +275,167 @@ 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 ) ;
}
}
/// <summary>
/// Defines the edges around an anchor rectangle on which a popup will open.
/// </summary>
[Flags]
public enum PopupPositioningEdge
public enum PopupAnchor
{
/// <summary>
/// The center of the anchor rectangle.
/// </summary>
None ,
/// <summary>
/// The top edge of the anchor rectangle.
/// </summary>
Top = 1 ,
/// <summary>
/// The bottom edge of the anchor rectangle.
/// </summary>
Bottom = 2 ,
/// <summary>
/// The left edge of the anchor rectangle.
/// </summary>
Left = 4 ,
/// <summary>
/// The right edge of the anchor rectangle.
/// </summary>
Right = 8 ,
/// <summary>
/// The top-left corner of the anchor rectangle.
/// </summary>
TopLeft = Top | Left ,
/// <summary>
/// The top-right corner of the anchor rectangle.
/// </summary>
TopRight = Top | Right ,
/// <summary>
/// The bottom-left corner of the anchor rectangle.
/// </summary>
BottomLeft = Bottom | Left ,
/// <summary>
/// The bottom-right corner of the anchor rectangle.
/// </summary>
BottomRight = Bottom | Right ,
/// <summary>
/// A mask for the vertical component flags.
/// </summary>
VerticalMask = Top | Bottom ,
/// <summary>
/// A mask for the horizontal component flags.
/// </summary>
HorizontalMask = Left | Right ,
/// <summary>
/// A mask for all flags.
/// </summary>
AllMask = VerticalMask | HorizontalMask
}
/// <summary>
/// Defines the direction in which a popup will open.
/// </summary>
[Flags]
public enum PopupGravity
{
/// <summary>
/// The popup will be centered over the anchor edge.
/// </summary>
None ,
/// <summary>
/// The popup will be positioned above the anchor edge
/// </summary>
Top = 1 ,
/// <summary>
/// The popup will be positioned below the anchor edge
/// </summary>
Bottom = 2 ,
/// <summary>
/// The popup will be positioned to the left of the anchor edge
/// </summary>
Left = 4 ,
/// <summary>
/// The popup will be positioned to the right of the anchor edge
/// </summary>
Right = 8 ,
/// <summary>
/// The popup will be positioned to the top-left of the anchor edge
/// </summary>
TopLeft = Top | Left ,
/// <summary>
/// The popup will be positioned to the top-right of the anchor edge
/// </summary>
TopRight = Top | Right ,
/// <summary>
/// The popup will be positioned to the bottom-left of the anchor edge
/// </summary>
BottomLeft = Bottom | Left ,
/// <summary>
/// The popup will be positioned to the bottom-right of the anchor edge
/// </summary>
BottomRight = Bottom | Right ,
}
/// <summary>
/// Positions an <see cref="IPopupHost"/>.
/// </summary>
/// <remarks>
/// <see cref="IPopupPositioner"/> is an abstraction of the wayland xdg_positioner spec.
///
/// The popup positioner implementation is determined by the platform implementation. A default
/// managed implementation is provided in <see cref="ManagedPopupPositioner"/> for platforms
/// on which popups can be arbitrarily positioned.
/// </remarks>
public interface IPopupPositioner
{
/// <summary>
/// Updates the position of the associated <see cref="IPopupHost"/> according to the
/// specified parameters.
/// </summary>
/// <param name="parameters">The positioning parameters.</param>
void Update ( PopupPositionerParameters parameters ) ;
}
@ -296,18 +444,19 @@ 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 ,
PopupPositionerConstraintAdjustment constraintAdjustment , Rect ? rect )
{
// We need a better way for tracking the last pointer position
var pointer = topLevel . PointToClient ( topLevel . PlatformImpl . MouseDevice . Position ) ;
positionerParameters . Offset = offset ;
positionerParameters . ConstraintAdjustment = PopupPositionerConstraintAdjustment . All ;
positionerParameters . ConstraintAdjustment = constraintAdjustment ;
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
{
@ -317,32 +466,33 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
if ( matrix = = null )
{
if ( target . GetVisualRoot ( ) = = null )
throw new InvalidCast Exception ( "Target control is not attached to the visual tree" ) ;
throw new InvalidCast Exception ( "Target control is not in the same tree as the popup parent" ) ;
throw new InvalidOperation Exception ( "Target control is not attached to the visual tree" ) ;
throw new InvalidOperation Exception ( "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 )
{