Răsfoiți Sursa

Merge pull request #10492 from AvaloniaUI/flyout-api

Update Flyout and Popup APIs to make gap between them smaller
Max Katz 2 ani în urmă
părinte
comite
f247e89516

+ 3 - 2
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs

@@ -36,8 +36,9 @@ namespace ControlCatalog.Pages
             customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
             customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
 
 
             var cancellableContextBorder = this.Get<Border>("CancellableContextBorder");
             var cancellableContextBorder = this.Get<Border>("CancellableContextBorder");
-            cancellableContextBorder.ContextFlyout!.Closing += ContextFlyoutPage_Closing;
-            cancellableContextBorder.ContextFlyout!.Opening += ContextFlyoutPage_Opening;
+            var flyout = (Flyout)cancellableContextBorder.ContextFlyout!;
+            flyout.Closing += ContextFlyoutPage_Closing;
+            flyout.Opening += ContextFlyoutPage_Opening;
         }
         }
 
 
         private ContextPageViewModel? _model;
         private ContextPageViewModel? _model;

+ 15 - 1
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@@ -16,8 +16,13 @@
             <MenuItem Header="Item 3" />
             <MenuItem Header="Item 3" />
         </MenuFlyout>
         </MenuFlyout>
         <Flyout Placement="Bottom" x:Key="BasicFlyout">
         <Flyout Placement="Bottom" x:Key="BasicFlyout">
+            <Flyout.FlyoutPresenterTheme>
+                <ControlTheme TargetType="FlyoutPresenter" BasedOn="{StaticResource {x:Type FlyoutPresenter}}">
+                    <Setter Property="CornerRadius" Value="20" />
+                </ControlTheme>
+            </Flyout.FlyoutPresenterTheme>
             <Panel Width="100" Height="100">
             <Panel Width="100" Height="100">
-                <TextBlock Text="Flyout Content!" />
+                <TextBlock Text="Flyout Content with a custom presenter theme!" TextWrapping="Wrap" />
             </Panel>
             </Panel>
         </Flyout>
         </Flyout>
     </UserControl.Resources>
     </UserControl.Resources>
@@ -136,6 +141,15 @@
                                 </Flyout>
                                 </Flyout>
                             </Button.Flyout>
                             </Button.Flyout>
                         </Button>
                         </Button>
+                        <Button Content="Placement=Center">
+                            <Button.Flyout>
+                                <Flyout Placement="Center">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
                         <Button Content="Placement=TopEdgeAlignedLeft">
                         <Button Content="Placement=TopEdgeAlignedLeft">
                             <Button.Flyout>
                             <Button.Flyout>
                                 <Flyout Placement="TopEdgeAlignedLeft">
                                 <Flyout Placement="TopEdgeAlignedLeft">

+ 1 - 0
src/Avalonia.Base/Input/MouseDevice.cs

@@ -4,6 +4,7 @@ using Avalonia.Reactive;
 using Avalonia.Input.Raw;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Utilities;
 using Avalonia.Utilities;
+#pragma warning disable CS0618
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
 {
 {

+ 2 - 1
src/Avalonia.Base/Input/PenDevice.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Reflection;
 using System.Reflection;
 using Avalonia.Input.Raw;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
 using Avalonia.Platform;
+#pragma warning disable CS0618
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
 {
 {
@@ -129,7 +130,7 @@ namespace Avalonia.Input
                 var e = new PointerReleasedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers,
                 var e = new PointerReleasedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers,
                     _lastMouseDownButton);
                     _lastMouseDownButton);
 
 
-                source?.RaiseEvent(e);
+                source.RaiseEvent(e);
                 pointer.Capture(null);
                 pointer.Capture(null);
                 _lastMouseDownButton = default;
                 _lastMouseDownButton = default;
                 return e.Handled;
                 return e.Handled;

+ 2 - 0
src/Avalonia.Base/Input/PointerEventArgs.cs

@@ -42,7 +42,9 @@ namespace Avalonia.Input
             PointerPointProperties properties,
             PointerPointProperties properties,
             KeyModifiers modifiers,
             KeyModifiers modifiers,
             Lazy<IReadOnlyList<RawPointerPoint>?>? previousPoints)
             Lazy<IReadOnlyList<RawPointerPoint>?>? previousPoints)
+#pragma warning disable CS0618
             : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers)
             : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers)
+#pragma warning restore CS0618
         {
         {
             _previousPoints = previousPoints;
             _previousPoints = previousPoints;
         }
         }

+ 20 - 13
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@@ -6,7 +6,8 @@ namespace Avalonia.Input
     internal class PointerOverPreProcessor : IObserver<RawInputEventArgs>
     internal class PointerOverPreProcessor : IObserver<RawInputEventArgs>
     {
     {
         private IPointerDevice? _lastActivePointerDevice;
         private IPointerDevice? _lastActivePointerDevice;
-        private (IPointer pointer, PixelPoint position)? _lastPointer;
+        private (IPointer pointer, PixelPoint position)? _currentPointer;
+        private PixelPoint? _lastKnownPosition;
 
 
         private readonly IInputRoot _inputRoot;
         private readonly IInputRoot _inputRoot;
 
 
@@ -15,7 +16,7 @@ namespace Avalonia.Input
             _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
             _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
         }
         }
 
 
-        public PixelPoint? LastPosition => _lastPointer?.position;
+        public PixelPoint? LastPosition => _lastKnownPosition;
         
         
         public void OnCompleted()
         public void OnCompleted()
         {
         {
@@ -41,14 +42,14 @@ namespace Avalonia.Input
                 }
                 }
 
 
                 if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
                 if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
-                    && _lastPointer is (var lastPointer, var lastPosition))
+                    && _currentPointer is var (lastPointer, lastPosition))
                 {
                 {
-                    _lastPointer = null;
+                    _currentPointer = null;
                     ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition),
                     ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition),
                         new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
                         new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
                         args.InputModifiers.ToKeyModifiers());
                         args.InputModifiers.ToKeyModifiers());
                 }
                 }
