csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
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.
228 lines
8.4 KiB
228 lines
8.4 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Transactions;
|
|
|
|
namespace Avalonia.Controls.Primitives.PopupPositioning
|
|
{
|
|
public interface IManagedPopupPositionerPopup
|
|
{
|
|
IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens { get; }
|
|
Rect ParentClientAreaScreenGeometry { get; }
|
|
double Scaling { get; }
|
|
void MoveAndResize(Point devicePoint, Size virtualSize);
|
|
}
|
|
|
|
public class ManagedPopupPositionerScreenInfo
|
|
{
|
|
public Rect Bounds { get; }
|
|
public Rect WorkingArea { get; }
|
|
|
|
public ManagedPopupPositionerScreenInfo(Rect bounds, Rect workingArea)
|
|
{
|
|
Bounds = bounds;
|
|
WorkingArea = workingArea;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// An <see cref="IPopupPositioner"/> implementation for platforms on which a popup can be
|
|
/// arbitrarily positioned.
|
|
/// </summary>
|
|
public class ManagedPopupPositioner : IPopupPositioner
|
|
{
|
|
private readonly IManagedPopupPositionerPopup _popup;
|
|
|
|
public ManagedPopupPositioner(IManagedPopupPositionerPopup popup)
|
|
{
|
|
_popup = popup;
|
|
}
|
|
|
|
|
|
private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge)
|
|
{
|
|
double x, y;
|
|
if (edge.HasAllFlags(PopupAnchor.Left))
|
|
x = anchorRect.X;
|
|
else if (edge.HasAllFlags(PopupAnchor.Right))
|
|
x = anchorRect.Right;
|
|
else
|
|
x = anchorRect.X + anchorRect.Width / 2;
|
|
|
|
if (edge.HasAllFlags(PopupAnchor.Top))
|
|
y = anchorRect.Y;
|
|
else if (edge.HasAllFlags(PopupAnchor.Bottom))
|
|
y = anchorRect.Bottom;
|
|
else
|
|
y = anchorRect.Y + anchorRect.Height / 2;
|
|
return new Point(x, y);
|
|
}
|
|
|
|
private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity)
|
|
{
|
|
double x, y;
|
|
if (gravity.HasAllFlags(PopupGravity.Left))
|
|
x = -size.Width;
|
|
else if (gravity.HasAllFlags(PopupGravity.Right))
|
|
x = 0;
|
|
else
|
|
x = -size.Width / 2;
|
|
|
|
if (gravity.HasAllFlags(PopupGravity.Top))
|
|
y = -size.Height;
|
|
else if (gravity.HasAllFlags(PopupGravity.Bottom))
|
|
y = 0;
|
|
else
|
|
y = -size.Height / 2;
|
|
return anchorPoint + new Point(x, y);
|
|
}
|
|
|
|
public void Update(PopupPositionerParameters parameters)
|
|
{
|
|
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 Rect Calculate(Size translatedSize,
|
|
Rect anchorRect, PopupAnchor anchor, PopupGravity gravity,
|
|
PopupPositionerConstraintAdjustment constraintAdjustment, Point offset)
|
|
{
|
|
var parentGeometry = _popup.ParentClientAreaScreenGeometry;
|
|
anchorRect = anchorRect.Translate(parentGeometry.TopLeft);
|
|
|
|
Rect GetBounds()
|
|
{
|
|
var screens = _popup.Screens;
|
|
|
|
var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft))
|
|
?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect))
|
|
?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft))
|
|
?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry))
|
|
?? screens.FirstOrDefault();
|
|
|
|
if (targetScreen != null && targetScreen.WorkingArea.IsEmpty)
|
|
{
|
|
return targetScreen.Bounds;
|
|
}
|
|
|
|
return targetScreen?.WorkingArea
|
|
?? new Rect(0, 0, double.MaxValue, double.MaxValue);
|
|
}
|
|
|
|
var bounds = GetBounds();
|
|
|
|
bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask)
|
|
{
|
|
if (edge.HasAllFlags(PopupAnchor.Left) && rc.X < bounds.X ||
|
|
edge.HasAllFlags(PopupAnchor.Top) && rc.Y < bounds.Y ||
|
|
edge.HasAllFlags(PopupAnchor.Right) && rc.Right > bounds.Right ||
|
|
edge.HasAllFlags(PopupAnchor.Bottom) && rc.Bottom > bounds.Bottom)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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);
|
|
|
|
|
|
var geo = GetUnconstrained(anchor, gravity);
|
|
|
|
// If flipping geometry and anchor is allowed and helps, use the flipped one,
|
|
// otherwise leave it as is
|
|
if (!FitsInBounds(geo, PopupAnchor.HorizontalMask)
|
|
&& constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.FlipX))
|
|
{
|
|
var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
|
|
if (FitsInBounds(flipped, PopupAnchor.HorizontalMask))
|
|
geo = geo.WithX(flipped.X);
|
|
}
|
|
|
|
// If sliding is allowed, try moving the rect into the bounds
|
|
if (constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.SlideX))
|
|
{
|
|
geo = geo.WithX(Math.Max(geo.X, bounds.X));
|
|
if (geo.Right > bounds.Right)
|
|
geo = geo.WithX(bounds.Right - geo.Width);
|
|
}
|
|
|
|
// Resize the rect horizontally if allowed.
|
|
if (constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.ResizeX))
|
|
{
|
|
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, PopupAnchor.VerticalMask)
|
|
&& constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.FlipY))
|
|
{
|
|
var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
|
|
if (FitsInBounds(flipped, PopupAnchor.VerticalMask))
|
|
geo = geo.WithY(flipped.Y);
|
|
}
|
|
|
|
// If sliding is allowed, try moving the rect into the bounds
|
|
if (constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.SlideY))
|
|
{
|
|
geo = geo.WithY(Math.Max(geo.Y, bounds.Y));
|
|
if (geo.Bottom > bounds.Bottom)
|
|
geo = geo.WithY(bounds.Bottom - geo.Height);
|
|
}
|
|
|
|
// Resize the rect vertically if allowed.
|
|
if (constraintAdjustment.HasAllFlags(PopupPositionerConstraintAdjustment.ResizeY))
|
|
{
|
|
var unconstrainedRect = geo;
|
|
|
|
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Top))
|
|
{
|
|
unconstrainedRect = unconstrainedRect.WithY(bounds.Y);
|
|
}
|
|
|
|
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom))
|
|
{
|
|
unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y);
|
|
}
|
|
|
|
if (IsValid(unconstrainedRect))
|
|
{
|
|
geo = unconstrainedRect;
|
|
}
|
|
}
|
|
|
|
return geo;
|
|
}
|
|
}
|
|
}
|
|
|