完成 popup flip 功能

This commit is contained in:
polarboy 2024-06-30 16:39:05 +08:00
parent 900a57f414
commit f43a41f825
14 changed files with 650 additions and 241 deletions

View File

@ -56,7 +56,7 @@ public partial class ArrowDecoratedBox : IControlCustomStyle
_controlTokenBinder.AddControlBinding(MinHeightProperty, GlobalResourceKey.ControlHeight);
_controlTokenBinder.AddControlBinding(PaddingProperty, GlobalResourceKey.PaddingXS);
_controlTokenBinder.AddControlBinding(ArrowSizeTokenProperty, ArrowDecoratedBoxResourceKey.ArrowSize);
_controlTokenBinder.AddControlBinding(BackgroundProperty, GlobalResourceKey.ColorPrimary);
_controlTokenBinder.AddControlBinding(BackgroundProperty, GlobalResourceKey.ColorBgContainer);
_controlTokenBinder.AddControlBinding(CornerRadiusProperty, GlobalResourceKey.BorderRadius);
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives.PopupPositioning;
@ -7,8 +8,25 @@ using Avalonia.Styling;
namespace AtomUI.Controls;
using PopupControl = Popup;
public class Flyout : PopupFlyoutBase
{
/// <summary>
/// 是否显示指示箭头
/// </summary>
public static readonly StyledProperty<bool> IsShowArrowProperty =
ArrowDecoratedBox.IsShowArrowProperty.AddOwner<PopupFlyoutBase>();
public static readonly StyledProperty<bool> IsShowArrowEffectiveProperty =
ArrowDecoratedBox.IsShowArrowProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// 箭头是否始终指向中心
/// </summary>
public static readonly StyledProperty<bool> IsPointAtCenterProperty =
AvaloniaProperty.Register<PopupFlyoutBase, bool>(nameof(IsPointAtCenter), false);
/// <summary>
/// Defines the <see cref="Content"/> property
/// </summary>
@ -28,6 +46,27 @@ public class Flyout : PopupFlyoutBase
public static readonly StyledProperty<ControlTheme?> FlyoutPresenterThemeProperty =
AvaloniaProperty.Register<Flyout, ControlTheme?>(nameof(FlyoutPresenterTheme));
public bool IsShowArrow
{
get => GetValue(IsShowArrowProperty);
set => SetValue(IsShowArrowProperty, value);
}
/// <summary>
/// 是否实际显示箭头
/// </summary>
public bool IsShowArrowEffective
{
get => GetValue(IsShowArrowEffectiveProperty);
set => SetValue(IsShowArrowEffectiveProperty, value);
}
public bool IsPointAtCenter
{
get => GetValue(IsPointAtCenterProperty);
set => SetValue(IsPointAtCenterProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for the flyout presenter.
/// </summary>
@ -49,10 +88,12 @@ public class Flyout : PopupFlyoutBase
protected override Control CreatePresenter()
{
return new FlyoutPresenter
var presenter = new FlyoutPresenter
{
[!BorderedStyleControl.ChildProperty] = this[!ContentProperty]
};
BindUtils.RelayBind(this, IsShowArrowEffectiveProperty, presenter, IsShowArrowProperty);
return presenter;
}
protected override void OnOpening(CancelEventArgs args)
@ -73,12 +114,66 @@ public class Flyout : PopupFlyoutBase
/// <summary>
/// 判断是否可以启用箭头,有些组合是不能启用箭头绘制的,因为没有意义
/// </summary>
/// <param name="placementMode"></param>
/// <param name="placement"></param>
/// <param name="anchor"></param>
/// <param name="gravity"></param>
/// <returns></returns>
private bool CanEnabledArrow(PlacementMode placementMode, PopupAnchor? anchor, PopupGravity? gravity)
private bool CanEnabledArrow(PlacementMode placement, PopupAnchor? anchor, PopupGravity? gravity)
{
return true;
if (placement == PlacementMode.Center ||
placement == PlacementMode.Pointer) {
return false;
}
return PopupControl.IsCanonicalAnchorType(placement, anchor, gravity);
}
private Point CalculatePopupPositionDelta(Control anchorTarget, PlacementMode placement, PopupAnchor? anchor = null,
PopupGravity? gravity = null)
{
var offsetX = 0d;
var offsetY = 0d;
if (IsShowArrow && IsPointAtCenter) {
if (CanEnabledArrow(placement, anchor, gravity)) {
if (Popup.Child is ArrowDecoratedBox arrowDecoratedBox) {
var arrowVertexPoint = arrowDecoratedBox.ArrowVertexPoint;
var anchorSize = anchorTarget.Bounds.Size;
var centerX = anchorSize.Width / 2;
var centerY = anchorSize.Height / 2;
// 这里计算不需要全局坐标
if (placement == PlacementMode.TopEdgeAlignedLeft ||
placement == PlacementMode.BottomEdgeAlignedLeft) {
offsetX += centerX - arrowVertexPoint.Item1;
} else if (placement == PlacementMode.TopEdgeAlignedRight ||
placement == PlacementMode.BottomEdgeAlignedRight) {
offsetX -= centerX - arrowVertexPoint.Item2;
} else if (placement == PlacementMode.RightEdgeAlignedTop ||
placement == PlacementMode.LeftEdgeAlignedTop) {
offsetY += centerY - arrowVertexPoint.Item1;
} else if (placement == PlacementMode.RightEdgeAlignedBottom ||
placement == PlacementMode.LeftEdgeAlignedBottom) {
offsetY -= centerY - arrowVertexPoint.Item2;
}
}
}
}
return new Point(offsetX, offsetY);
}
// 因为在某些 placement 下箭头是不能显示的
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == IsShowArrowProperty ||
e.Property == PlacementProperty ||
e.Property == PlacementAnchorProperty ||
e.Property == PlacementGravityProperty) {
if (IsShowArrow == false) {
IsShowArrowEffective = false;
} else {
IsShowArrowEffective = CanEnabledArrow(Placement, PlacementAnchor, PlacementGravity);
}
}
}
}