-                else if (pointerDevice.TryGetPointer(args) is IPointer pointer
+                else if (pointerDevice.TryGetPointer(args) is { } pointer
                     && pointer.Type != PointerType.Touch)
                     && pointer.Type != PointerType.Touch)
                 {
                 {
                     var element = pointer.Captured ?? args.InputHitTestResult;
                     var element = pointer.Captured ?? args.InputHitTestResult;
@@ -62,7 +63,7 @@ namespace Avalonia.Input
 
 
         public void SceneInvalidated(Rect dirtyRect)
         public void SceneInvalidated(Rect dirtyRect)
         {
         {
-            if (_lastPointer is (var pointer, var position))
+            if (_currentPointer is (var pointer, var position))
             {
             {
                 var clientPoint = PointToClient(_inputRoot, position);
                 var clientPoint = PointToClient(_inputRoot, position);
 
 
@@ -80,12 +81,12 @@ namespace Avalonia.Input
 
 
         private void ClearPointerOver()
         private void ClearPointerOver()
         {
         {
-            if (_lastPointer is (var pointer, var position))
+            if (_currentPointer is (var pointer, var position))
             {
             {
                 var clientPoint = PointToClient(_inputRoot, position);
                 var clientPoint = PointToClient(_inputRoot, position);
                 ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
                 ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
             }
             }
-            _lastPointer = null;
+            _currentPointer = null;
             _lastActivePointerDevice = null;
             _lastActivePointerDevice = null;
         }
         }
 
 
@@ -100,9 +101,11 @@ namespace Avalonia.Input
 
 
             // Do not pass rootVisual, when we have unknown position,
             // Do not pass rootVisual, when we have unknown position,
             // so GetPosition won't return invalid values.
             // so GetPosition won't return invalid values.
+#pragma warning disable CS0618
             var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer,
             var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer,
                 position.HasValue ? root as Visual : null, position.HasValue ? position.Value : default,
                 position.HasValue ? root as Visual : null, position.HasValue ? position.Value : default,
                 timestamp, properties, inputModifiers);
                 timestamp, properties, inputModifiers);
+#pragma warning restore CS0618
 
 
             if (element is Visual v && !v.IsAttachedToVisualTree)
             if (element is Visual v && !v.IsAttachedToVisualTree)
             {
             {
@@ -122,18 +125,18 @@ namespace Avalonia.Input
 
 
             root.PointerOverElement = null;
             root.PointerOverElement = null;
             _lastActivePointerDevice = null;
             _lastActivePointerDevice = null;
-            _lastPointer = null;
+            _currentPointer = null;
         }
         }
 
 
         private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot)
         private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot)
         {
         {
             if (element is Visual v)
             if (element is Visual v)
             {
             {
-                foreach (IInputElement el in v.VisualChildren)
+                foreach (var el in v.VisualChildren)
                 {
                 {
-                    if (el.IsPointerOver)
+                    if (el is IInputElement { IsPointerOver: true } child)
                     {
                     {
-                        ClearChildrenPointerOver(e, el, true);
+                        ClearChildrenPointerOver(e, child, true);
                         break;
                         break;
                     }
                     }
                 }
                 }
@@ -151,6 +154,8 @@ namespace Avalonia.Input
             ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
             ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
         {
         {
             var pointerOverElement = root.PointerOverElement;
             var pointerOverElement = root.PointerOverElement;
+            var screenPosition = ((Visual)root).PointToScreen(position);
+            _lastKnownPosition = screenPosition;
 
 
             if (element != pointerOverElement)
             if (element != pointerOverElement)
             {
             {
@@ -164,7 +169,7 @@ namespace Avalonia.Input
                 }
                 }
             }
             }
 
 
-            _lastPointer = (pointer, ((Visual)root).PointToScreen(position));
+            _currentPointer = (pointer, screenPosition);
         }
         }
 
 
         private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element,
         private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element,
@@ -186,8 +191,10 @@ namespace Avalonia.Input
 
 
             el = root.PointerOverElement;
             el = root.PointerOverElement;
 
 
+#pragma warning disable CS0618
             var e = new PointerEventArgs(InputElement.PointerExitedEvent, el, pointer, (Visual)root, position,
             var e = new PointerEventArgs(InputElement.PointerExitedEvent, el, pointer, (Visual)root, position,
                 timestamp, properties, inputModifiers);
                 timestamp, properties, inputModifiers);
+#pragma warning restore CS0618
             if (el is Visual v && branch != null && !v.IsAttachedToVisualTree)
             if (el is Visual v && branch != null && !v.IsAttachedToVisualTree)
             {
             {
                 ClearChildrenPointerOver(e, branch, false);
                 ClearChildrenPointerOver(e, branch, false);

+ 1 - 0
src/Avalonia.Base/Input/TouchDevice.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Reflection;
 using System.Reflection;
 using Avalonia.Input.Raw;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
 using Avalonia.Platform;
+#pragma warning disable CS0618
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
 {
 {

+ 32 - 6
src/Avalonia.Controls/Flyouts/Flyout.cs

@@ -1,9 +1,11 @@
-using Avalonia.Controls.Primitives;
+using System.ComponentModel;
+using Avalonia.Controls.Primitives;
 using Avalonia.Metadata;
 using Avalonia.Metadata;
+using Avalonia.Styling;
 
 
 namespace Avalonia.Controls
 namespace Avalonia.Controls
 {
 {
-    public class Flyout : FlyoutBase
+    public class Flyout : PopupFlyoutBase
     {
     {
         /// <summary>
         /// <summary>
         /// Defines the <see cref="Content"/> property
         /// Defines the <see cref="Content"/> property
@@ -18,6 +20,21 @@ namespace Avalonia.Controls
 
 
         private Classes? _classes;
         private Classes? _classes;
 
 
+        /// <summary>
+        /// Defines the <see cref="FlyoutPresenterTheme"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ControlTheme?> FlyoutPresenterThemeProperty =
+            AvaloniaProperty.Register<Flyout, ControlTheme?>(nameof(FlyoutPresenterTheme));
+        
+        /// <summary>
+        /// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for the flyout presenter.
+        /// </summary>
+        public ControlTheme? FlyoutPresenterTheme
+        {
+            get => GetValue(FlyoutPresenterThemeProperty); 
+            set => SetValue(FlyoutPresenterThemeProperty, value);
+        }
+        
         /// <summary>
         /// <summary>
         /// Gets or sets the content to display in this flyout
         /// Gets or sets the content to display in this flyout
         /// </summary>
         /// </summary>
@@ -36,13 +53,22 @@ namespace Avalonia.Controls
             };
             };
         }
         }
 
 
-        protected override void OnOpened()
+        protected override void OnOpening(CancelEventArgs args)
         {
         {
-            if (_classes != null)
+            if (Popup.Child is { } presenter)
             {
             {
-                SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
+                if (_classes != null)
+                {
+                    SetPresenterClasses(presenter, FlyoutPresenterClasses);
+                }
+
+                if (FlyoutPresenterTheme is { } theme)
+                {
+                    presenter.SetValue(Control.ThemeProperty, theme);
+                }
             }
             }
-            base.OnOpened();
+
+            base.OnOpening(args);
         }
         }
     }
     }
 }
 }

+ 11 - 537
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@@ -1,17 +1,8 @@
 using System;
 using System;
-using System.ComponentModel;
-using Avalonia.Controls.Diagnostics;
-using System.Linq;
-using Avalonia.Input;
-using Avalonia.Input.Platform;
-using Avalonia.Input.Raw;
-using Avalonia.Layout;
-using Avalonia.Logging;
-using Avalonia.Reactive;
 
 
 namespace Avalonia.Controls.Primitives
 namespace Avalonia.Controls.Primitives
 {
 {
-    public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
+    public abstract class FlyoutBase : AvaloniaObject
     {
     {
         /// <summary>
         /// <summary>
         /// Defines the <see cref="IsOpen"/> property
         /// Defines the <see cref="IsOpen"/> property
@@ -26,75 +17,25 @@ namespace Avalonia.Controls.Primitives
         public static readonly DirectProperty<FlyoutBase, Control?> TargetProperty =
         public static readonly DirectProperty<FlyoutBase, Control?> TargetProperty =
             AvaloniaProperty.RegisterDirect<FlyoutBase, Control?>(nameof(Target), x => x.Target);
             AvaloniaProperty.RegisterDirect<FlyoutBase, Control?>(nameof(Target), x => x.Target);
 
 
-        /// <summary>
-        /// Defines the <see cref="Placement"/> property
-        /// </summary>
-        public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
-            AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
-
-        /// <summary>
-        /// Defines the <see cref="ShowMode"/> property
-        /// </summary>
-        public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
-            AvaloniaProperty.Register<FlyoutBase, FlyoutShowMode>(nameof(ShowMode));
-
-        /// <summary>
-        /// Defines the <see cref="OverlayInputPassThroughElement"/> property
-        /// </summary>
-        public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
-            Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>();
-
         /// <summary>
         /// <summary>
         /// Defines the AttachedFlyout property
         /// Defines the AttachedFlyout property
         /// </summary>
         /// </summary>
         public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
         public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
             AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
             AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
 
 
-        private readonly Lazy<Popup> _popupLazy;
         private bool _isOpen;
         private bool _isOpen;
         private Control? _target;
         private Control? _target;
-        private Rect? _enlargedPopupRect;
-        private PixelRect? _enlargePopupRectScreenPixelRect;
-        private IDisposable? _transientDisposable;
-        private Action<IPopupHost?>? _popupHostChangedHandler;
-
-        static FlyoutBase()
-        {
-            Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
-        }
-
-        public FlyoutBase()
-        {
-            _popupLazy = new Lazy<Popup>(() => CreatePopup());
-        }
-
-        protected Popup Popup => _popupLazy.Value;
 
 
+        public event EventHandler? Opened;
+        public event EventHandler? Closed;
+        
         /// <summary>
         /// <summary>
         /// Gets whether this Flyout is currently Open
         /// Gets whether this Flyout is currently Open
         /// </summary>
         /// </summary>
         public bool IsOpen
         public bool IsOpen
         {
         {
             get => _isOpen;
             get => _isOpen;
-            private set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
-        }
-
-        /// <summary>
-        /// Gets or sets the desired placement
-        /// </summary>
-        public FlyoutPlacementMode Placement
-        {
-            get => GetValue(PlacementProperty);
-            set => SetValue(PlacementProperty, value);
-        }
-
-        /// <summary>
-        /// Gets or sets the desired ShowMode
-        /// </summary>
-        public FlyoutShowMode ShowMode
-        {
-            get => GetValue(ShowModeProperty);
-            set => SetValue(ShowModeProperty, value);
+            protected set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -103,32 +44,9 @@ namespace Avalonia.Controls.Primitives
         public Control? Target
         public Control? Target
         {
         {
             get => _target;
             get => _target;
-            private set => SetAndRaise(TargetProperty, ref _target, value);
+            protected set => SetAndRaise(TargetProperty, ref _target, value);
         }
         }
-
-        /// <summary>
-        /// Gets or sets an element that should receive pointer input events even when underneath
-        /// the flyout's overlay.
-        /// </summary>
-        public IInputElement? OverlayInputPassThroughElement
-        {
-            get => GetValue(OverlayInputPassThroughElementProperty);
-            set => SetValue(OverlayInputPassThroughElementProperty, value);
-        }
-
-        IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
-
-        event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
-        {
-            add => _popupHostChangedHandler += value;
-            remove => _popupHostChangedHandler -= value;
-        }
-
-        public event EventHandler? Closed;
-        public event EventHandler<CancelEventArgs>? Closing;
-        public event EventHandler? Opened;
-        public event EventHandler? Opening;
-
+        
         public static FlyoutBase? GetAttachedFlyout(Control element)
         public static FlyoutBase? GetAttachedFlyout(Control element)
         {
         {
             return element.GetValue(AttachedFlyoutProperty);
             return element.GetValue(AttachedFlyoutProperty);
@@ -145,462 +63,18 @@ namespace Avalonia.Controls.Primitives
             flyout?.ShowAt(flyoutOwner);
             flyout?.ShowAt(flyoutOwner);
         }
         }
 
 
-        /// <summary>
-        /// Shows the Flyout at the given Control
-        /// </summary>
-        /// <param name="placementTarget">The control to show the Flyout at</param>
-        public void ShowAt(Control placementTarget)
-        {
-            ShowAtCore(placementTarget);
-        }
-
-        /// <summary>
-        /// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
-        /// </summary>
-        /// <param name="placementTarget">The target control</param>
-        /// <param name="showAtPointer">True to show at pointer</param>
-        public void ShowAt(Control placementTarget, bool showAtPointer)
-        {
-            ShowAtCore(placementTarget, showAtPointer);
-        }
-
-        /// <summary>
-        /// Hides the Flyout
-        /// </summary>
-        public void Hide()
-        {
-            HideCore();
-        }
-
-        /// <returns>True, if action was handled</returns>
-        protected virtual bool HideCore(bool canCancel = true)
-        {
-            if (!IsOpen)
-            {
-                return false;
-            }
-
-            if (canCancel)
-            {
-                if (CancelClosing())
-                {
-                    return false;
-                }
-            }
-
-            IsOpen = false;
-            Popup.IsOpen = false;
-
-            ((ISetLogicalParent)Popup).SetParent(null);
-
-            // Ensure this isn't active
-            _transientDisposable?.Dispose();
-            _transientDisposable = null;
-            _enlargedPopupRect = null;
-            _enlargePopupRectScreenPixelRect = null;
-
-            if (Target != null)
-            {
-                Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
-                Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
-            }
-
-            OnClosed();
-
-            return true;
-        }
-
-        /// <returns>True, if action was handled</returns>
-        protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
-        {
-            if (placementTarget == null)
-            {
-                throw new ArgumentNullException(nameof(placementTarget));
-            }
-
-            if (IsOpen)
-            {
-                if (placementTarget == Target)
-                {
-                    return false;
-                }
-                else // Close before opening a new one
-                {
-                    _ = HideCore(false);
-                }
-            }
-
-            if (Popup.Parent != null && Popup.Parent != placementTarget)
-            {
-                ((ISetLogicalParent)Popup).SetParent(null);
-            }
-
-            if (Popup.Parent == null || Popup.PlacementTarget != placementTarget)
-            {
-                Popup.PlacementTarget = Target = placementTarget;
-                ((ISetLogicalParent)Popup).SetParent(placementTarget);
-                Popup.TemplatedParent = placementTarget.TemplatedParent;
-            }
-
-            if (Popup.Child == null)
-            {
-                Popup.Child = CreatePresenter();
-            }
-
-            Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
-
-            if (CancelOpening())
-            {
-                return false;
-            }
-
-            PositionPopup(showAtPointer);
-            IsOpen = Popup.IsOpen = true;
-            OnOpened();
-
-            placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
-            placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
-
-            if (ShowMode == FlyoutShowMode.Standard)
-            {
-                // Try and focus content inside Flyout
-                if (Popup.Child.Focusable)
-                {
-                    FocusManager.Instance?.Focus(Popup.Child);
-                }
-                else
-                {
-                    var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
-                    if (nextFocus != null)
-                    {
-                        FocusManager.Instance?.Focus(nextFocus);
-                    }
-                }
-            }
-            else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
-            {
-                _transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
-            }
-
-            return true;
-        }
-
-        private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
-        {
-            _ = HideCore(false);
-        }
-
-        private void HandleTransientDismiss(RawInputEventArgs args)
-        {
-            if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move)
-            {
-                // In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
-                // shown as long as the pointer is within a certain px distance from the
-                // flyout itself. I'm not sure what WinUI uses, but I'm defaulting to 
-                // 100px, which seems about right
-                // enlargedPopupRect is the Flyout bounds enlarged 100px
-                // For windowed popups, enlargedPopupRect is in screen coordinates,
-                // for overlay popups, its in OverlayLayer coordinates
-
-                if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null)
-                {
-                    // Only do this once when the Flyout opens & cache the result
-                    if (Popup?.Host is PopupRoot root)
-                    {
-                        // Get the popup root bounds and convert to screen coordinates
-                        
-                        var tmp = root.Bounds.Inflate(100);
-                        _enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight));
-                    }
-                    else if (Popup?.Host is OverlayPopupHost host)
-                    {
-                        // Overlay popups are in OverlayLayer coordinates, just use that
-                        _enlargedPopupRect = host.Bounds.Inflate(100);
-                    }
-
-                    return;
-                }
-
-                if (Popup?.Host is PopupRoot && pArgs.Root is Visual eventRoot)
-                {
-                    // As long as the pointer stays within the enlargedPopupRect
-                    // the flyout stays open. If it leaves, close it
-                    // Despite working in screen coordinates, leaving the TopLevel
-                    // window will not close this (as pointer events stop), which 
-                    // does match UWP
-                    var pt = eventRoot.PointToScreen(pArgs.Position);
-                    if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false)
-                    {
-                        HideCore(false);
-                    }
-                }
-                else if (Popup?.Host is OverlayPopupHost)
-                {
-                    // Same as above here, but just different coordinate space
-                    // so we don't need to translate
-                    if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false)
-                    {
-                        HideCore(false);
-                    }
-                }
-            }
-        }
-
-        protected virtual void OnOpening(CancelEventArgs args)
-        {
-            Opening?.Invoke(this, args);
-        }
-
+        public abstract void ShowAt(Control placementTarget);
+        
+        public abstract void Hide();
+        
         protected virtual void OnOpened()
         protected virtual void OnOpened()
         {
         {
             Opened?.Invoke(this, EventArgs.Empty);
             Opened?.Invoke(this, EventArgs.Empty);
         }
         }
 
 
-        protected virtual void OnClosing(CancelEventArgs args)
-        {
-            Closing?.Invoke(this, args);
-        }
-
         protected virtual void OnClosed()
         protected virtual void OnClosed()
         {
         {
             Closed?.Invoke(this, EventArgs.Empty);
             Closed?.Invoke(this, EventArgs.Empty);
         }
         }
-
-        /// <summary>
-        /// Used to create the content the Flyout displays
-        /// </summary>
-        /// <returns></returns>
-        protected abstract Control CreatePresenter();
-
-        private Popup CreatePopup()
-        {
-            var popup = new Popup
-            {
-                WindowManagerAddShadowHint = false,
-                IsLightDismissEnabled = true,
-                //Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
-                OverlayDismissEventPassThrough = false
-            };
-
-            popup.Opened += OnPopupOpened;
-            popup.Closed += OnPopupClosed;
-            popup.Closing += OnPopupClosing;
-            popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
-            return popup;
-        }
-
-        private void OnPopupOpened(object? sender, EventArgs e)
-        {
-            IsOpen = true;
-
-            _popupHostChangedHandler?.Invoke(Popup.Host);
-        }
-
-        private void OnPopupClosing(object? sender, CancelEventArgs e)
-        {
-            if (IsOpen)
-            {
-                e.Cancel = CancelClosing();
-            }
-        }
-
-        private void OnPopupClosed(object? sender, EventArgs e)
-        {
-            HideCore(false);
-
-            _popupHostChangedHandler?.Invoke(null);
-        }
-
-        // This method is handling both popup logical tree and target logical tree.
-        private void OnPlacementTargetOrPopupKeyUp(object? sender, KeyEventArgs e)
-        {
-            if (!e.Handled
-                && IsOpen
-                && Target?.ContextFlyout == this)
-            {
-                var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
-
-                if (keymap?.OpenContextMenu.Any(k => k.Matches(e)) == true)
-                {
-                    e.Handled = HideCore();
-                }
-            }
-        }
-
-        private void PositionPopup(bool showAtPointer)
-        {
-            Size sz;
-            // Popup.Child can't be null here, it was set in ShowAtCore.
-            if (Popup.Child!.DesiredSize.IsDefault)
-            {
-                // Popup may not have been shown yet. Measure content
-                sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
-            }
-            else
-            {
-                sz = Popup.Child.DesiredSize;
-            }
-
-            if (showAtPointer)
-            {
-                Popup.PlacementMode = PlacementMode.Pointer;
-            }
-            else
-            {
-                Popup.PlacementMode = PlacementMode.AnchorAndGravity;
-                Popup.PlacementConstraintAdjustment =
-                    PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
-                    PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;
-            }
-
-            var trgtBnds = Target?.Bounds ?? default;
-
-            switch (Placement)
-            {
-                case FlyoutPlacementMode.Top: //Above & centered
-                    Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width - 1, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top;
-                    break;
-
-                case FlyoutPlacementMode.TopEdgeAlignedLeft:
-                    Popup.PlacementRect = new Rect(0, 0, 0, 0);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
-                    break;
-
-                case FlyoutPlacementMode.TopEdgeAlignedRight:
-                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
-                    break;
-
-                case FlyoutPlacementMode.RightEdgeAlignedTop:
-                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
-                    break;
-
-                case FlyoutPlacementMode.Right: //Right & centered
-                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, trgtBnds.Height);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Right;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
-                    break;
-
-                case FlyoutPlacementMode.RightEdgeAlignedBottom:
-                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
-                    break;
-
-                case FlyoutPlacementMode.Bottom: //Below & centered
-                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, trgtBnds.Width, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Bottom;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
-                    break;
-
-                case FlyoutPlacementMode.BottomEdgeAlignedLeft:
-                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
-                    break;
-
-                case FlyoutPlacementMode.BottomEdgeAlignedRight:
-                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
-                    break;
-
-                case FlyoutPlacementMode.LeftEdgeAlignedTop:
-                    Popup.PlacementRect = new Rect(0, 0, 1, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
-                    break;
-
-                case FlyoutPlacementMode.Left: //Left & centered
-                    Popup.PlacementRect = new Rect(0, 0, 1, trgtBnds.Height);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Left;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
-                    break;
-
-                case FlyoutPlacementMode.LeftEdgeAlignedBottom:
-                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
-                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.BottomLeft;
-                    break;
-
-                //includes Auto (not sure what determines that)...
-                default:
-                    //This is just FlyoutPlacementMode.Top behavior (above & centered)
-                    Popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1);
-                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
-                    break;
-            }
-        }
-
-        private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
-        {
-            if (args.Sender is Control c)
-            {
-                if (args.OldValue is FlyoutBase)
-                {
-                    c.ContextRequested -= OnControlContextRequested;
-                }
-                if (args.NewValue is FlyoutBase)
-                {
-                    c.ContextRequested += OnControlContextRequested;
-                }
-            }
-        }
-
-        private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e)
-        {
-            if (!e.Handled
-                && sender is Control control
-                && control.ContextFlyout is FlyoutBase flyout)
-            {
-                if (control.ContextMenu != null)
-                {
-                    Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
-                    return;
-                }
-
-                // We do not support absolute popup positioning yet, so we ignore "point" at this moment.
-                var triggeredByPointerInput = e.TryGetPosition(null, out _);
-                e.Handled = flyout.ShowAtCore(control, triggeredByPointerInput);
-            }
-        }
-
-        private bool CancelClosing()
-        {
-            var eventArgs = new CancelEventArgs();
-            OnClosing(eventArgs);
-            return eventArgs.Cancel;
-        }
-
-        private bool CancelOpening()
-        {
-            var eventArgs = new CancelEventArgs();
-            OnOpening(eventArgs);
-            return eventArgs.Cancel;
-        }
-
-        internal static void SetPresenterClasses(Control? presenter, Classes classes)
-        {
-            if(presenter is null)
-            {
-                return;
-            }
-            //Remove any classes no longer in use, ignoring pseudo classes
-            for (int i = presenter.Classes.Count - 1; i >= 0; i--)
-            {
-                if (!classes.Contains(presenter.Classes[i]) &&
-                    !presenter.Classes[i].Contains(':'))
-                {
-                    presenter.Classes.RemoveAt(i);
-                }
-            }
-
-            //Add new classes
-            presenter.Classes.AddRange(classes);
-        }
     }
     }
 }
 }

