A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

507 lines
19 KiB

// The documentation and flag names in this file are initially taken from
// xdg_shell wayland protocol this API is designed after
// therefore, I'm including the license from wayland-protocols repo
/*
Copyright © 2008-2013 Kristian Høgsberg
Copyright © 2010-2013 Intel Corporation
Copyright © 2013 Rafael Antognolli
Copyright © 2013 Jasper St. Pierre
Copyright © 2014 Jonas Ådahl
Copyright © 2014 Jason Ekstrand
Copyright © 2014-2015 Collabora, Ltd.
Copyright © 2015 Red Hat Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
---
The above is the version of the MIT "Expat" License used by X.org:
http://cgit.freedesktop.org/xorg/xserver/tree/COPYING
Adjustments for Avalonia needs:
Copyright © 2019 Nikita Tsukanov
*/
using System;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
/// <summary>
/// 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 PopupGravity _gravity;
private PopupAnchor _anchor;
/// <summary>
/// Set the size of the popup that is to be positioned with the positioner object, in device-
/// independent pixels.
/// </summary>
public Size Size { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <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
{
PopupPositioningEdgeHelper.ValidateEdge(value);
_anchor = value;
}
}
/// <summary>
/// Defines in what direction a popup should be positioned, relative to the anchor point of
/// the parent.
/// </summary>
/// <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.ValidateGravity(value);
_gravity = value;
}
}
/// <summary>
/// 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 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.
/// </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.
/// </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>
/// 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
{
/// <summary>
/// Don't alter the surface position even if it is constrained on some
/// axis, for example partially outside the edge of an output.
/// </summary>
None = 0,
/// <summary>
/// Slide the surface along the x axis until it is no longer 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.
/// </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.
/// </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.
/// </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.
///
/// 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,
/// <summary>
/// Horizontally resize the surface
/// </summary>
/// <remarks>
/// Resize the surface horizontally so that it is completely unconstrained.
/// </remarks>
ResizeX = 16,
/// <summary>
/// Vertically resize the surface
/// </summary>
/// <remarks>
/// Resize the surface vertically so that it is completely unconstrained.
/// </remarks>
ResizeY = 16,
All = SlideX|SlideY|FlipX|FlipY|ResizeX|ResizeY
}
static class PopupPositioningEdgeHelper
{
public static void ValidateEdge(this PopupAnchor edge)
{
if (edge.HasAllFlags(PopupAnchor.Left | PopupAnchor.Right) ||
edge.HasAllFlags(PopupAnchor.Top | PopupAnchor.Bottom))
throw new ArgumentException("Opposite edges specified");
}
public static void ValidateGravity(this PopupGravity gravity)
{
ValidateEdge((PopupAnchor)gravity);
}
public static PopupAnchor Flip(this PopupAnchor edge)
{
if (edge.HasAnyFlag(PopupAnchor.HorizontalMask))
edge ^= PopupAnchor.HorizontalMask;
if (edge.HasAnyFlag(PopupAnchor.VerticalMask))
edge ^= PopupAnchor.VerticalMask;
return edge;
}
public static PopupAnchor FlipX(this PopupAnchor edge)
{
if (edge.HasAnyFlag(PopupAnchor.HorizontalMask))
edge ^= PopupAnchor.HorizontalMask;
return edge;
}
public static PopupAnchor FlipY(this PopupAnchor edge)
{
if (edge.HasAnyFlag(PopupAnchor.VerticalMask))
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 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);
}
static class PopupPositionerExtensions
{
public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
TopLevel topLevel,
IVisual target, PlacementMode placement, Point offset,
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 = constraintAdjustment;
if (placement == PlacementMode.Pointer)
{
positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1));
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else
{
if (target == null)
throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
var 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 = rect ?? bounds;
positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
if (placement == PlacementMode.Right)
{
positionerParameters.Anchor = PopupAnchor.TopRight;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Bottom)
{
positionerParameters.Anchor = PopupAnchor.BottomLeft;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Left)
{
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.BottomLeft;
}
else if (placement == PlacementMode.Top)
{
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.TopRight;
}
else if (placement == PlacementMode.AnchorAndGravity)
{
positionerParameters.Anchor = anchor;
positionerParameters.Gravity = gravity;
}
else
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
}
}
}
}