View File

@ -1,4 +1,6 @@
using System.Reactive.Disposables;
using AtomUI.Data;
using AtomUI.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
@ -10,6 +12,8 @@ using Avalonia.Metadata;
namespace AtomUI.Controls;
using FlyoutControl = Flyout;
public enum FlyoutTriggerType
{
Hover,
@ -44,19 +48,14 @@ public class FlyoutHost : Control
/// 箭头是否始终指向中心
/// </summary>
public static readonly StyledProperty<bool> IsPointAtCenterProperty =
PopupFlyoutBase.IsPointAtCenterProperty.AddOwner<FlyoutHost>();
/// <summary>
/// Defines the ToolTip.Placement property.
/// </summary>
FlyoutControl.IsPointAtCenterProperty.AddOwner<FlyoutHost>();
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementProperty.AddOwner<FlyoutHost>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner<FlyoutHost>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<FlyoutHost>();
@ -66,7 +65,7 @@ public class FlyoutHost : Control
/// 还有些 anchor 和 gravity 的组合也没有用
/// </summary>
public static readonly StyledProperty<double> MarginToAnchorProperty =
AvaloniaProperty.Register<FlyoutHost, double>(nameof(MarginToAnchor), 0);
Popup.MarginToAnchorProperty.AddOwner<FlyoutHost>();
public static readonly StyledProperty<int> ShowDelayProperty =
AvaloniaProperty.Register<FlyoutHost, int>(nameof(ShowDelay), 400);
@ -83,10 +82,7 @@ public class FlyoutHost : Control
get => GetValue(AnchorTargetProperty);
set => SetValue(AnchorTargetProperty, value);
}
/// <summary>
/// Gets or sets the Flyout that should be shown with this button.
/// </summary>
public PopupFlyoutBase? Flyout
{
get => GetValue(FlyoutProperty);
@ -116,15 +112,13 @@ public class FlyoutHost : Control
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <inheritdoc cref="Popup.PlacementGravity"/>
public PopupGravity PlacementGravity
{
get => GetValue(PlacementGravityProperty);
set => SetValue(PlacementGravityProperty, value);
}
/// <inheritdoc cref="Popup.PlacementAnchor"/>
public PopupAnchor PlacementAnchor
{
get => GetValue(PlacementAnchorProperty);
@ -151,6 +145,7 @@ public class FlyoutHost : Control
private bool _initialized = false;
private CompositeDisposable _compositeDisposable;
private GlobalTokenBinder _globalTokenBinder;
static FlyoutHost()
{
@ -160,11 +155,7 @@ public class FlyoutHost : Control
public FlyoutHost()
{
_compositeDisposable = new CompositeDisposable();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
_globalTokenBinder = new GlobalTokenBinder();
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
@ -175,6 +166,7 @@ public class FlyoutHost : Control
((ISetLogicalParent)AnchorTarget).SetParent(this);
VisualChildren.Add(AnchorTarget);
}
_globalTokenBinder.AddGlobalBinding(this, MarginToAnchorProperty, GlobalResourceKey.MarginXXS);
_initialized = true;
}
}
@ -200,6 +192,7 @@ public class FlyoutHost : Control
_compositeDisposable.Add(BindUtils.RelayBind(this, PlacementGravityProperty, Flyout));
_compositeDisposable.Add(BindUtils.RelayBind(this, IsShowArrowProperty, Flyout));
_compositeDisposable.Add(BindUtils.RelayBind(this, IsPointAtCenterProperty, Flyout));
_compositeDisposable.Add(BindUtils.RelayBind(this, MarginToAnchorProperty, Flyout));
}
}

View File

@ -1,5 +1,4 @@
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input;
using Avalonia.LogicalTree;
namespace AtomUI.Controls;

View File

@ -1,6 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Metadata;
using Avalonia.Controls;
namespace AtomUI.Controls;

View File