+ 0 - 77
src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs

@@ -1,77 +0,0 @@
-namespace Avalonia.Controls
-{
-    public enum FlyoutPlacementMode
-    {
-        /// <summary>
-        /// Preferred location is above the target element
-        /// </summary>
-        Top = 0,
-
-        /// <summary>
-        /// Preferred location is below the target element
-        /// </summary>
-        Bottom = 1,
-
-        /// <summary>
-        /// Preferred location is to the left of the target element
-        /// </summary>
-        Left = 2,
-
-        /// <summary>
-        /// Preferred location is to the right of the target element
-        /// </summary>
-        Right = 3,
-
-        //TODO
-        // <summary>
-        // Preferred location is centered on the screen
-        // </summary>
-        //Full = 4,
-
-        /// <summary>
-        /// Preferred location is above the target element, with the left edge of the flyout
-        /// aligned with the left edge of the target element
-        /// </summary>
-        TopEdgeAlignedLeft = 5,
-
-        /// <summary>
-        /// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element.
-        /// </summary>
-        TopEdgeAlignedRight = 6,
-
-        /// <summary>
-        /// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element.
-        /// </summary>
-        BottomEdgeAlignedLeft = 7,
-
-        /// <summary>
-        /// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element.
-        /// </summary>
-        BottomEdgeAlignedRight = 8,
-
-        /// <summary>
-        /// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element.
-        /// </summary>
-        LeftEdgeAlignedTop = 9,
-
-        /// <summary>
-        /// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
-        /// </summary>
-        LeftEdgeAlignedBottom = 10,
-
-        /// <summary>
-        /// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element.
-        /// </summary>
-        RightEdgeAlignedTop = 11,
-
-        /// <summary>
-        /// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
-        /// </summary>
-        RightEdgeAlignedBottom = 12,
-
-        /// <summary>
-        /// Preferred location is determined automatically.
-        /// </summary>
-        Auto = 13
-    }
-}

