Add ManagedDialogPositionerDialogImplHelper

This commit is contained in:
chinboy
2025-09-09 09:19:30 +08:00
parent 64c413cd7f
commit 6d229e10eb
6 changed files with 308 additions and 17 deletions
@@ -8,7 +8,7 @@ public class DialogPositionRequest
internal DialogPositionRequest(Visual target,
DialogHorizontalPlacement horizontalPlacement,
DialogVerticalPlacement verticalPlacement,
Point? offset = null,
Point offset,
Rect? anchorRect = null,
CustomDialogPlacementCallback? placementCallback = null)
{
@@ -21,7 +21,8 @@ public class DialogPositionRequest
public Visual Target { get; }
public DialogHorizontalPlacement HorizontalPlacement { get; set; }
public DialogVerticalPlacement VerticalPlacement { get; set; }
public Point? Offset { get; }
public Point Offset { get; }
public Rect? AnchorRect { get; }
public DialogPositionerConstraintAdjustment ConstraintAdjustment {get;}
public CustomDialogPlacementCallback? PlacementCallback { get; }
}
@@ -5,13 +5,17 @@ namespace AtomUI.Controls.DialogPositioning;
public record CustomDialogPlacement
{
public DialogHorizontalPlacement HorizontalPlacement { get; set; } = DialogHorizontalPlacement.Center;
public DialogVerticalPlacement PlacementAnchor { get; set; } = DialogVerticalPlacement.Center;
public DialogVerticalPlacement VerticalPlacement { get; set; } = DialogVerticalPlacement.Center;
public Size DialogSize { get; }
public Visual Target { get; }
public Point Offset { get; set; }
public DialogPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
public Rect AnchorRectangle { get; set; }
internal CustomDialogPlacement(Size size)
internal CustomDialogPlacement(Size size, Visual target)
{
DialogSize = size;
Target = target;
}
}
@@ -2,13 +2,125 @@ using Avalonia;
namespace AtomUI.Controls.DialogPositioning;
public record struct PopupPositionerParameters
{
public Size Size { get; set; }
public Point Offset { get; set; }
}
public record struct DialogPositionerParameters
{
public Rect AnchorRectangle { get; set; }
public DialogHorizontalPlacement HorizontalPlacement { get; set; }
public DialogVerticalPlacement VerticalPlacement { get; set; }
public Size Size { get; set; }
public DialogPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
public Point Offset { get; set; }
}
public interface IDialogPositioner
{
void Update(PopupPositionerParameters parameters);
void Update(DialogPositionerParameters parameters);
}
[Flags]
public enum DialogPositionerConstraintAdjustment
{
/// <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>
/// 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 = 32,
All = SlideX|SlideY|ResizeX|ResizeY
}
internal static class DialogPositionerExtensions
{
public static void Update(
this IDialogPositioner positioner,
DialogPositionRequest positionRequest,
Size popupSize)
{
if (popupSize == default)
{
return;
}
var parameters = BuildParameters(positionRequest, popupSize);
positioner.Update(parameters);
}
private static DialogPositionerParameters BuildParameters(
DialogPositionRequest positionRequest,
Size dialogSize)
{
DialogPositionerParameters positionerParameters = default;
positionerParameters.Offset = positionRequest.Offset;
positionerParameters.Size = dialogSize;
positionerParameters.ConstraintAdjustment = positionRequest.ConstraintAdjustment;
if (positionRequest.HorizontalPlacement == DialogHorizontalPlacement.Custom ||
positionRequest.VerticalPlacement == DialogVerticalPlacement.Custom)
{
if (positionRequest.PlacementCallback is null)
throw new InvalidOperationException(
"CustomDialogPlacementCallback property must be set, when Placement=PlacementMode.Custom");
var customPlacementParameters = new CustomDialogPlacement(
dialogSize,
positionRequest.Target)
{
HorizontalPlacement = positionerParameters.HorizontalPlacement,
VerticalPlacement = positionerParameters.VerticalPlacement,
ConstraintAdjustment = positionerParameters.ConstraintAdjustment,
Offset = positionerParameters.Offset
};
positionRequest.PlacementCallback.Invoke(customPlacementParameters);
positionerParameters.HorizontalPlacement = customPlacementParameters.HorizontalPlacement;
positionerParameters.VerticalPlacement = customPlacementParameters.VerticalPlacement;
positionerParameters.ConstraintAdjustment = customPlacementParameters.ConstraintAdjustment;
positionerParameters.Offset = customPlacementParameters.Offset;
}
return positionerParameters;
}
}
@@ -1,3 +1,4 @@
using AtomUI.Utils;
using Avalonia;
namespace AtomUI.Controls.DialogPositioning;
@@ -31,7 +32,171 @@ public class ManagedDialogPositioner : IDialogPositioner
_dialog = dialog;
}
public void Update(PopupPositionerParameters parameters)
public void Update(DialogPositionerParameters parameters)
{
var rect = Calculate(
parameters.Size * _dialog.Scaling,
new Rect(
parameters.AnchorRectangle.TopLeft * _dialog.Scaling,
parameters.AnchorRectangle.Size * _dialog.Scaling),
parameters.HorizontalPlacement,
parameters.VerticalPlacement,
parameters.ConstraintAdjustment,
parameters.Offset * _dialog.Scaling);
_dialog.MoveAndResize(
rect.Position,
rect.Size / _dialog.Scaling);
}
private Rect Calculate(Size translatedSize,
Rect anchorRect,
DialogHorizontalPlacement horizontalPlacement,
DialogVerticalPlacement verticalPlacement,
DialogPositionerConstraintAdjustment constraintAdjustment,
Point offset)
{
var parentGeometry = _dialog.ParentClientAreaScreenGeometry;
anchorRect = anchorRect.Translate(parentGeometry.TopLeft);
Rect GetBounds()
{
var screens = _dialog.Screens;
var targetScreen = screens.FirstOrDefault(s => s.Bounds.ContainsExclusive(anchorRect.TopLeft))
?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect))
?? screens.FirstOrDefault(s => s.Bounds.ContainsExclusive(parentGeometry.TopLeft))
?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry))
?? screens.FirstOrDefault();
if (targetScreen != null &&
(targetScreen.WorkingArea.Width == 0 && targetScreen.WorkingArea.Height == 0))
{
return targetScreen.Bounds;
}
return targetScreen?.WorkingArea
?? new Rect(0, 0, double.MaxValue, double.MaxValue);
}
var bounds = GetBounds();
bool FitsInBounds(Rect rc, Direction direction)
{
if (direction == Direction.Left && rc.X < bounds.X ||
direction == Direction.Top && rc.Y < bounds.Y ||
direction == Direction.Right && rc.Right > bounds.Right ||
direction == Direction.Bottom && rc.Bottom > bounds.Bottom)
{
return false;
}
return true;
}
static bool IsValid(in Rect rc) => rc.Width > 0 && rc.Height > 0;
var offsetX = 0.0d;
var offsetY = 0.0d;
if (horizontalPlacement == DialogHorizontalPlacement.Left)
{
offsetX = 0;
}
else if (horizontalPlacement == DialogHorizontalPlacement.Right)
{
offsetX = bounds.Right - translatedSize.Width;
}
else if (horizontalPlacement == DialogHorizontalPlacement.Center)
{
offsetX = (bounds.Width - translatedSize.Width) / 2;
}
else
{
offsetX = offset.X;
}
if (verticalPlacement == DialogVerticalPlacement.Top)
{
offsetY = 0;
}
else if (verticalPlacement == DialogVerticalPlacement.Bottom)
{
offsetY = bounds.Bottom - translatedSize.Height;
}
else if (verticalPlacement == DialogVerticalPlacement.Center)
{
offsetY = (bounds.Height - translatedSize.Height) / 2;
}
else
{
offsetY = offset.Y;
}
Rect geo = new Rect(new Point(offsetX, offsetY), translatedSize);
// If sliding is allowed, try moving the rect into the bounds
if (constraintAdjustment.HasAllFlags(DialogPositionerConstraintAdjustment.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(DialogPositionerConstraintAdjustment.ResizeX))
{
var unconstrainedRect = geo;
if (!FitsInBounds(unconstrainedRect, Direction.Left))
{
unconstrainedRect = unconstrainedRect.WithX(bounds.X);
}
if (!FitsInBounds(unconstrainedRect, Direction.Right))
{
unconstrainedRect = unconstrainedRect.WithWidth(bounds.Width - unconstrainedRect.X);
}
if (IsValid(unconstrainedRect))
{
geo = unconstrainedRect;
}
}
// If sliding is allowed, try moving the rect into the bounds
if (constraintAdjustment.HasAllFlags(DialogPositionerConstraintAdjustment.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(DialogPositionerConstraintAdjustment.ResizeY))
{
var unconstrainedRect = geo;
if (!FitsInBounds(unconstrainedRect, Direction.Top))
{
unconstrainedRect = unconstrainedRect.WithY(bounds.Y);
}
if (!FitsInBounds(unconstrainedRect, Direction.Bottom))
{
unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y);
}
if (IsValid(unconstrainedRect))
{
geo = unconstrainedRect;
}
}
return geo;
}
}
@@ -38,7 +38,6 @@ public class ManagedDialogPositionerDialogImplHelper : IManagedDialogPositionerD
var point = _parent.PointToScreen(default);
var size = _parent.ClientSize * Scaling;
return new Rect(point.X, point.Y, size.Width, size.Height);
}
}
@@ -244,20 +244,30 @@ public sealed class OverlayDialogHost : ContentControl,
if (_needsUpdate && _dialogPositionRequest is not null)
{
_needsUpdate = false;
// _positioner.Update(TopLevel.GetTopLevel(_overlayLayer)!, _popupPositionRequest, _popupSize, FlowDirection);
_positioner.Update(_dialogPositionRequest, _dialogSize);
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsDragMovableProperty)
if (change.Property == IsModalProperty)
{
if (!IsHidden)
{
// TODO 需要重构,这样不行
Hide();
Show();
if (change.OldValue is bool oldValue && oldValue && _dialogMask != null)
{
_dialogLayer.Children.Remove(_dialogMask);
}
if (change.NewValue is bool newValue && newValue)
{
// 找到自己
var index = _dialogLayer.Children.IndexOf(this);
_dialogMask ??= new OverlayDialogMask();
_dialogLayer.Children.Insert(index, _dialogMask);
ConfigureMaskSize(_dialogLayer.Bounds.Size);
}
}
}
}