@ -1,49 +1,26 @@
using Avalonia;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
namespace AtomUI.Controls;
using AvaloniaPopupFlyoutBase = Avalonia.Controls.Primitives.PopupFlyoutBase;
using PopupControl = Popup;
/// <summary>
/// 最基本得弹窗 Flyout在这里不处理那种带箭头得
/// </summary>
public abstract class PopupFlyoutBase : AvaloniaPopupFlyoutBase
{
/// <summary>
/// 是否显示指示箭头
/// </summary>
public static readonly StyledProperty<bool> IsShowArrowProperty =
ArrowDecoratedBox.IsShowArrowProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// 箭头是否始终指向中心
/// </summary>
public static readonly StyledProperty<bool> IsPointAtCenterProperty =
AvaloniaProperty.Register<PopupFlyoutBase, bool>(nameof(IsPointAtCenter), false);
/// <summary>
/// 距离 anchor 的边距,根据垂直和水平进行设置
/// 但是对某些组合无效,比如跟随鼠标的情况
/// 还有些 anchor 和 gravity 的组合也没有用
/// </summary>
public static readonly StyledProperty<double> MarginToAnchorProperty =
AvaloniaProperty.Register<PopupFlyoutBase, double>(nameof(MarginToAnchor), 0);
public bool IsShowArrow
{
get => GetValue(IsShowArrowProperty);
set => SetValue(IsShowArrowProperty, value);
}
public bool IsPointAtCenter
{
get => GetValue(IsPointAtCenterProperty);
set => SetValue(IsPointAtCenterProperty, value);
}
PopupControl.MarginToAnchorProperty.AddOwner<PopupFlyoutBase>();
public double MarginToAnchor
{
get => GetValue(MarginToAnchorProperty);
@ -68,8 +45,11 @@ public abstract class PopupFlyoutBase : AvaloniaPopupFlyoutBase
presenter.Classes.AddRange(classes);
}
protected internal virtual void NotifyPopupCreated(Popup popup) { }
protected internal virtual void NotifyPopupCreated(Popup popup)
{
BindUtils.RelayBind(this, MarginToAnchorProperty, popup);
}
protected internal virtual void NotifyPositionPopup(bool showAtPointer)
{
Size sz;
@ -83,8 +63,10 @@ public abstract class PopupFlyoutBase : AvaloniaPopupFlyoutBase
Popup.VerticalOffset = VerticalOffset;
Popup.HorizontalOffset = HorizontalOffset;
Popup.PlacementAnchor = PlacementAnchor;
Popup.PlacementGravity = PlacementGravity;
if (showAtPointer) {
Popup.Placement = PlacementMode.Pointer;
} else {
@ -92,14 +74,4 @@ public abstract class PopupFlyoutBase : AvaloniaPopupFlyoutBase
Popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
}
}
protected override void OnOpened()
{
base.OnOpened();
}
protected override void OnClosed()
{
base.OnClosed();
}
}

View File

@ -63,9 +63,15 @@ internal static class PopupFlyoutBaseInterceptor
return false;
}
public static void UpdateHostPositionInterceptor(AbstractPopup __instance, IPopupHost popupHost, Control placementTarget)
public static void UpdateHostPositionPostfixInterceptor(AbstractPopup __instance, IPopupHost popupHost, Control placementTarget)
{
__instance.NotifyPopupHostPositionUpdated(popupHost, placementTarget);
__instance.NotifyHostPositionUpdated(popupHost, placementTarget);
}
public static bool UpdateHostPositionPrefixInterceptor(AbstractPopup __instance, IPopupHost popupHost, Control placementTarget)
{
__instance.NotifyAboutToUpdateHostPosition(popupHost, placementTarget);
return true;
}
public static bool PositionPopupInterceptor(PopupFlyoutBase __instance, bool showAtPointer)
@ -80,7 +86,8 @@ internal static class PopupFlyoutBaseInterceptorRegister
public static void Register(Harmony harmony)
{
RegisterPopupFlyoutBaseCreatePopup(harmony);
RegisterPopupUpdateHostPosition(harmony);
RegisterPopupUpdateHostPositionPrefix(harmony);
RegisterPopupUpdateHostPositionPostfix(harmony);
RegisterPopupPositionPopup(harmony);
}
@ -93,11 +100,20 @@ internal static class PopupFlyoutBaseInterceptorRegister
harmony.Patch(origin, prefix: new HarmonyMethod(prefixInterceptor));
}
private static void RegisterPopupUpdateHostPosition(Harmony harmony)
private static void RegisterPopupUpdateHostPositionPrefix(Harmony harmony)
{
var origin = typeof(AvaloniaPopup).GetMethod("UpdateHostPosition", BindingFlags.Instance | BindingFlags.NonPublic);
var prefixInterceptor = typeof(PopupFlyoutBaseInterceptor)
.GetMethod(nameof(PopupFlyoutBaseInterceptor.UpdateHostPositionPrefixInterceptor),
BindingFlags.Static | BindingFlags.Public);
harmony.Patch(origin, prefix: new HarmonyMethod(prefixInterceptor));
}
private static void RegisterPopupUpdateHostPositionPostfix(Harmony harmony)
{
var origin = typeof(AvaloniaPopup).GetMethod("UpdateHostPosition", BindingFlags.Instance | BindingFlags.NonPublic);
var postfixInterceptor = typeof(PopupFlyoutBaseInterceptor)
.GetMethod(nameof(PopupFlyoutBaseInterceptor.UpdateHostPositionInterceptor),
.GetMethod(nameof(PopupFlyoutBaseInterceptor.UpdateHostPositionPostfixInterceptor),
BindingFlags.Static | BindingFlags.Public);
harmony.Patch(origin, postfix: new HarmonyMethod(postfixInterceptor));
}

View File

@ -1,6 +1,8 @@
using Avalonia.Controls;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
namespace AtomUI.Controls;
@ -8,7 +10,19 @@ using AvaloniaPopup = Avalonia.Controls.Primitives.Popup;
public abstract class AbstractPopup : AvaloniaPopup
{
public event EventHandler<EventArgs>? PopupHostCreated;
public event EventHandler<EventArgs>? AboutToClosing;
private static readonly FieldInfo ManagedPopupPositionerPopupInfo;
protected IManagedPopupPositionerPopup? _managedPopupPositioner; // 在弹窗有效期获取
protected internal WeakReference<IPopupHost>? _popupHost;
static AbstractPopup()
{
ManagedPopupPositionerPopupInfo = typeof(ManagedPopupPositioner).GetField("_popup",
BindingFlags.Instance | BindingFlags.NonPublic)!;
}
public AbstractPopup()
{
(this as IPopupHostProvider).PopupHostChanged += HandlePopupChanged;
@ -22,15 +36,44 @@ public abstract class AbstractPopup : AvaloniaPopup
}
}
protected internal virtual void NotifyPopupHostPositionUpdated(IPopupHost popupHost, Control placementTarget)
protected internal virtual void NotifyHostPositionUpdated(IPopupHost popupHost, Control placementTarget)
{
if (_popupHost is null) {
_popupHost = new WeakReference<IPopupHost>(popupHost);
NotifyPopupHostCreated(popupHost);
}
}
// 开始定位 Host 窗口
protected internal virtual void NotifyAboutToUpdateHostPosition(IPopupHost popupHost, Control placementTarget)
{
if (popupHost is PopupRoot popupRoot) {
if (popupRoot.PlatformImpl?.PopupPositioner is ManagedPopupPositioner managedPopupPositioner) {
_managedPopupPositioner =
ManagedPopupPositionerPopupInfo.GetValue(managedPopupPositioner) as IManagedPopupPositionerPopup;
}
}
}
protected virtual void NotifyPopupHostCreated(IPopupHost popupHost) {}
protected internal virtual void NotifyAboutToClosing() {}
protected virtual void NotifyPopupHostCreated(IPopupHost popupHost)
{
PopupHostCreated?.Invoke(this, new PopupHostCreatedEventArgs(popupHost));
}
protected internal virtual void NotifyAboutToClosing()
{
AboutToClosing?.Invoke(this, EventArgs.Empty);
}
protected virtual void NotifyClosed() {}
}
public class PopupHostCreatedEventArgs : EventArgs
{
public IPopupHost PopupHost { get; }
public PopupHostCreatedEventArgs(IPopupHost host)
{
PopupHost = host;
}
}