+ 32 - 7
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@@ -1,4 +1,5 @@
 using System.Collections;
 using System.Collections;
+using System.ComponentModel;
 using Avalonia.Collections;
 using Avalonia.Collections;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Templates;
@@ -7,7 +8,7 @@ using Avalonia.Styling;
 
 
 namespace Avalonia.Controls
 namespace Avalonia.Controls
 {
 {
-    public class MenuFlyout : FlyoutBase
+    public class MenuFlyout : PopupFlyoutBase
     {
     {
         public MenuFlyout()
         public MenuFlyout()
         {
         {
@@ -34,6 +35,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<ControlTheme?> ItemContainerThemeProperty =
         public static readonly StyledProperty<ControlTheme?> ItemContainerThemeProperty =
             ItemsControl.ItemContainerThemeProperty.AddOwner<MenuFlyout>();
             ItemsControl.ItemContainerThemeProperty.AddOwner<MenuFlyout>();
 
 
+        /// <summary>
+        /// Defines the <see cref="FlyoutPresenterTheme"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ControlTheme?> FlyoutPresenterThemeProperty =
+            Flyout.FlyoutPresenterThemeProperty.AddOwner<MenuFlyout>();
+        
         public Classes FlyoutPresenterClasses => _classes ??= new Classes();
         public Classes FlyoutPresenterClasses => _classes ??= new Classes();
 
 
         /// <summary>
         /// <summary>
@@ -60,10 +67,19 @@ namespace Avalonia.Controls
         /// </summary>
         /// </summary>
         public ControlTheme? ItemContainerTheme
         public ControlTheme? ItemContainerTheme
         {
         {
-            get { return GetValue(ItemContainerThemeProperty); }
-            set { SetValue(ItemContainerThemeProperty, value); }
+            get => GetValue(ItemContainerThemeProperty);
+            set => SetValue(ItemContainerThemeProperty, value);
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for the flyout presenter.
+        /// </summary>
+        public ControlTheme? FlyoutPresenterTheme
+        {
+            get => GetValue(FlyoutPresenterThemeProperty); 
+            set => SetValue(FlyoutPresenterThemeProperty, value);
+        }
+        
         private Classes? _classes;
         private Classes? _classes;
         private IEnumerable? _items;
         private IEnumerable? _items;
         private IDataTemplate? _itemTemplate;
         private IDataTemplate? _itemTemplate;
@@ -78,13 +94,22 @@ namespace Avalonia.Controls
             };
             };
         }
         }
 
 
-        protected override void OnOpened()
+        protected override void OnOpening(CancelEventArgs args)
         {
         {
-            if (_classes != null)
+            if (Popup.Child is { } presenter)
             {
             {
-                SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
+                if (_classes != null)
+                {
+                    SetPresenterClasses(presenter, FlyoutPresenterClasses);
+                }
+
+                if (FlyoutPresenterTheme is { } theme)
+                {
+                    presenter.SetValue(Control.ThemeProperty, theme);
+                }
             }
             }
-            base.OnOpened();
+
+            base.OnOpening(args);
         }
         }
     }
     }
 }
 }

