Browse Source

Merge pull request #3970 from AvaloniaUI/feature/contentmenu-positioning

Added additional positioning properties to Popup and ContextMenu.
pull/4129/head
Jumar Macato 6 years ago
committed by GitHub
parent
commit
17e56a8e64
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 167
      src/Avalonia.Controls/ContextMenu.cs
  2. 55
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  3. 12
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  4. 117
      src/Avalonia.Controls/Primitives/Popup.cs
  5. 424
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  6. 117
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  7. 6
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  8. 8
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  9. 3
      src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs

167
src/Avalonia.Controls/ContextMenu.cs

@ -1,17 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Styling;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -19,11 +20,59 @@ namespace Avalonia.Controls
/// </summary>
public class ContextMenu : MenuBase, ISetterValue
{
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
Popup.HorizontalOffsetProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="VerticalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> VerticalOffsetProperty =
Popup.VerticalOffsetProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementAnchor"/> property.
/// </summary>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property.
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
Popup.PlacementConstraintAdjustmentProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementGravity"/> property.
/// </summary>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
Popup.PlacementModeProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementRect"/> property.
/// </summary>
public static readonly StyledProperty<Rect?> PlacementRectProperty =
AvaloniaProperty.Register<Popup, Rect?>(nameof(PlacementRect));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
Popup.PlacementTargetProperty.AddOwner<ContextMenu>();
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
private List<Control> _attachedControls;
private IInputElement _previousFocus;
private Popup? _popup;
private List<Control>? _attachedControls;
private IInputElement? _previousFocus;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
@ -47,23 +96,107 @@ namespace Avalonia.Controls
/// </summary>
static ContextMenu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel);
ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
}
/// <summary>
/// Gets or sets the Horizontal offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double HorizontalOffset
{
get { return GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the Vertical offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double VerticalOffset
{
get { return GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupAnchor PlacementAnchor
{
get { return GetValue(PlacementAnchorProperty); }
set { SetValue(PlacementAnchorProperty, value); }
}
/// <summary>
/// Gets or sets a value describing how the context menu position will be adjusted if the
/// unadjusted position would result in the context menu being partly constrained.
/// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get { return GetValue(PlacementConstraintAdjustmentProperty); }
set { SetValue(PlacementConstraintAdjustmentProperty, value); }
}
/// <summary>
/// Gets or sets a value which defines in what direction the context menu should open
/// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupGravity PlacementGravity
{
get { return GetValue(PlacementGravityProperty); }
set { SetValue(PlacementGravityProperty, value); }
}
/// <summary>
/// Gets or sets the placement mode of the context menu in relation to the<see cref="PlacementTarget"/>.
/// </summary>
public PlacementMode PlacementMode
{
get { return GetValue(PlacementModeProperty); }
set { SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
/// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
public Rect? PlacementRect
{
get { return GetValue(PlacementRectProperty); }
set { SetValue(PlacementRectProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from false to true.
/// </summary>
public event CancelEventHandler ContextMenuOpening;
public event CancelEventHandler? ContextMenuOpening;
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from true to false.
/// </summary>
public event CancelEventHandler ContextMenuClosing;
public event CancelEventHandler? ContextMenuClosing;
/// <summary>
/// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
@ -77,7 +210,7 @@ namespace Avalonia.Controls
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
@ -97,7 +230,7 @@ namespace Avalonia.Controls
/// Opens a context menu on the specified control.
/// </summary>
/// <param name="control">The control.</param>
public void Open(Control control)
public void Open(Control? control)
{
if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
@ -113,7 +246,7 @@ namespace Avalonia.Controls
nameof(control));
}
control ??= _attachedControls[0];
control ??= _attachedControls![0];
if (IsOpen)
{
@ -124,8 +257,14 @@ namespace Avalonia.Controls
{
_popup = new Popup
{
PlacementMode = PlacementMode.Pointer,
PlacementTarget = control,
HorizontalOffset = HorizontalOffset,
VerticalOffset = VerticalOffset,
PlacementAnchor = PlacementAnchor,
PlacementConstraintAdjustment = PlacementConstraintAdjustment,
PlacementGravity = PlacementGravity,
PlacementMode = PlacementMode,
PlacementRect = PlacementRect,
PlacementTarget = PlacementTarget ?? control,
StaysOpen = false
};
@ -204,7 +343,7 @@ namespace Avalonia.Controls
if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup!).SetParent(null);
}
// HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic.

55
src/Avalonia.Controls/Primitives/IPopupHost.cs

@ -5,19 +5,70 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Represents the top-level control opened by a <see cref="Popup"/>.
/// </summary>
/// <remarks>
/// A popup host can be either be a popup window created by the operating system
/// (<see cref="PopupRoot"/>) or an <see cref="OverlayPopupHost"/> which is created
/// on an <see cref="OverlayLayer"/>.
/// </remarks>
public interface IPopupHost : IDisposable
{
/// <summary>
/// Sets the control to display in the popup.
/// </summary>
/// <param name="control"></param>
void SetChild(IControl control);
/// <summary>
/// Gets the presenter from the control's template.
/// </summary>
IContentPresenter Presenter { get; }
/// <summary>
/// Gets the root of the visual tree in the case where the popup is presented using a
/// separate visual tree.
/// </summary>
IVisual HostedVisualTreeRoot { get; }
/// <summary>
/// Raised when the control's template is applied.
/// </summary>
event EventHandler<TemplateAppliedEventArgs> TemplateApplied;
/// <summary>
/// Configures the position of the popup according to a target control and a set of
/// placement parameters.
/// </summary>
/// <param name="target">The placement target.</param>
/// <param name="placement">The placement mode.</param>
/// <param name="offset">The offset, in device-independent pixels.</param>
/// <param name="anchor">The anchor point.</param>
/// <param name="gravity">The popup gravity.</param>
/// <param name="rect">
/// The anchor rect. If null, the bounds of <paramref name="target"/> will be used.
/// </param>
void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor = PopupPositioningEdge.None,
PopupPositioningEdge gravity = PopupPositioningEdge.None);
PopupAnchor anchor = PopupAnchor.None,
PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null);
/// <summary>
/// Shows the popup.
/// </summary>
void Show();
/// <summary>
/// Hides the popup.
/// </summary>
void Hide();
/// <summary>
/// Binds the constraints of the popup host to a set of properties, usally those present on
/// <see cref="Popup"/>.
/// </summary>
IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty,
StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty,
StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,

12
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -71,10 +71,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,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null)
{
_positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor,
gravity);
gravity, constraintAdjustment, rect);
UpdatePosition();
}
@ -122,10 +124,8 @@ namespace Avalonia.Controls.Primitives
}, DispatcherPriority.Layout);
}
Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt;
Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size;
double IManagedPopupPositionerPopup.Scaling => 1;
public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver)
{
var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup();

117
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;
@ -37,12 +38,45 @@ namespace Avalonia.Controls.Primitives
o => o.IsOpen,
(o, v) => o.IsOpen = v);
/// <summary>
/// Defines the <see cref="PlacementAnchor"/> property.
/// </summary>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
AvaloniaProperty.Register<Popup, PopupAnchor>(nameof(PlacementAnchor));
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property.
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
AvaloniaProperty.Register<Popup, PopupPositionerConstraintAdjustment>(
nameof(PlacementConstraintAdjustment),
PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
/// <summary>
/// Defines the <see cref="PlacementGravity"/> property.
/// </summary>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
AvaloniaProperty.Register<Popup, PopupGravity>(nameof(PlacementGravity));
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
/// <summary>
/// Defines the <see cref="PlacementRect"/> property.
/// </summary>
public static readonly StyledProperty<Rect?> PlacementRectProperty =
AvaloniaProperty.Register<Popup, Rect?>(nameof(PlacementRect));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
#pragma warning disable 618
/// <summary>
/// Defines the <see cref="ObeyScreenEdges"/> property.
@ -63,12 +97,6 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<double> VerticalOffsetProperty =
AvaloniaProperty.Register<Popup, double>(nameof(VerticalOffset));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
/// <summary>
/// Defines the <see cref="StaysOpen"/> property.
/// </summary>
@ -145,6 +173,36 @@ namespace Avalonia.Controls.Primitives
set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
/// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupAnchor PlacementAnchor
{
get { return GetValue(PlacementAnchorProperty); }
set { SetValue(PlacementAnchorProperty, value); }
}
/// <summary>
/// Gets or sets a value describing how the popup position will be adjusted if the
/// unadjusted position would result in the popup being partly constrained.
/// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get { return GetValue(PlacementConstraintAdjustmentProperty); }
set { SetValue(PlacementConstraintAdjustmentProperty, value); }
}
/// <summary>
/// Gets or sets a value which defines in what direction the popup should open
/// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupGravity PlacementGravity
{
get { return GetValue(PlacementGravityProperty); }
set { SetValue(PlacementGravityProperty, value); }
}
/// <summary>
/// Gets or sets the placement mode of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
@ -154,6 +212,32 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// Gets or sets the the anchor rectangle within the parent that the popup will be placed
/// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
public Rect? PlacementRect
{
get { return GetValue(PlacementRectProperty); }
set { SetValue(PlacementRectProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
[Obsolete("This property has no effect")]
public bool ObeyScreenEdges
{
@ -162,7 +246,7 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double HorizontalOffset
{
@ -171,7 +255,7 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>
/// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double VerticalOffset
{
@ -179,15 +263,6 @@ namespace Avalonia.Controls.Primitives
set { SetValue(VerticalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
@ -260,8 +335,12 @@ namespace Avalonia.Controls.Primitives
popupHost.ConfigurePosition(
placementTarget,
PlacementMode,
new Point(HorizontalOffset, VerticalOffset));
PlacementMode,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect);
DeferCleanup(SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,

424
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -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 pixels.
/// </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 = 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 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 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)
{

117
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Transactions;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
@ -8,9 +9,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
{
IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens { get; }
Rect ParentClientAreaScreenGeometry { get; }
double Scaling { get; }
void MoveAndResize(Point devicePoint, Size virtualSize);
Point TranslatePoint(Point pt);
Size TranslateSize(Size size);
}
public class ManagedPopupPositionerScreenInfo
@ -25,6 +25,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
}
/// <summary>
/// An <see cref="IPopupPositioner"/> implementation for platforms on which a popup can be
/// aritrarily positioned.
/// </summary>
public class ManagedPopupPositioner : IPopupPositioner
{
private readonly IManagedPopupPositionerPopup _popup;
@ -35,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;
@ -75,17 +79,24 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public void Update(PopupPositionerParameters parameters)
{
Update(_popup.TranslateSize(parameters.Size), parameters.Size,
new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft),
_popup.TranslateSize(parameters.AnchorRectangle.Size)),
parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment,
_popup.TranslatePoint(parameters.Offset));
var rect = Calculate(
parameters.Size * _popup.Scaling,
new Rect(
parameters.AnchorRectangle.TopLeft * _popup.Scaling,
parameters.AnchorRectangle.Size * _popup.Scaling),
parameters.Anchor,
parameters.Gravity,
parameters.ConstraintAdjustment,
parameters.Offset * _popup.Scaling);
_popup.MoveAndResize(
rect.Position,
rect.Size / _popup.Scaling);
}
private void Update(Size translatedSize, Size originalSize,
Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity,
private Rect Calculate(Size translatedSize,
Rect anchorRect, PopupAnchor anchor, PopupGravity gravity,
PopupPositionerConstraintAdjustment constraintAdjustment, Point offset)
{
var parentGeometry = _popup.ParentClientAreaScreenGeometry;
@ -112,28 +123,30 @@ 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) =>
static bool IsValid(in Rect rc) => rc.Width > 0 && rc.Height > 0;
Rect GetUnconstrained(PopupAnchor a, PopupGravity g) =>
new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize);
@ -141,11 +154,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);
}
@ -157,13 +170,34 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
geo = geo.WithX(bounds.Right - geo.Width);
}
// Resize the rect horizontally if allowed.
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeX) != 0)
{
var unconstrainedRect = geo;
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Left))
{
unconstrainedRect = unconstrainedRect.WithX(bounds.X);
}
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Right))
{
unconstrainedRect = unconstrainedRect.WithWidth(bounds.Width - unconstrainedRect.X);
}
if (IsValid(unconstrainedRect))
{
geo = unconstrainedRect;
}
}
// 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);
}
@ -175,7 +209,28 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
geo = geo.WithY(bounds.Bottom - geo.Height);
}
_popup.MoveAndResize(geo.TopLeft, originalSize);
// Resize the rect vertically if allowed.
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeY) != 0)
{
var unconstrainedRect = geo;
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Top))
{
unconstrainedRect = unconstrainedRect.WithY(bounds.Y);
}
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom))
{
unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y);
}
if (IsValid(unconstrainedRect))
{
geo = unconstrainedRect;
}
}
return geo;
}
}
}

6
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs

@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
{
// Popup positioner operates with abstract coordinates, but in our case they are pixel ones
var point = _parent.PointToScreen(default);
var size = TranslateSize(_parent.ClientSize);
var size = _parent.ClientSize * Scaling;
return new Rect(point.X, point.Y, size.Width, size.Height);
}
@ -43,8 +43,6 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
_moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling);
}
public virtual Point TranslatePoint(Point pt) => pt * _parent.Scaling;
public virtual Size TranslateSize(Size size) => size * _parent.Scaling;
public virtual double Scaling => _parent.Scaling;
}
}

8
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -82,11 +82,13 @@ 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,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null)
{
_positionerParameters.ConfigurePosition(_parent, target,
placement, offset, anchor, gravity);
placement, offset, anchor, gravity, constraintAdjustment, rect);
if (_positionerParameters.Size != default)
UpdatePosition();

3
src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs

@ -9,8 +9,7 @@ namespace Avalonia.Native
{
}
public override Point TranslatePoint(Point pt) => pt;
public override Size TranslateSize(Size size) => size;
public override double Scaling => 1;
}
}

Loading…
Cancel
Save