View File

@ -7,6 +7,4 @@ public interface IShadowDecorator
public BoxShadows MaskShadows { get; set; }
public void AttachToTarget(Popup host);
public void DetachedFromTarget(Popup host);
public void ShowShadows();
public void HideShadows();
}

View File

@ -13,7 +13,7 @@ public class LiteWindow : WindowBase, IHostedVisualTreeRoot, IDisposable
{
BackgroundProperty.OverrideDefaultValue(typeof(LiteWindow), Brushes.White);
}
public LiteWindow(TopLevel parent, IPopupImpl impl)
: this(parent, impl, null) { }
@ -36,7 +36,7 @@ public class LiteWindow : WindowBase, IHostedVisualTreeRoot, IDisposable
/// Gets the platform-specific window implementation.
/// </summary>
public new IPopupImpl? PlatformImpl => (IPopupImpl?)base.PlatformImpl;
/// <summary>
/// Gets the control that is hosting the popup root.
/// </summary>
@ -57,7 +57,6 @@ public class LiteWindow : WindowBase, IHostedVisualTreeRoot, IDisposable
public TopLevel ParentTopLevel { get; }
/// <inheritdoc/>
public void Dispose()
{
PlatformImpl?.Dispose();

View File

@ -1,9 +1,13 @@
using System.Reactive.Disposables;
using System.Reflection;
using AtomUI.Data;
using AtomUI.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
@ -14,44 +18,390 @@ public class Popup : AbstractPopup
public static readonly StyledProperty<BoxShadows> MaskShadowsProperty =
Border.BoxShadowProperty.AddOwner<Popup>();
public static readonly StyledProperty<double> MarginToAnchorProperty =
AvaloniaProperty.Register<Popup, double>(nameof(MarginToAnchor));
public BoxShadows MaskShadows
{
get => GetValue(MaskShadowsProperty);
set => SetValue(MaskShadowsProperty, value);
}
public double MarginToAnchor
{
get => GetValue(MarginToAnchorProperty);
set => SetValue(MarginToAnchorProperty, value);
}
private PopupShadowLayer _shadowLayer;
private static readonly MethodInfo ConfigurePositionMethodInfo;
private PopupShadowLayer? _shadowLayer;
private GlobalTokenBinder _globalTokenBinder;
private CompositeDisposable _compositeDisposable;
private CompositeDisposable? _compositeDisposable;
private bool _initialized;
static Popup()
{
var type = typeof(IPopupPositioner).Assembly.GetType("Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerExtensions")!;
ConfigurePositionMethodInfo = type.GetMethod("ConfigurePosition", BindingFlags.Public | BindingFlags.Static)!;
}
public Popup()
{
IsLightDismissEnabled = false;
_shadowLayer = new PopupShadowLayer();
_shadowLayer.AttachToTarget(this);
_globalTokenBinder = new GlobalTokenBinder();
_compositeDisposable = new CompositeDisposable();
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
if (!_initialized) {
_globalTokenBinder.AddGlobalBinding(this, MaskShadowsProperty, GlobalResourceKey.BoxShadowsSecondary);
_compositeDisposable.Add(BindUtils.RelayBind(this, MaskShadowsProperty, _shadowLayer));
_globalTokenBinder.AddGlobalBinding(this, MaskShadowsProperty, GlobalResourceKey.BoxShadowsSecondary);
_initialized = true;
}
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
_compositeDisposable.Dispose();
_compositeDisposable?.Dispose();
}
private PopupShadowLayer CreateShadowLayer()
protected override void NotifyPopupHostCreated(IPopupHost popupHost)
{
return default!;
base.NotifyPopupHostCreated(popupHost);
if (PlacementTarget is not null) {
var toplevel = TopLevel.GetTopLevel(PlacementTarget);
if (toplevel is null) {
throw new InvalidOperationException(
"Unable to create shadow layer, top level for PlacementTarget is null.");
}
_compositeDisposable = new CompositeDisposable();
_shadowLayer = new PopupShadowLayer(toplevel);
_compositeDisposable?.Add(BindUtils.RelayBind(this, MaskShadowsProperty, _shadowLayer!));
_shadowLayer.AttachToTarget(this);
}
}
protected override void NotifyClosed()
{
base.NotifyClosed();
_compositeDisposable?.Dispose();
_shadowLayer = null;
}
internal (bool, bool) CalculateFlipInfo(Size translatedSize, Rect anchorRect, PopupAnchor anchor,
PopupGravity gravity,
Point offset)
{
var result = (false, false);
var bounds = GetBounds(anchorRect);
offset *= _managedPopupPositioner!.Scaling;
bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask)
{
if (edge.HasFlag(PopupAnchor.Left) && rc.X < bounds.X ||
edge.HasFlag(PopupAnchor.Top) && rc.Y < bounds.Y ||
edge.HasFlag(PopupAnchor.Right) && rc.Right > bounds.Right ||
edge.HasFlag(PopupAnchor.Bottom) && rc.Bottom > bounds.Bottom) {
return false;
}
return true;
}
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)) {
result.Item1 = true;
}
if (!FitsInBounds(geo, PopupAnchor.VerticalMask)) {
result.Item2 = true;
}
return result;
}
private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity)
{
double x, y;
if (gravity.HasFlag(PopupGravity.Left)) {
x = -size.Width;
} else if (gravity.HasFlag(PopupGravity.Right)) {
x = 0;
} else {
x = -size.Width / 2;
}
if (gravity.HasFlag(PopupGravity.Top)) {
y = -size.Height;
} else if (gravity.HasFlag(PopupGravity.Bottom)) {
y = 0;
} else {
y = -size.Height / 2;
}
return anchorPoint + new Point(x, y);
}
private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge)
{
double x, y;
if (edge.HasFlag(PopupAnchor.Left)) {
x = anchorRect.X;
} else if (edge.HasFlag(PopupAnchor.Right)) {
x = anchorRect.Right;
} else {
x = anchorRect.X + anchorRect.Width / 2;
}
if (edge.HasFlag(PopupAnchor.Top)) {
y = anchorRect.Y;
} else if (edge.HasFlag(PopupAnchor.Bottom)) {
y = anchorRect.Bottom;
} else {
y = anchorRect.Y + anchorRect.Height / 2;
}
return new Point(x, y);
}
private Rect GetBounds(Rect anchorRect)
{
// 暂时只支持窗口的方式
if (_managedPopupPositioner is null) {
throw new InvalidOperationException("ManagedPopupPositioner is null");
}
var parentGeometry = _managedPopupPositioner.ParentClientAreaScreenGeometry;
var screens = _managedPopupPositioner.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);
}
public static void ConfigurePosition(ref PopupPositionerParameters positionerParameters,
TopLevel topLevel,
Visual target, PlacementMode placement, Point offset,
PopupAnchor anchor, PopupGravity gravity,
PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect,
FlowDirection flowDirection)
{
var arguments = new object?[]
{
positionerParameters,
topLevel,
target,
placement,
offset,
anchor,
gravity,
constraintAdjustment,
rect,
flowDirection
};
ConfigurePositionMethodInfo.Invoke(null, arguments);
positionerParameters = (PopupPositionerParameters)arguments[0]!;
}
internal static Direction GetDirection(PlacementMode placement)
{
return placement switch
{
PlacementMode.Left => Direction.Left,
PlacementMode.LeftEdgeAlignedBottom => Direction.Left,
PlacementMode.LeftEdgeAlignedTop => Direction.Left,
PlacementMode.Top => Direction.Top,
PlacementMode.TopEdgeAlignedLeft => Direction.Top,
PlacementMode.TopEdgeAlignedRight => Direction.Top,
PlacementMode.Right => Direction.Right,
PlacementMode.RightEdgeAlignedBottom => Direction.Right,
PlacementMode.RightEdgeAlignedTop => Direction.Right,
PlacementMode.Bottom => Direction.Bottom,
PlacementMode.BottomEdgeAlignedLeft => Direction.Bottom,
PlacementMode.BottomEdgeAlignedRight => Direction.Bottom,
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, "Invalid value for PlacementMode")
};
}
private Point CalculateMarginToAnchorOffset(PlacementMode placement)
{
var offsetX = 0d;
var offsetY = 0d;
if (placement != PlacementMode.Center &&
placement != PlacementMode.Pointer &&
IsCanonicalAnchorType(placement, PlacementAnchor, PlacementGravity)) {
var direction = GetDirection(placement);
if (direction == Direction.Bottom) {
offsetY += MarginToAnchor;
} else if (direction == Direction.Top) {
offsetY += -MarginToAnchor;
} else if (direction == Direction.Left) {
offsetX += -MarginToAnchor;
} else {
offsetX += MarginToAnchor;
}
} else if (placement == PlacementMode.Pointer) {
offsetX += MarginToAnchor;
offsetY += MarginToAnchor;
}
return new Point(offsetX, offsetY);
}
/// <summary>
/// 是否为标准的 anchor 类型
/// </summary>
/// <param name="placement"></param>
/// <param name="anchor"></param>
/// <param name="gravity"></param>
/// <returns></returns>
internal static bool IsCanonicalAnchorType(PlacementMode placement, PopupAnchor? anchor, PopupGravity? gravity)
{
if (placement == PlacementMode.AnchorAndGravity) {
switch (anchor, gravity) {
case (PopupAnchor.Bottom, PopupGravity.Bottom):
case (PopupAnchor.Right, PopupGravity.Right):
case (PopupAnchor.Left, PopupGravity.Left):
case (PopupAnchor.Top, PopupGravity.Top):
case (PopupAnchor.TopRight, PopupGravity.TopLeft):
case (PopupAnchor.TopLeft, PopupGravity.TopRight):
case (PopupAnchor.BottomLeft, PopupGravity.BottomRight):
case (PopupAnchor.BottomRight, PopupGravity.BottomLeft):
case (PopupAnchor.TopLeft, PopupGravity.BottomLeft):
case (PopupAnchor.BottomLeft, PopupGravity.TopLeft):
case (PopupAnchor.TopRight, PopupGravity.BottomRight):
case (PopupAnchor.BottomRight, PopupGravity.TopRight):
break;
default:
return false;
}
}
return true;
}
protected internal override void NotifyAboutToUpdateHostPosition(IPopupHost popupHost, Control placementTarget)
{
base.NotifyAboutToUpdateHostPosition(popupHost, placementTarget);
var offsetX = HorizontalOffset;
var offsetY = VerticalOffset;
var marginToAnchorOffset = CalculateMarginToAnchorOffset(Placement);
offsetX += marginToAnchorOffset.X;
offsetY += marginToAnchorOffset.Y;
HorizontalOffset = offsetX;
VerticalOffset = offsetY;
if (Placement != PlacementMode.Center &&
Placement != PlacementMode.Pointer) {
// 计算是否 flip
PopupPositionerParameters parameters = new PopupPositionerParameters();
if (popupHost is PopupRoot popupRoot) {
var offset = new Point(HorizontalOffset, VerticalOffset);
ConfigurePosition(ref parameters, popupRoot.ParentTopLevel,
PlacementTarget!,
Placement,
offset,
PlacementAnchor,
PlacementGravity,
PopupPositionerConstraintAdjustment.All,
null,
FlowDirection);
Size popupSize;
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Child!.DesiredSize == default) {
// Popup may not have been shown yet. Measure content
popupSize = LayoutHelper.MeasureChild(Child, Size.Infinity, new Thickness());
} else {
popupSize = Child.DesiredSize;
}
var scaling = _managedPopupPositioner!.Scaling;
var anchorRect = new Rect(
parameters.AnchorRectangle.TopLeft * scaling,
parameters.AnchorRectangle.Size * scaling);
anchorRect = anchorRect.Translate(_managedPopupPositioner.ParentClientAreaScreenGeometry.TopLeft);
var flipInfo = CalculateFlipInfo(popupSize * scaling,
anchorRect,
parameters.Anchor,
parameters.Gravity,
offset);
if (flipInfo.Item1 || flipInfo.Item2) {
var flipPlacement = GetFlipPlacement(Placement);
var flipAnchorAndGravity = GetAnchorAndGravity(flipPlacement);
var flipOffset = CalculateMarginToAnchorOffset(flipPlacement);
Placement = flipPlacement;
PlacementAnchor = flipAnchorAndGravity.Item1;
PlacementGravity = flipAnchorAndGravity.Item2;
HorizontalOffset = flipOffset.X;
VerticalOffset = flipOffset.Y;
}
}
}
}
private (PopupAnchor, PopupGravity) GetAnchorAndGravity(PlacementMode placement)
{
return placement switch
{
PlacementMode.Bottom => (PopupAnchor.Bottom, PopupGravity.Bottom),
PlacementMode.Right => (PopupAnchor.Right, PopupGravity.Right),
PlacementMode.Left => (PopupAnchor.Left, PopupGravity.Left),
PlacementMode.Top => (PopupAnchor.Top, PopupGravity.Top),
PlacementMode.TopEdgeAlignedRight => (PopupAnchor.TopRight, PopupGravity.TopLeft),
PlacementMode.TopEdgeAlignedLeft => (PopupAnchor.TopLeft, PopupGravity.TopRight),
PlacementMode.BottomEdgeAlignedLeft => (PopupAnchor.BottomLeft, PopupGravity.BottomRight),
PlacementMode.BottomEdgeAlignedRight => (PopupAnchor.BottomRight, PopupGravity.BottomLeft),
PlacementMode.LeftEdgeAlignedTop => (PopupAnchor.TopLeft, PopupGravity.BottomLeft),
PlacementMode.LeftEdgeAlignedBottom => (PopupAnchor.BottomLeft, PopupGravity.TopLeft),
PlacementMode.RightEdgeAlignedTop => (PopupAnchor.TopRight, PopupGravity.BottomRight),
PlacementMode.RightEdgeAlignedBottom => (PopupAnchor.BottomRight, PopupGravity.TopRight),
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, "Invalid value for PlacementMode")
};
}
protected PlacementMode GetFlipPlacement(PlacementMode placement)
{
return placement switch
{
PlacementMode.Left => PlacementMode.Right,
PlacementMode.LeftEdgeAlignedTop => PlacementMode.RightEdgeAlignedTop,
PlacementMode.LeftEdgeAlignedBottom => PlacementMode.RightEdgeAlignedBottom,
PlacementMode.Top => PlacementMode.Bottom,
PlacementMode.TopEdgeAlignedLeft => PlacementMode.BottomEdgeAlignedLeft,
PlacementMode.TopEdgeAlignedRight => PlacementMode.BottomEdgeAlignedRight,
PlacementMode.Right => PlacementMode.Left,
PlacementMode.RightEdgeAlignedTop => PlacementMode.LeftEdgeAlignedTop,
PlacementMode.RightEdgeAlignedBottom => PlacementMode.LeftEdgeAlignedBottom,
PlacementMode.Bottom => PlacementMode.Top,
PlacementMode.BottomEdgeAlignedLeft => PlacementMode.TopEdgeAlignedLeft,
PlacementMode.BottomEdgeAlignedRight => PlacementMode.TopEdgeAlignedRight,
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, "Invalid value for PlacementMode")
};
}
}