+ 512 - 0
src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs

@@ -0,0 +1,512 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using Avalonia.Controls.Diagnostics;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Input.Raw;
+using Avalonia.Layout;
+using Avalonia.Logging;
+using Avalonia.Reactive;
+
+namespace Avalonia.Controls.Primitives
+{
+    public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
+    {
+        /// <inheritdoc cref="Popup.PlacementModeProperty"/>
+        public static readonly StyledProperty<PlacementMode> PlacementProperty =
+            Popup.PlacementModeProperty.AddOwner<PopupFlyoutBase>();
+
+        /// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
+        public static readonly StyledProperty<double> HorizontalOffsetProperty =
+            Popup.HorizontalOffsetProperty.AddOwner<PopupFlyoutBase>();
+        
+        /// <inheritdoc cref="Popup.VerticalOffsetProperty"/>
+        public static readonly StyledProperty<double> VerticalOffsetProperty =
+            Popup.VerticalOffsetProperty.AddOwner<PopupFlyoutBase>();
+            
+        /// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
+        public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
+            Popup.PlacementAnchorProperty.AddOwner<PopupFlyoutBase>();
+        
+        /// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
+        public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
+            Popup.PlacementGravityProperty.AddOwner<PopupFlyoutBase>();
+
+        /// <summary>
+        /// Defines the <see cref="ShowMode"/> property
+        /// </summary>
+        public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
+            AvaloniaProperty.Register<PopupFlyoutBase, FlyoutShowMode>(nameof(ShowMode));
+
+        /// <summary>
+        /// Defines the <see cref="OverlayInputPassThroughElement"/> property
+        /// </summary>
+        public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
+            Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>();
+        
+        private readonly Lazy<Popup> _popupLazy;
+        private Rect? _enlargedPopupRect;
+        private PixelRect? _enlargePopupRectScreenPixelRect;
+        private IDisposable? _transientDisposable;
+        private Action<IPopupHost?>? _popupHostChangedHandler;
+
+        static PopupFlyoutBase()
+        {
+            Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
+        }
+
+        public PopupFlyoutBase()
+        {
+            _popupLazy = new Lazy<Popup>(() => CreatePopup());
+        }
+
+        protected Popup Popup => _popupLazy.Value;
+
+        /// <summary>
+        /// Gets or sets the desired placement.
+        /// </summary>
+        public PlacementMode Placement
+        {
+            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);
+            set => SetValue(PlacementAnchorProperty, value);
+        }
+
+        /// <inheritdoc cref="Popup.HorizontalOffset"/>
+        public double HorizontalOffset
+        {
+            get => GetValue(HorizontalOffsetProperty);
+            set => SetValue(HorizontalOffsetProperty, value);
+        }
+
+        /// <inheritdoc cref="Popup.VerticalOffset"/>
+        public double VerticalOffset
+        {
+            get => GetValue(VerticalOffsetProperty);
+            set => SetValue(VerticalOffsetProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the desired ShowMode
+        /// </summary>
+        public FlyoutShowMode ShowMode
+        {
+            get => GetValue(ShowModeProperty);
+            set => SetValue(ShowModeProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets an element that should receive pointer input events even when underneath
+        /// the flyout's overlay.
+        /// </summary>
+        public IInputElement? OverlayInputPassThroughElement
+        {
+            get => GetValue(OverlayInputPassThroughElementProperty);
+            set => SetValue(OverlayInputPassThroughElementProperty, value);
+        }
+
+        IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
+
+        event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
+        {
+            add => _popupHostChangedHandler += value;
+            remove => _popupHostChangedHandler -= value;
+        }
+        
+        public event EventHandler<CancelEventArgs>? Closing;
+        public event EventHandler? Opening;
+
+        /// <summary>
+        /// Shows the Flyout at the given Control
+        /// </summary>
+        /// <param name="placementTarget">The control to show the Flyout at</param>
+        public sealed override void ShowAt(Control placementTarget)
+        {
+            ShowAtCore(placementTarget);
+        }
+
+        /// <summary>
+        /// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
+        /// </summary>
+        /// <param name="placementTarget">The target control</param>
+        /// <param name="showAtPointer">True to show at pointer</param>
+        public void ShowAt(Control placementTarget, bool showAtPointer)
+        {
+            ShowAtCore(placementTarget, showAtPointer);
+        }
+
+        /// <summary>
+        /// Hides the Flyout
+        /// </summary>
+        public sealed override void Hide()
+        {
+            HideCore();
+        }
+
+        /// <returns>True, if action was handled</returns>
+        protected virtual bool HideCore(bool canCancel = true)
+        {
+            if (!IsOpen)
+            {
+                return false;
+            }
+
+            if (canCancel)
+            {
+                if (CancelClosing())
+                {
+                    return false;
+                }
+            }
+
+            IsOpen = false;
+            Popup.IsOpen = false;
+
+            ((ISetLogicalParent)Popup).SetParent(null);
+
+            // Ensure this isn't active
+            _transientDisposable?.Dispose();
+            _transientDisposable = null;
+            _enlargedPopupRect = null;
+            _enlargePopupRectScreenPixelRect = null;
+
+            if (Target != null)
+            {
+                Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
+                Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
+            }
+
+            OnClosed();
+
+            return true;
+        }
+
+        /// <returns>True, if action was handled</returns>
+        protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
+        {
+            if (placementTarget == null)
+            {
+                throw new ArgumentNullException(nameof(placementTarget));
+            }
+
+            if (IsOpen)
+            {
+                if (placementTarget == Target)
+                {
+                    return false;
+                }
+                else // Close before opening a new one
+                {
+                    _ = HideCore(false);
+                }
+            }
+
+            if (Popup.Parent != null && Popup.Parent != placementTarget)
+            {
+                ((ISetLogicalParent)Popup).SetParent(null);
+            }
+
+            if (Popup.Parent == null || Popup.PlacementTarget != placementTarget)
+            {
+                Popup.PlacementTarget = Target = placementTarget;
+                ((ISetLogicalParent)Popup).SetParent(placementTarget);
+                Popup.TemplatedParent = placementTarget.TemplatedParent;
+            }
+
+            if (Popup.Child == null)
+            {
+                Popup.Child = CreatePresenter();
+            }
+
+            Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
+
+            if (CancelOpening())
+            {
+                return false;
+            }
+
+            PositionPopup(showAtPointer);
+            IsOpen = Popup.IsOpen = true;
+            OnOpened();
+
+            placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
+            placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
+
+            if (ShowMode == FlyoutShowMode.Standard)
+            {
+                // Try and focus content inside Flyout
+                if (Popup.Child.Focusable)
+                {
+                    FocusManager.Instance?.Focus(Popup.Child);
+                }
+                else
+                {
+                    var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
+                    if (nextFocus != null)
+                    {
+                        FocusManager.Instance?.Focus(nextFocus);
+                    }
+                }
+            }
+            else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
+            {
+                _transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
+            }
+
+            return true;
+        }
+
+        private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
+        {
+            _ = HideCore(false);
+        }
+
+        private void HandleTransientDismiss(RawInputEventArgs args)
+        {
+            if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move)
+            {
+                // In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
+                // shown as long as the pointer is within a certain px distance from the
+                // flyout itself. I'm not sure what WinUI uses, but I'm defaulting to 
+                // 100px, which seems about right
+                // enlargedPopupRect is the Flyout bounds enlarged 100px
+                // For windowed popups, enlargedPopupRect is in screen coordinates,
+                // for overlay popups, its in OverlayLayer coordinates
+
+                if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null)
+                {
+                    // Only do this once when the Flyout opens & cache the result
+                    if (Popup?.Host is PopupRoot root)
+                    {
+                        // Get the popup root bounds and convert to screen coordinates
+                        
+                        var tmp = root.Bounds.Inflate(100);
+                        _enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight));
+                    }
+                    else if (Popup?.Host is OverlayPopupHost host)
+                    {
+                        // Overlay popups are in OverlayLayer coordinates, just use that
+                        _enlargedPopupRect = host.Bounds.Inflate(100);
+                    }
+
+                    return;
+                }
+
+                if (Popup?.Host is PopupRoot && pArgs.Root is Visual eventRoot)
+                {
+                    // As long as the pointer stays within the enlargedPopupRect
+                    // the flyout stays open. If it leaves, close it
+                    // Despite working in screen coordinates, leaving the TopLevel
+                    // window will not close this (as pointer events stop), which 
+                    // does match UWP
+                    var pt = eventRoot.PointToScreen(pArgs.Position);
+                    if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false)
+                    {
+                        HideCore(false);
+                    }
+                }
+                else if (Popup?.Host is OverlayPopupHost)
+                {
+                    // Same as above here, but just different coordinate space
+                    // so we don't need to translate
+                    if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false)
+                    {
+                        HideCore(false);
+                    }
+                }
+            }
+        }
+
+        protected virtual void OnOpening(CancelEventArgs args)
+        {
+            Opening?.Invoke(this, args);
+        }
+        
+        protected virtual void OnClosing(CancelEventArgs args)
+        {
+            Closing?.Invoke(this, args);
+        }
+
+        /// <summary>
+        /// Used to create the content the Flyout displays
+        /// </summary>
+        /// <returns></returns>
+        protected abstract Control CreatePresenter();
+
+        private Popup CreatePopup()
+        {
+            var popup = new Popup
+            {
+                WindowManagerAddShadowHint = false,
+                IsLightDismissEnabled = true,
+                //Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
+                OverlayDismissEventPassThrough = false
+            };
+
+            popup.Opened += OnPopupOpened;
+            popup.Closed += OnPopupClosed;
+            popup.Closing += OnPopupClosing;
+            popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
+            return popup;
+        }
+
+        private void OnPopupOpened(object? sender, EventArgs e)
+        {
+            IsOpen = true;
+
+            _popupHostChangedHandler?.Invoke(Popup.Host);
+        }
+
+        private void OnPopupClosing(object? sender, CancelEventArgs e)
+        {
+            if (IsOpen)
+            {
+                e.Cancel = CancelClosing();
+            }
+        }
+
+        private void OnPopupClosed(object? sender, EventArgs e)
+        {
+            HideCore(false);
+
+            _popupHostChangedHandler?.Invoke(null);
+        }
+
+        // This method is handling both popup logical tree and target logical tree.
+        private void OnPlacementTargetOrPopupKeyUp(object? sender, KeyEventArgs e)
+        {
+            if (!e.Handled
+                && IsOpen
+                && Target?.ContextFlyout == this)
+            {
+                var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
+
+                if (keymap?.OpenContextMenu.Any(k => k.Matches(e)) == true)
+                {
+                    e.Handled = HideCore();
+                }
+            }
+        }
+
+        private void PositionPopup(bool showAtPointer)
+        {
+            Size sz;
+            // Popup.Child can't be null here, it was set in ShowAtCore.
+            if (Popup.Child!.DesiredSize.IsDefault)
+            {
+                // Popup may not have been shown yet. Measure content
+                sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
+            }
+            else
+            {
+                sz = Popup.Child.DesiredSize;
+            }
+
+            Popup.VerticalOffset = VerticalOffset;
+            Popup.HorizontalOffset = HorizontalOffset;
+            Popup.PlacementAnchor = PlacementAnchor;
+            Popup.PlacementGravity = PlacementGravity;
+            if (showAtPointer)
+            {
+                Popup.PlacementMode = PlacementMode.Pointer;
+            }
+            else
+            {
+                Popup.PlacementMode = Placement;
+                Popup.PlacementConstraintAdjustment =
+                    PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
+                    PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;
+            }
+        }
+
+        private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+        {
+            if (args.Sender is Control c)
+            {
+                if (args.OldValue is FlyoutBase)
+                {
+                    c.ContextRequested -= OnControlContextRequested;
+                }
+                if (args.NewValue is FlyoutBase)
+                {
+                    c.ContextRequested += OnControlContextRequested;
+                }
+            }
+        }
+
+        private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e)
+        {
+            if (!e.Handled
+                && sender is Control control
+                && control.ContextFlyout is { } flyout)
+            {
+                if (control.ContextMenu != null)
+                {
+                    Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
+                    return;
+                }
+
+                if (flyout is PopupFlyoutBase popupFlyout)
+                {
+                    // We do not support absolute popup positioning yet, so we ignore "point" at this moment.
+                    var triggeredByPointerInput = e.TryGetPosition(null, out _);
+                    e.Handled = popupFlyout.ShowAtCore(control, triggeredByPointerInput);
+                }
+                else
+                {
+                    flyout.ShowAt(control);
+                    e.Handled = true;
+                }
+            }
+        }
+
+        private bool CancelClosing()
+        {
+            var eventArgs = new CancelEventArgs();
+            OnClosing(eventArgs);
+            return eventArgs.Cancel;
+        }
+
+        private bool CancelOpening()
+        {
+            var eventArgs = new CancelEventArgs();
+            OnOpening(eventArgs);
+            return eventArgs.Cancel;
+        }
+
+        internal static void SetPresenterClasses(Control? presenter, Classes classes)
+        {
+            if(presenter is null)
+            {
+                return;
+            }
+            //Remove any classes no longer in use, ignoring pseudo classes
+            for (int i = presenter.Classes.Count - 1; i >= 0; i--)
+            {
+                if (!classes.Contains(presenter.Classes[i]) &&
+                    !presenter.Classes[i].Contains(':'))
+                {
+                    presenter.Classes.RemoveAt(i);
+                }
+            }
+
+            //Add new classes
+            presenter.Classes.AddRange(classes);
+        }
+    }
+}

