diff --git a/src/AtomUI.Controls/Dialog/DialogPositionRequest.cs b/src/AtomUI.Controls/Dialog/DialogPositionRequest.cs index 09c67c691..859c719a2 100644 --- a/src/AtomUI.Controls/Dialog/DialogPositionRequest.cs +++ b/src/AtomUI.Controls/Dialog/DialogPositionRequest.cs @@ -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; } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Dialog/DialogPositioning/CustomDialogPlacement.cs b/src/AtomUI.Controls/Dialog/DialogPositioning/CustomDialogPlacement.cs index 578fa39fd..f63b7936c 100644 --- a/src/AtomUI.Controls/Dialog/DialogPositioning/CustomDialogPlacement.cs +++ b/src/AtomUI.Controls/Dialog/DialogPositioning/CustomDialogPlacement.cs @@ -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; } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Dialog/DialogPositioning/IDialogPositioner.cs b/src/AtomUI.Controls/Dialog/DialogPositioning/IDialogPositioner.cs index 29d339ee5..1455193b9 100644 --- a/src/AtomUI.Controls/Dialog/DialogPositioning/IDialogPositioner.cs +++ b/src/AtomUI.Controls/Dialog/DialogPositioning/IDialogPositioner.cs @@ -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 +{ + /// + /// Don't alter the surface position even if it is constrained on some + /// axis, for example partially outside the edge of an output. + /// + None = 0, + + /// + /// 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. + /// + SlideX = 1, + + /// + /// 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. + /// + SlideY = 2, + + /// + /// Horizontally resize the surface + /// + /// + /// Resize the surface horizontally so that it is completely unconstrained. + /// + ResizeX = 16, + + /// + /// Vertically resize the surface + /// + /// + /// Resize the surface vertically so that it is completely unconstrained. + /// + 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; + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Dialog/DialogPositioning/ManagedDialogPositioner.cs b/src/AtomUI.Controls/Dialog/DialogPositioning/ManagedDialogPositioner.cs index 6ae272abe..7e12d6086 100644 --- a/src/AtomUI.Controls/Dialog/DialogPositioning/ManagedDialogPositioner.cs +++ b/src/AtomUI.Controls/Dialog/DialogPositioning/ManagedDialogPositioner.cs @@ -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; + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Dialog/DialogPositioning/ManagedDialogPositionerDialogImplHelper.cs b/src/AtomUI.Controls/Dialog/DialogPositioning/ManagedDialogPositionerDialogImplHelper.cs index b8e6d74ee..a160d444f 100644 --- a/src/AtomUI.Controls/Dialog/DialogPositioning/ManagedDialogPositionerDialogImplHelper.cs +++ b/src/AtomUI.Controls/Dialog/DialogPositioning/ManagedDialogPositionerDialogImplHelper.cs @@ -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); - } } diff --git a/src/AtomUI.Controls/Dialog/OverlayDialogHost.cs b/src/AtomUI.Controls/Dialog/OverlayDialogHost.cs index e98a25cf0..48391b96c 100644 --- a/src/AtomUI.Controls/Dialog/OverlayDialogHost.cs +++ b/src/AtomUI.Controls/Dialog/OverlayDialogHost.cs @@ -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); + } } } }