View File

@ -2,7 +2,6 @@
using System.Reflection;
using AtomUI.Media;
using AtomUI.Platform.Windows;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
@ -11,76 +10,65 @@ using Avalonia.Media;
namespace AtomUI.Controls;
internal class PopupShadowLayer : AbstractPopup, IShadowDecorator
internal class PopupShadowLayer : LiteWindow, IShadowDecorator
{
public static readonly StyledProperty<BoxShadows> MaskShadowsProperty =
Border.BoxShadowProperty.AddOwner<PopupShadowLayer>();
public static readonly StyledProperty<BoxShadows> MaskShadowsProperty =
Border.BoxShadowProperty.AddOwner<PopupShadowLayer>();
public BoxShadows MaskShadows
{
get => GetValue(MaskShadowsProperty);
set => SetValue(MaskShadowsProperty, value);
}
private static readonly FieldInfo ManagedPopupPositionerPopupInfo;
private IManagedPopupPositionerPopup? _managedPopupPositionerPopup;
static PopupShadowLayer()
{
ManagedPopupPositionerPopupInfo = typeof(ManagedPopupPositioner).GetField("_popup",
ManagedPopupPositionerPopupInfo = typeof(ManagedPopupPositioner).GetField("_popup",
BindingFlags.Instance | BindingFlags.NonPublic)!;
}
public PopupShadowLayer()
{
WindowManagerAddShadowHint = false;
IsLightDismissEnabled = false;
_compositeDisposable = new CompositeDisposable();
}
private Popup? _target;
private ShadowRenderer? _shadowRenderer;
private CompositeDisposable _compositeDisposable;
private CompositeDisposable? _compositeDisposable;
private IManagedPopupPositionerPopup? _managedPopupPositionerPopup;
private TopLevel? _topLevel;
private bool _isOpened = false;
public PopupShadowLayer(TopLevel topLevel)
: base(topLevel, topLevel.PlatformImpl?.CreatePopup()!)
{
_topLevel = topLevel;
Background = new SolidColorBrush(Colors.Transparent);
if (this is WindowBase window) {
window.SetTransparentForMouseEvents(true);
}
if (PlatformImpl?.PopupPositioner is ManagedPopupPositioner managedPopupPositioner) {
_managedPopupPositionerPopup =
ManagedPopupPositionerPopupInfo.GetValue(managedPopupPositioner) as IManagedPopupPositionerPopup;
}
}
public void AttachToTarget(Popup popup)
{
if (_target is not null && _target != popup) {
// 释放资源
_compositeDisposable.Dispose();
}
_target = popup;
ConfigureShadowPopup();
}
public void DetachedFromTarget(Popup popup) { }
public void ShowShadows() { }
public void HideShadows() {}
private void ConfigureShadowPopup()
{
var offset = CalculatePopupOffset();
HorizontalOffset = offset.X;
VerticalOffset = offset.Y;
// // 绑定资源要管理起来
if (_target is not null) {
_target.Opened += HandleTargetOpened;
_target.Closed += HandleTargetClosed;
SetupRelayBindings();
}
if (_shadowRenderer is null) {
_shadowRenderer ??= new ShadowRenderer();
Child = _shadowRenderer;
}
}
private void SetupRelayBindings()
{
if (_target is not null) {
// _compositeDisposable.Add(BindUtils.RelayBind(_target, PlacementProperty, this));
// _compositeDisposable.Add(BindUtils.RelayBind(_target, PlacementGravityProperty, this));
// _compositeDisposable.Add(BindUtils.RelayBind(_target, PlacementAnchorProperty, this));
_compositeDisposable.Add(BindUtils.RelayBind(_target, PlacementTargetProperty, this));
SetChild(_shadowRenderer);
}
}
@ -89,22 +77,62 @@ internal class PopupShadowLayer : AbstractPopup, IShadowDecorator
SetupShadowRenderer();
Open();
}
private void HandleTargetClosed(object? sender, EventArgs args)
private void Open()
{
_compositeDisposable.Dispose();
Close();
if (_isOpened) {
return;
}
_compositeDisposable = new CompositeDisposable();
if (_topLevel is Avalonia.Controls.Window window && window.PlatformImpl != null) {
}
if (_target?.Host is PopupRoot popupRoot) {
popupRoot.PositionChanged += TargetPopupPositionChanged;
}
_compositeDisposable.Add(Disposable.Create(this, state =>
{
state.SetChild(null);
Hide();
((ISetLogicalParent)state).SetParent(null);
Dispose();
}));
((ISetLogicalParent)this).SetParent(_target);
SetupPositionAndSize();
_isOpened = true;
Show();
}
protected override void NotifyClosed()
private void TargetPopupPositionChanged(object? sender, PixelPointEventArgs e)
{
base.NotifyClosed();
_managedPopupPositionerPopup = null;
SetupPositionAndSize();
}
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler,
Action<T, TEventHandler> subscribe,
Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler),
state => state.unsubscribe(state.target, state.handler));
}
private void HandleTargetClosed(object? sender, EventArgs args)
{
if (_target is not null) {
_target.Opened -= HandleTargetOpened;
_target.Closed -= HandleTargetClosed;
}
_compositeDisposable?.Dispose();
_compositeDisposable = null;
}
private void SetupShadowRenderer()
{
SetupRelayBindings();
if (_target?.Child is not null && _shadowRenderer is not null) {
// 理论上现在已经有大小了
var content = _target?.Child!;
@ -123,22 +151,6 @@ internal class PopupShadowLayer : AbstractPopup, IShadowDecorator
}
}
protected override void NotifyPopupHostCreated(IPopupHost popupHost)
{
base.NotifyPopupHostCreated(popupHost);
if (popupHost is WindowBase window) {
window.Background = new SolidColorBrush(Colors.Transparent);
window.SetTransparentForMouseEvents(true);
if (_managedPopupPositionerPopup is null) {
if (popupHost is PopupRoot popupRoot) {
if (popupRoot.PlatformImpl?.PopupPositioner is ManagedPopupPositioner managedPopupPositioner) {
_managedPopupPositionerPopup = ManagedPopupPositionerPopupInfo.GetValue(managedPopupPositioner) as IManagedPopupPositionerPopup;
}
}
}
}
}
private Size CalculateShadowRendererSize(Control content)
{
var shadowThickness = MaskShadows.Thickness();
@ -147,18 +159,19 @@ internal class PopupShadowLayer : AbstractPopup, IShadowDecorator
return new Size(targetWidth, targetHeight);
}
private Point CalculatePopupOffset()
private void SetupPositionAndSize()
{
return default;
if (_target?.Host is PopupRoot popupRoot) {
var impl = popupRoot.PlatformImpl!;
var targetPosition = impl.Position;
double offsetX = targetPosition.X;
double offsetY = targetPosition.Y;
double scaling = _managedPopupPositionerPopup!.Scaling;
var shadowThickness = MaskShadows.Thickness();
offsetX -= shadowThickness.Left * scaling;
offsetY -= shadowThickness.Top * scaling;
_managedPopupPositionerPopup?.MoveAndResize(new Point(offsetX, offsetY),
new Size(_shadowRenderer!.Width, _shadowRenderer.Height));
}
}
protected internal override void NotifyPopupHostPositionUpdated(IPopupHost popupHost, Control placementTarget)
{
base.NotifyPopupHostPositionUpdated(popupHost, placementTarget);
popupHost.ConfigurePosition(placementTarget,
PlacementMode.Pointer,
offset: new Point(-40, 1),
anchor: PopupAnchor.Top,
gravity: PopupGravity.Top);
}
}
}