+ 52 - 6
src/Avalonia.Controls/PlacementMode.cs

@@ -13,28 +13,74 @@ namespace Avalonia.Controls
         Pointer,
         Pointer,
 
 
         /// <summary>
         /// <summary>
-        /// The popup is placed at the bottom left of its target.
+        /// Preferred location is below the target element.
         /// </summary>
         /// </summary>
         Bottom,
         Bottom,
 
 
         /// <summary>
         /// <summary>
-        /// The popup is placed at the top right of its target.
+        /// Preferred location is to the right of the target element.
         /// </summary>
         /// </summary>
         Right,
         Right,
         
         
         /// <summary>
         /// <summary>
-        /// The popup is placed at the top left of its target.
+        /// Preferred location is to the left of the target element.
         /// </summary>
         /// </summary>
         Left,
         Left,
         
         
         /// <summary>
         /// <summary>
-        /// The popup is placed at the top left of its target.
+        /// Preferred location is above the target element.
         /// </summary>
         /// </summary>
         Top,
         Top,
         
         
         /// <summary>
         /// <summary>
-        /// The popup is placed according to anchor and gravity rules
+        /// Preferred location is centered over the target element.
         /// </summary>
         /// </summary>
-        AnchorAndGravity
+        Center,
+
+        /// <summary>
+        /// The popup is placed according to <see cref="Popup.PlacementAnchor"/> and <see cref="Popup.PlacementGravity"/> rules.
+        /// </summary>
+        AnchorAndGravity,
+        
+        /// <summary>
+        /// Preferred location is above the target element, with the left edge of the popup
+        /// aligned with the left edge of the target element.
+        /// </summary>
+        TopEdgeAlignedLeft,
+
+        /// <summary>
+        /// Preferred location is above the target element, with the right edge of popup aligned with right edge of the target element.
+        /// </summary>
+        TopEdgeAlignedRight,
+
+        /// <summary>
+        /// Preferred location is below the target element, with the left edge of popup aligned with left edge of the target element.
+        /// </summary>
+        BottomEdgeAlignedLeft,
+
+        /// <summary>
+        /// Preferred location is below the target element, with the right edge of popup aligned with right edge of the target element.
+        /// </summary>
+        BottomEdgeAlignedRight,
+
+        /// <summary>
+        /// Preferred location is to the left of the target element, with the top edge of popup aligned with top edge of the target element.
+        /// </summary>
+        LeftEdgeAlignedTop,
+
+        /// <summary>
+        /// Preferred location is to the left of the target element, with the bottom edge of popup aligned with bottom edge of the target element.
+        /// </summary>
+        LeftEdgeAlignedBottom,
+
+        /// <summary>
+        /// Preferred location is to the right of the target element, with the top edge of popup aligned with top edge of the target element.
+        /// </summary>
+        RightEdgeAlignedTop,
+
+        /// <summary>
+        /// Preferred location is to the right of the target element, with the bottom edge of popup aligned with bottom edge of the target element.
+        /// </summary>
+        RightEdgeAlignedBottom
     }
     }
 }
 }

+ 21 - 27
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@@ -478,35 +478,29 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
 
 
                 var bounds = new Rect(default, target.Bounds.Size);
                 var bounds = new Rect(default, target.Bounds.Size);
                 var anchorRect = rect ?? bounds;
                 var anchorRect = rect ?? bounds;
-                positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
+                positionerParameters.AnchorRectangle =  anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
 
 
-                if (placement == PlacementMode.Right)
+                var parameters = placement switch
                 {
                 {
-                    positionerParameters.Anchor = PopupAnchor.TopRight;
-                    positionerParameters.Gravity = PopupGravity.BottomRight;
-                }
-                else if (placement == PlacementMode.Bottom)
-                {
-                    positionerParameters.Anchor = PopupAnchor.BottomLeft;
-                    positionerParameters.Gravity = PopupGravity.BottomRight;
-                }
-                else if (placement == PlacementMode.Left)
-                {
-                    positionerParameters.Anchor = PopupAnchor.TopLeft;
-                    positionerParameters.Gravity = PopupGravity.BottomLeft;
-                }
-                else if (placement == PlacementMode.Top)
-                {
-                    positionerParameters.Anchor = PopupAnchor.TopLeft;
-                    positionerParameters.Gravity = PopupGravity.TopRight;
-                }
-                else if (placement == PlacementMode.AnchorAndGravity)
-                {
-                    positionerParameters.Anchor = anchor;
-                    positionerParameters.Gravity = gravity;
-                }
-                else
-                    throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
+                    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.Center => (PopupAnchor.None, PopupGravity.None),
+                    PlacementMode.AnchorAndGravity => (anchor, gravity),
+                    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 Popup.PlacementMode")
+                };
+                positionerParameters.Anchor = parameters.Item1;
+                positionerParameters.Gravity = parameters.Item2;
             }
             }
 
 
             // Invert coordinate system if FlowDirection is RTL
             // Invert coordinate system if FlowDirection is RTL