View File

@ -1,62 +0,0 @@
using System.Reactive.Disposables;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace AtomUI.Controls;
public class ShadowPopupHost : LiteWindow
{
public static readonly StyledProperty<bool> IsOpenProperty = AvaloniaProperty.Register<Popup, bool>(nameof(IsOpen));
public bool IsOpen
{
get => GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
private CompositeDisposable? _compositeDisposable;
private PopupRoot _popupRoot;
public ShadowPopupHost(PopupRoot popupRoot)
: base(popupRoot, popupRoot.PlatformImpl!)
{
_popupRoot = popupRoot;
}
private void Open()
{
if (_compositeDisposable != null) {
return;
}
Topmost = true;
// SubscribeToEventHandler<PopupRoot, EventHandler<PixelPointEventArgs>>(parentPopupRoot, ParentPopupPositionChanged,
// (x, handler) => x.PositionChanged += handler,
// (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup);
//
// if (parentPopupRoot.Parent is Popup popup)
// {
// SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(popup, ParentClosed,
// (x, handler) => x.Closed += handler,
// (x, handler) => x.Closed -= handler).DisposeWith(handlerCleanup);
// }
_compositeDisposable = new CompositeDisposable();
_compositeDisposable.Add(Disposable.Create(this, state =>
{
state.SetChild(null);
Hide();
((ISetLogicalParent)state).SetParent(null);
Dispose();
}));
Show();
}
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler,
Action<T, TEventHandler> subscribe,
Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
}

View File

@ -66,7 +66,7 @@ internal class ShadowRenderer : Control
var maskContent = new Border
{
BorderThickness = new Thickness(0),
Background = new SolidColorBrush(Colors.Red),
Background = new SolidColorBrush(Colors.Transparent),
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
@ -98,9 +98,4 @@ internal class ShadowRenderer : Control
Canvas.SetTop(_maskContent, shadowThickness.Top);
}
}
// public sealed override void Render(DrawingContext context)
// {
// context.FillRectangle(new SolidColorBrush(Colors.Bisque), new Rect(new Point(0, 0), DesiredSize));
// }
}