+ 1 - 1
src/Avalonia.Controls/SplitButton/SplitButton.cs

@@ -172,7 +172,7 @@ namespace Avalonia.Controls
                 flyout.Opened += Flyout_Opened;
                 flyout.Opened += Flyout_Opened;
                 flyout.Closed += Flyout_Closed;
                 flyout.Closed += Flyout_Closed;
 
 
-                _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged);
+                _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged);
             }
             }
         }
         }
 
 

+ 6 - 6
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@@ -82,7 +82,7 @@ namespace Avalonia.Diagnostics.ViewModels
                 {
                 {
                     Popup p => GetPopupHostObservable(p),
                     Popup p => GetPopupHostObservable(p),
                     Control c => Observable.CombineLatest(
                     Control c => Observable.CombineLatest(
-                            new IObservable<IPopupHostProvider?>[]
+                            new IObservable<object?>[]
                             {
                             {
                                 c.GetObservable(Control.ContextFlyoutProperty),
                                 c.GetObservable(Control.ContextFlyoutProperty),
                                 c.GetObservable(Control.ContextMenuProperty),
                                 c.GetObservable(Control.ContextMenuProperty),
@@ -93,11 +93,11 @@ namespace Avalonia.Diagnostics.ViewModels
                         .Select(
                         .Select(
                             items =>
                             items =>
                             {
                             {
-                                var contextFlyout = items[0];
-                                var contextMenu = (ContextMenu?)items[1];
-                                var attachedFlyout = items[2];
-                                var toolTip = items[3];
-                                var buttonFlyout = items[4];
+                                var contextFlyout = items[0] as IPopupHostProvider;
+                                var contextMenu = items[1] as ContextMenu;
+                                var attachedFlyout = items[2] as IPopupHostProvider;
+                                var toolTip = items[3] as IPopupHostProvider;
+                                var buttonFlyout = items[4] as IPopupHostProvider;
 
 
                                 if (contextMenu != null)
                                 if (contextMenu != null)
                                     //Note: ContextMenus are special since all the items are added as visual children.
                                     //Note: ContextMenus are special since all the items are added as visual children.