diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 4e48811c35..c91b58311b 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -71,10 +71,5 @@ namespace Avalonia.Android { throw new NotSupportedException(); } - - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs deleted file mode 100644 index e89414d1f8..0000000000 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using Android.Content; -using Android.Graphics; -using Android.Runtime; -using Android.Views; -using Avalonia.Controls; -using Avalonia.Platform; - -namespace Avalonia.Android.Platform.SkiaPlatform -{ - class PopupImpl : TopLevelImpl, IPopupImpl - { - private PixelPoint _position; - private bool _isAdded; - Action IWindowBaseImpl.Activated { get; set; } - public Action PositionChanged { get; set; } - public Action Deactivated { get; set; } - - public PopupImpl() : base(ActivityTracker.Current, true) - { - } - - private Size _clientSize = new Size(1, 1); - - public void Resize(Size value) - { - if (View == null) - return; - _clientSize = value; - UpdateParams(); - } - - public void SetMinMaxSize(Size minSize, Size maxSize) - { - } - - public IScreenImpl Screen { get; } - - public PixelPoint Position - { - get { return _position; } - set - { - _position = value; - PositionChanged?.Invoke(_position); - UpdateParams(); - } - } - - WindowManagerLayoutParams CreateParams() => new WindowManagerLayoutParams(0, - WindowManagerFlags.NotTouchModal, Format.Translucent) - { - Gravity = GravityFlags.Left | GravityFlags.Top, - WindowAnimations = 0, - X = (int) _position.X, - Y = (int) _position.Y, - Width = Math.Max(1, (int) _clientSize.Width), - Height = Math.Max(1, (int) _clientSize.Height) - }; - - void UpdateParams() - { - if (_isAdded) - ActivityTracker.Current?.WindowManager?.UpdateViewLayout(View, CreateParams()); - } - - public override void Show() - { - if (_isAdded) - return; - ActivityTracker.Current.WindowManager.AddView(View, CreateParams()); - _isAdded = true; - } - - public override void Hide() - { - if (_isAdded) - { - var wm = View.Context.ApplicationContext.GetSystemService(Context.WindowService) - .JavaCast(); - wm.RemoveView(View); - _isAdded = false; - } - } - - public override void Dispose() - { - Hide(); - base.Dispose(); - } - - - public void Activate() - { - } - - public void BeginMoveDrag() - { - //Not supported - } - - public void BeginResizeDrag(WindowEdge edge) - { - //Not supported - } - - public void SetTopmost(bool value) - { - //Not supported - } - } -} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index f42faeaa63..0d0d9db252 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -191,6 +191,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } + public IPopupImpl CreatePopup() => null; + ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface); } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f32b8fabc6..a70d26624c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -202,7 +202,7 @@ namespace Avalonia.Controls { if (!e.Handled) { - if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot) + if (_popup?.IsInsidePopup((IVisual)e.Source) == true) { if (UpdateSelectionFromEventSource(e.Source)) { diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index d0804107b3..a5025df82d 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -91,6 +91,8 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (control == null) + throw new ArgumentNullException(nameof(control)); if (IsOpen) { return; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 9c53dc0c10..29f0374301 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -61,5 +61,6 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action Closed { get; set; } public abstract IMouseDevice MouseDevice { get; } + public IPopupImpl CreatePopup() => null; } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index bd558af5ef..38cc3f6daf 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls public bool IsTopLevel => Parent is Menu; /// - bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false; + bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; /// IMenuElement IMenuItem.Parent => Parent as IMenuElement; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 93873cbf7d..aa91224572 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -150,7 +150,7 @@ namespace Avalonia.Controls.Notifications private void Install(Window host) { var adornerLayer = host.GetVisualDescendants() - .OfType() + .OfType() .FirstOrDefault() ?.AdornerLayer; diff --git a/src/Avalonia.Controls/PlacementMode.cs b/src/Avalonia.Controls/PlacementMode.cs index db77b6a365..99958c4c9e 100644 --- a/src/Avalonia.Controls/PlacementMode.cs +++ b/src/Avalonia.Controls/PlacementMode.cs @@ -23,6 +23,21 @@ namespace Avalonia.Controls /// /// The popup is placed at the top right of its target. /// - Right + Right, + + /// + /// The popup is placed at the top left of its target. + /// + Left, + + /// + /// The popup is placed at the top left of its target. + /// + Top, + + /// + /// The popup is placed according to anchor and gravity rules + /// + AnchorAndGravity } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 5f63a44717..b0dfa4185e 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -396,7 +396,7 @@ namespace Avalonia.Controls.Platform protected internal virtual void WindowDeactivated(object sender, EventArgs e) { - Menu.Close(); + Menu?.Close(); } protected void Click(IMenuItem item) diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index 1b606f550b..2978016519 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.Primitives.PopupPositioning; + namespace Avalonia.Platform { /// @@ -8,6 +10,6 @@ namespace Avalonia.Platform /// public interface IPopupImpl : IWindowBaseImpl { - + IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 8d8ce35c38..cfbc0b1c4b 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -107,5 +107,7 @@ namespace Avalonia.Platform /// [CanBeNull] IMouseDevice MouseDevice { get; } + + IPopupImpl CreatePopup(); } } diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index b37521de30..8c99dffc28 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -15,21 +15,10 @@ namespace Avalonia.Platform /// void Hide(); - /// - /// Starts moving a window with left button being held. Should be called from left mouse button press event handler. - /// - void BeginMoveDrag(); - - /// - /// Starts resizing a window. This function is used if an application has window resizing controls. - /// Should be called from left mouse button press event handler - /// - void BeginResizeDrag(WindowEdge edge); - /// /// Gets the position of the window in device pixels. /// - PixelPoint Position { get; set; } + PixelPoint Position { get; } /// /// Gets or sets a method called when the window's position changes. @@ -61,17 +50,6 @@ namespace Avalonia.Platform /// Size MaxClientSize { get; } - /// - /// Sets the client size of the top level. - /// - void Resize(Size clientSize); - - /// - /// Minimum width of the window. - /// - /// - void SetMinMaxSize(Size minSize, Size maxSize); - /// /// Sets whether this window appears on top of all other windows /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 2ddc5a5c85..bc5d38c845 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -57,5 +57,32 @@ namespace Avalonia.Platform /// Return true to prevent the underlying implementation from closing. /// Func Closing { get; set; } + + /// + /// Starts moving a window with left button being held. Should be called from left mouse button press event handler. + /// + void BeginMoveDrag(); + + /// + /// Starts resizing a window. This function is used if an application has window resizing controls. + /// Should be called from left mouse button press event handler + /// + void BeginResizeDrag(WindowEdge edge); + + /// + /// Sets the client size of the top level. + /// + void Resize(Size clientSize); + + /// + /// Sets the client size of the top level. + /// + void Move(PixelPoint point); + + /// + /// Minimum width of the window. + /// + /// + void SetMinMaxSize(Size minSize, Size maxSize); } } diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 5c2c1a8da3..a55bd63c6a 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -4,6 +4,5 @@ namespace Avalonia.Platform { IWindowImpl CreateWindow(); IEmbeddableWindowImpl CreateEmbeddableWindow(); - IPopupImpl CreatePopup(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index fa01b9e839..ef453274b8 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -41,10 +41,5 @@ namespace Avalonia.Controls.Platform throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered."); return platform.CreateEmbeddableWindow(); } - - public static IPopupImpl CreatePopup() - { - return AvaloniaLocator.Current.GetService().CreatePopup(); - } } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index c2690d503d..e2e73bd465 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -229,6 +229,7 @@ namespace Avalonia.Controls.Presenters if (oldChild != null) { VisualChildren.Remove(oldChild); + ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent); } if (oldChild?.Parent == this) diff --git a/src/Avalonia.Controls/Primitives/AdornerDecorator.cs b/src/Avalonia.Controls/Primitives/AdornerDecorator.cs deleted file mode 100644 index 4608d64806..0000000000 --- a/src/Avalonia.Controls/Primitives/AdornerDecorator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.LogicalTree; - -namespace Avalonia.Controls.Primitives -{ - public class AdornerDecorator : Decorator - { - public AdornerDecorator() - { - AdornerLayer = new AdornerLayer(); - ((ISetLogicalParent)AdornerLayer).SetParent(this); - AdornerLayer.ZIndex = int.MaxValue; - VisualChildren.Add(AdornerLayer); - } - - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnAttachedToLogicalTree(e); - - ((ILogical)AdornerLayer).NotifyAttachedToLogicalTree(e); - } - - public AdornerLayer AdornerLayer - { - get; - } - - protected override Size MeasureOverride(Size availableSize) - { - AdornerLayer.Measure(availableSize); - return base.MeasureOverride(availableSize); - } - - protected override Size ArrangeOverride(Size finalSize) - { - AdornerLayer.Arrange(new Rect(finalSize)); - return base.ArrangeOverride(finalSize); - } - } -} diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index d198570909..ebe5e0a93e 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives public static AdornerLayer GetAdornerLayer(IVisual visual) { return visual.GetVisualAncestors() - .OfType() + .OfType() .FirstOrDefault() ?.AdornerLayer; } diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs new file mode 100644 index 0000000000..74a3ca8818 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public interface IPopupHost : IDisposable + { + void SetChild(IControl control); + IContentPresenter Presenter { get; } + IVisual HostedVisualTreeRoot { get; } + + event EventHandler TemplateApplied; + + void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, + PopupPositioningEdge gravity = PopupPositioningEdge.None); + void Show(); + void Hide(); + IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, + StyledProperty minWidthProperty, StyledProperty maxWidthProperty, + StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty); + } +} diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs new file mode 100644 index 0000000000..487a5e91e4 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -0,0 +1,38 @@ +using System.Linq; +using Avalonia.Rendering; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public class OverlayLayer : Canvas, ICustomSimpleHitTest + { + public Size AvailableSize { get; private set; } + public static OverlayLayer GetOverlayLayer(IVisual visual) + { + foreach(var v in visual.GetVisualAncestors()) + if(v is VisualLayerManager vlm) + if (vlm.OverlayLayer != null) + return vlm.OverlayLayer; + if (visual is TopLevel tl) + { + var layers = tl.GetVisualDescendants().OfType().FirstOrDefault(); + return layers?.OverlayLayer; + } + + return null; + } + + public bool HitTest(Point point) + { + return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); + } + + protected override Size ArrangeOverride(Size finalSize) + { + // We are saving it here since child controls might need to know the entire size of the overlay + // and Bounds won't be updated in time + AvailableSize = finalSize; + return base.ArrangeOverride(finalSize); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs new file mode 100644 index 0000000000..3dc9d302db --- /dev/null +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup + { + private readonly OverlayLayer _overlayLayer; + private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); + private ManagedPopupPositioner _positioner; + private Point _lastRequestedPosition; + private bool _shown; + + public OverlayPopupHost(OverlayLayer overlayLayer) + { + _overlayLayer = overlayLayer; + _positioner = new ManagedPopupPositioner(this); + } + + public void SetChild(IControl control) + { + Content = control; + } + + public IVisual HostedVisualTreeRoot => null; + + /// + IInteractive IInteractive.InteractiveParent => Parent; + + public void Dispose() => Hide(); + + + public void Show() + { + _overlayLayer.Children.Add(this); + _shown = true; + } + + public void Hide() + { + _overlayLayer.Children.Remove(this); + _shown = false; + } + + public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, + StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty) + { + // Topmost property is not supported + var bindings = new List(); + + void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); + Bind(WidthProperty, widthProperty); + Bind(MinWidthProperty, minWidthProperty); + Bind(MaxWidthProperty, maxWidthProperty); + Bind(HeightProperty, heightProperty); + Bind(MinHeightProperty, minHeightProperty); + Bind(MaxHeightProperty, maxHeightProperty); + + return Disposable.Create(() => + { + foreach (var x in bindings) + x.Dispose(); + }); + } + + public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None) + { + _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor, + gravity); + UpdatePosition(); + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_positionerParameters.Size != finalSize) + { + _positionerParameters.Size = finalSize; + UpdatePosition(); + } + return base.ArrangeOverride(finalSize); + } + + + private void UpdatePosition() + { + // Don't bother the positioner with layout system artifacts + if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0) + return; + if (_shown) + { + _positioner.Update(_positionerParameters); + } + } + + IReadOnlyList IManagedPopupPositionerPopup.Screens + { + get + { + var rc = new Rect(default, _overlayLayer.AvailableSize); + return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)}; + } + } + + Rect IManagedPopupPositionerPopup.ParentClientAreaScreenGeometry => + new Rect(default, _overlayLayer.Bounds.Size); + + void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize) + { + _lastRequestedPosition = devicePoint; + Dispatcher.UIThread.Post(() => + { + OverlayLayer.SetLeft(this, _lastRequestedPosition.X); + OverlayLayer.SetTop(this, _lastRequestedPosition.Y); + }, DispatcherPriority.Layout); + } + + Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt; + + Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size; + + public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver) + { + var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup(); + if (platform != null) + return new PopupRoot((TopLevel)target.GetVisualRoot(), platform, dependencyResolver); + + var overlayLayer = OverlayLayer.GetOverlayLayer(target); + if (overlayLayer == null) + throw new InvalidOperationException( + "Unable to create IPopupImpl and no overlay layer is found for the target control"); + + + return new OverlayPopupHost(overlayLayer); + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size)); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 058658357f..5ddbed5944 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,7 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Reactive.Disposables; +using Avalonia.Controls.Presenters; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -42,7 +47,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty ObeyScreenEdgesProperty = - AvaloniaProperty.Register(nameof(ObeyScreenEdges)); + AvaloniaProperty.Register(nameof(ObeyScreenEdges), true); /// /// Defines the property. @@ -75,10 +80,12 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(Topmost)); private bool _isOpen; - private PopupRoot _popupRoot; + private IPopupHost _popupHost; private TopLevel _topLevel; private IDisposable _nonClientListener; + private IDisposable _presenterSubscription; bool _ignoreIsOpenChanged = false; + private List _bindings = new List(); /// /// Initializes static members of the class. @@ -88,7 +95,11 @@ namespace Avalonia.Controls.Primitives IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); - TopmostProperty.Changed.AddClassHandler((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue); + } + + public Popup() + { + } /// @@ -101,10 +112,7 @@ namespace Avalonia.Controls.Primitives /// public event EventHandler Opened; - /// - /// Raised when the popup root has been created, but before it has been shown. - /// - public event EventHandler PopupRootCreated; + public IPopupHost Host => _popupHost; /// /// Gets or sets the control to display in the popup. @@ -147,10 +155,7 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } - /// - /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary - /// when its opened at a position where it would otherwise overlap the screen edge. - /// + [Obsolete("This property has no effect")] public bool ObeyScreenEdges { get => GetValue(ObeyScreenEdgesProperty); @@ -184,11 +189,6 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementTargetProperty, value); } } - /// - /// Gets the root of the popup window. - /// - public PopupRoot PopupRoot => _popupRoot; - /// /// Gets or sets a value indicating whether the popup should stay open when the popup is /// pressed or loses focus. @@ -211,63 +211,58 @@ namespace Avalonia.Controls.Primitives /// /// Gets the root of the popup window. /// - IVisual IVisualTreeHost.Root => _popupRoot; + IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot; /// /// Opens the popup. /// public void Open() { - if (_popupRoot == null) + // Popup is currently open + if (_topLevel != null) + return; + CloseCurrent(); + var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType().FirstOrDefault(); + if (placementTarget == null) + throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null"); + + _topLevel = placementTarget.GetVisualRoot() as TopLevel; + + if (_topLevel == null) { - _popupRoot = new PopupRoot(DependencyResolver) - { - [~ContentControl.ContentProperty] = this[~ChildProperty], - [~WidthProperty] = this[~WidthProperty], - [~HeightProperty] = this[~HeightProperty], - [~MinWidthProperty] = this[~MinWidthProperty], - [~MaxWidthProperty] = this[~MaxWidthProperty], - [~MinHeightProperty] = this[~MinHeightProperty], - [~MaxHeightProperty] = this[~MaxHeightProperty], - }; - - ((ISetLogicalParent)_popupRoot).SetParent(this); + throw new InvalidOperationException( + "Attempted to open a popup not attached to a TopLevel"); } - _popupRoot.Position = GetPosition(); + _popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); + + _bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, + HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty)); - if (_topLevel == null && PlacementTarget != null) + _popupHost.SetChild(Child); + ((ISetLogicalParent)_popupHost).SetParent(this); + _popupHost.ConfigurePosition(placementTarget, + PlacementMode, new Point(HorizontalOffset, VerticalOffset)); + _popupHost.TemplateApplied += RootTemplateApplied; + + var window = _topLevel as Window; + if (window != null) { - _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel; + window.Deactivated += WindowDeactivated; } - - if (_topLevel != null) + else { - var window = _topLevel as Window; - if (window != null) + var parentPopuproot = _topLevel as PopupRoot; + if (parentPopuproot?.Parent is Popup popup) { - window.Deactivated += WindowDeactivated; + popup.Closed += ParentClosed; } - else - { - var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot?.Parent is Popup popup) - { - popup.Closed += ParentClosed; - } - } - _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); - _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick); } + _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); + _nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick); + - PopupRootCreated?.Invoke(this, EventArgs.Empty); - - _popupRoot.Show(); - - if (ObeyScreenEdges) - { - _popupRoot.SnapInsideScreenEdges(); - } + _popupHost.Show(); using (BeginIgnoringIsOpen()) { @@ -282,29 +277,14 @@ namespace Avalonia.Controls.Primitives /// public void Close() { - if (_popupRoot != null) + if (_popupHost != null) { - if (_topLevel != null) - { - _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside); - var window = _topLevel as Window; - if (window != null) - window.Deactivated -= WindowDeactivated; - else - { - var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot?.Parent is Popup popup) - { - popup.Closed -= ParentClosed; - } - } - _nonClientListener?.Dispose(); - _nonClientListener = null; - } - - _popupRoot.Hide(); + _popupHost.TemplateApplied -= RootTemplateApplied; } + _presenterSubscription?.Dispose(); + + CloseCurrent(); using (BeginIgnoringIsOpen()) { IsOpen = false; @@ -313,6 +293,41 @@ namespace Avalonia.Controls.Primitives Closed?.Invoke(this, EventArgs.Empty); } + void CloseCurrent() + { + if (_topLevel != null) + { + _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside); + var window = _topLevel as Window; + if (window != null) + window.Deactivated -= WindowDeactivated; + else + { + var parentPopuproot = _topLevel as PopupRoot; + if (parentPopuproot?.Parent is Popup popup) + { + popup.Closed -= ParentClosed; + } + } + _nonClientListener?.Dispose(); + _nonClientListener = null; + + _topLevel = null; + } + if (_popupHost != null) + { + foreach(var b in _bindings) + b.Dispose(); + _bindings.Clear(); + _popupHost.SetChild(null); + _popupHost.Hide(); + ((ISetLogicalParent)_popupHost).SetParent(null); + _popupHost.Dispose(); + _popupHost = null; + } + + } + /// /// Measures the control. /// @@ -323,27 +338,14 @@ namespace Avalonia.Controls.Primitives return new Size(); } - /// - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnAttachedToLogicalTree(e); - _topLevel = e.Root as TopLevel; - } - /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); - _topLevel = null; - - if (_popupRoot != null) - { - ((ISetLogicalParent)_popupRoot).SetParent(null); - _popupRoot.Dispose(); - _popupRoot = null; - } + Close(); } + /// /// Called when the property changes. /// @@ -380,49 +382,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Gets the position for the popup based on the placement properties. - /// - /// The popup's position in screen coordinates. - protected virtual PixelPoint GetPosition() - { - var result = GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot, - HorizontalOffset, VerticalOffset); - - return result; - } - - internal static PixelPoint GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset) - { - var root = target?.GetVisualRoot(); - var mode = root != null ? placement : PlacementMode.Pointer; - var scaling = root?.RenderScaling ?? 1; - - switch (mode) - { - case PlacementMode.Pointer: - if (popupRoot != null) - { - var screenOffset = PixelPoint.FromPoint(new Point(horizontalOffset, verticalOffset), scaling); - var mouseOffset = ((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default; - return new PixelPoint( - screenOffset.X + mouseOffset.X, - screenOffset.Y + mouseOffset.Y); - } - - return default; - - case PlacementMode.Bottom: - return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? default; - - case PlacementMode.Right: - return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? default; - - default: - throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); - } - } - private void ListenForNonClientClick(RawInputEventArgs e) { var mouse = e as RawPointerEventArgs; @@ -445,17 +404,62 @@ namespace Avalonia.Controls.Primitives } } - private bool IsChildOrThis(IVisual child) + private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e) { - IVisual root = child.GetVisualRoot(); - while (root is PopupRoot) + _popupHost.TemplateApplied -= RootTemplateApplied; + + if (_presenterSubscription != null) { - if (root == PopupRoot) return true; - root = ((PopupRoot)root).Parent.GetVisualRoot(); + _presenterSubscription.Dispose(); + _presenterSubscription = null; + } + + // If the Popup appears in a control template, then the child controls + // that appear in the popup host need to have their TemplatedParent + // properties set. + if (TemplatedParent != null) + { + _popupHost.Presenter?.ApplyTemplate(); + _popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty) + .Subscribe(SetTemplatedParentAndApplyChildTemplates); + } + } + + private void SetTemplatedParentAndApplyChildTemplates(IControl control) + { + if (control != null) + { + var templatedParent = TemplatedParent; + + if (control.TemplatedParent == null) + { + control.SetValue(TemplatedParentProperty, templatedParent); + } + + control.ApplyTemplate(); + + if (!(control is IPresenter) && control.TemplatedParent == templatedParent) + { + foreach (IControl child in control.GetVisualChildren()) + { + SetTemplatedParentAndApplyChildTemplates(child); + } + } } - return false; } + private bool IsChildOrThis(IVisual child) + { + return _popupHost != null && ((IVisual)_popupHost).FindCommonVisualAncestor(child) == _popupHost; + } + + public bool IsInsidePopup(IVisual visual) + { + return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true; + } + + public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver; + private void WindowDeactivated(object sender, EventArgs e) { if (!StaysOpen) diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs new file mode 100644 index 0000000000..3010a3d8a8 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -0,0 +1,358 @@ +// The documentation and flag names in this file are initially taken from +// xdg_shell wayland protocol this API is designed after +// therefore, I'm including the license from wayland-protocols repo + +/* +Copyright © 2008-2013 Kristian Høgsberg +Copyright © 2010-2013 Intel Corporation +Copyright © 2013 Rafael Antognolli +Copyright © 2013 Jasper St. Pierre +Copyright © 2014 Jonas Ådahl +Copyright © 2014 Jason Ekstrand +Copyright © 2014-2015 Collabora, Ltd. +Copyright © 2015 Red Hat Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +--- + +The above is the version of the MIT "Expat" License used by X.org: + + http://cgit.freedesktop.org/xorg/xserver/tree/COPYING + + +Adjustments for Avalonia needs: +Copyright © 2019 Nikita Tsukanov + + +*/ + +using System; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + /// + /// + /// The IPopupPositioner provides a collection of rules for the placement of a + /// a popup relative to its parent. Rules can be defined to ensure + /// the popup remains within the visible area's borders, and to + /// specify how the popup changes its position, such as sliding along + /// an axis, or flipping around a rectangle. These positioner-created rules are + /// constrained by the requirement that a popup must intersect with or + /// be at least partially adjacent to its parent surface. + /// + public struct PopupPositionerParameters + { + private PopupPositioningEdge _gravity; + private PopupPositioningEdge _anchor; + + /// + /// Set the size of the popup that is to be positioned with the positioner + /// object. The size is in scaled coordinates. + /// + public Size Size { get; set; } + + /// + /// Specify the anchor rectangle within the parent that the popup + /// will be placed relative to. The rectangle is relative to the + /// parent geometry + /// + /// The anchor rectangle may not extend outside the window geometry of the + /// popup's parent. The anchor rectangle is in scaled coordinates + /// + public Rect AnchorRectangle { get; set; } + + + /// + /// Defines the anchor point for the anchor rectangle. The specified anchor + /// is used derive an anchor point that the popup will be + /// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or + /// 'BottomRight'), the anchor point will be at the specified corner; + /// otherwise, the derived anchor point will be centered on the specified + /// edge, or in the center of the anchor rectangle if no edge is specified. + /// + public PopupPositioningEdge Anchor + { + get => _anchor; + set + { + PopupPositioningEdgeHelper.ValidateEdge(value); + _anchor = value; + } + } + + /// + /// Defines in what direction a popup should be positioned, relative to + /// the anchor point of the parent. If a corner gravity is + /// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup + /// will be placed towards the specified gravity; otherwise, the popup + /// will be centered over the anchor point on any axis that had no + /// gravity specified. + /// + public PopupPositioningEdge Gravity + { + get => _gravity; + set + { + PopupPositioningEdgeHelper.ValidateEdge(value); + _gravity = value; + } + } + + /// + /// Specify how the popup should be positioned if the originally intended + /// position caused the popup to be constrained, meaning at least + /// partially outside positioning boundaries set by the positioner. The + /// adjustment is set by constructing a bitmask describing the adjustment to + /// be made when the popup is constrained on that axis. + /// + /// If no bit for one axis is set, the positioner will assume that the child + /// surface should not change its position on that axis when constrained. + /// + /// If more than one bit for one axis is set, the order of how adjustments + /// are applied is specified in the corresponding adjustment descriptions. + /// + /// The default adjustment is none. + /// + public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; } + + /// + /// Specify the popup position offset relative to the position of the + /// anchor on the anchor rectangle and the anchor on the popup. For + /// example if the anchor of the anchor rectangle is at (x, y), the popup + /// has the gravity bottom|right, and the offset is (ox, oy), the calculated + /// surface position will be (x + ox, y + oy). The offset position of the + /// surface is the one used for constraint testing. See + /// set_constraint_adjustment. + /// + /// An example use case is placing a popup menu on top of a user interface + /// element, while aligning the user interface element of the parent surface + /// with some user interface element placed somewhere in the popup. + /// + public Point Offset { get; set; } + } + + /// + /// The constraint adjustment value define ways how popup position will + /// be adjusted if the unadjusted position would result in the popup + /// being partly constrained. + /// + /// Whether a popup is considered 'constrained' is left to the positioner + /// to determine. For example, the popup may be partly outside the + /// target platform defined 'work area', thus necessitating the popup's + /// position be adjusted until it is entirely inside the work area. + /// + [Flags] + public enum PopupPositionerConstraintAdjustment + { + /// + /// Don't alter the surface position even if it is constrained on some + /// axis, for example partially outside the edge of an output. + /// + None = 0, + + /// + /// Slide the surface along the x axis until it is no longer constrained. + /// First try to slide towards the direction of the gravity on the x axis + /// until either the edge in the opposite direction of the gravity is + /// unconstrained or the edge in the direction of the gravity is + /// constrained. + /// + /// Then try to slide towards the opposite direction of the gravity on the + /// x axis until either the edge in the direction of the gravity is + /// unconstrained or the edge in the opposite direction of the gravity is + /// constrained. + /// + SlideX = 1, + + + /// + /// Slide the surface along the y axis until it is no longer constrained. + /// + /// First try to slide towards the direction of the gravity on the y axis + /// until either the edge in the opposite direction of the gravity is + /// unconstrained or the edge in the direction of the gravity is + /// constrained. + /// + /// Then try to slide towards the opposite direction of the gravity on the + /// y axis until either the edge in the direction of the gravity is + /// unconstrained or the edge in the opposite direction of the gravity is + /// constrained. + /// */ + /// + SlideY = 2, + + /// + /// Invert the anchor and gravity on the x axis if the surface is + /// constrained on the x axis. For example, if the left edge of the + /// surface is constrained, the gravity is 'left' and the anchor is + /// 'left', change the gravity to 'right' and the anchor to 'right'. + /// + /// If the adjusted position also ends up being constrained, the resulting + /// position of the flip_x adjustment will be the one before the + /// adjustment. + /// + FlipX = 4, + + /// + /// Invert the anchor and gravity on the y axis if the surface is + /// constrained on the y axis. For example, if the bottom edge of the + /// surface is constrained, the gravity is 'bottom' and the anchor is + /// 'bottom', change the gravity to 'top' and the anchor to 'top'. + /// + /// The adjusted position is calculated given the original anchor + /// rectangle and offset, but with the new flipped anchor and gravity + /// values. + /// + /// If the adjusted position also ends up being constrained, the resulting + /// position of the flip_y adjustment will be the one before the + /// adjustment. + /// + FlipY = 8, + All = SlideX|SlideY|FlipX|FlipY + } + + static class PopupPositioningEdgeHelper + { + public static void ValidateEdge(this PopupPositioningEdge edge) + { + if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0) + || + ((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0)) + throw new ArgumentException("Opposite edges specified"); + } + + public static PopupPositioningEdge Flip(this PopupPositioningEdge edge) + { + var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right; + var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom; + if ((edge & hmask) != 0) + edge ^= hmask; + if ((edge & vmask) != 0) + edge ^= vmask; + return edge; + } + + public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge) + { + if ((edge & PopupPositioningEdge.HorizontalMask) != 0) + edge ^= PopupPositioningEdge.HorizontalMask; + return edge; + } + + public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge) + { + if ((edge & PopupPositioningEdge.VerticalMask) != 0) + edge ^= PopupPositioningEdge.VerticalMask; + return edge; + } + + } + + [Flags] + public enum PopupPositioningEdge + { + None, + Top = 1, + Bottom = 2, + Left = 4, + Right = 8, + TopLeft = Top | Left, + TopRight = Top | Right, + BottomLeft = Bottom | Left, + BottomRight = Bottom | Right, + + + VerticalMask = Top | Bottom, + HorizontalMask = Left | Right, + AllMask = VerticalMask|HorizontalMask + } + + public interface IPopupPositioner + { + void Update(PopupPositionerParameters parameters); + } + + static class PopupPositionerExtensions + { + public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters, + TopLevel topLevel, + IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor, PopupPositioningEdge gravity) + { + // We need a better way for tracking the last pointer position + var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position); + + positionerParameters.Offset = offset; + positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All; + if (placement == PlacementMode.Pointer) + { + positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); + positionerParameters.Anchor = PopupPositioningEdge.BottomRight; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else + { + if (target == null) + throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); + var matrix = target.TransformToVisual(topLevel); + if (matrix == null) + { + if (target.GetVisualRoot() == null) + throw new InvalidCastException("Target control is not attached to the visual tree"); + throw new InvalidCastException("Target control is not in the same tree as the popup parent"); + } + + positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size) + .TransformToAABB(matrix.Value); + + if (placement == PlacementMode.Right) + { + positionerParameters.Anchor = PopupPositioningEdge.TopRight; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else if (placement == PlacementMode.Bottom) + { + positionerParameters.Anchor = PopupPositioningEdge.BottomLeft; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else if (placement == PlacementMode.Left) + { + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; + positionerParameters.Gravity = PopupPositioningEdge.BottomLeft; + } + else if (placement == PlacementMode.Top) + { + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; + positionerParameters.Gravity = PopupPositioningEdge.TopRight; + } + else if (placement == PlacementMode.AnchorAndGravity) + { + positionerParameters.Anchor = anchor; + positionerParameters.Gravity = gravity; + } + else + throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); + } + } + } + +} diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs new file mode 100644 index 0000000000..d428952bb9 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + public interface IManagedPopupPositionerPopup + { + IReadOnlyList Screens { get; } + Rect ParentClientAreaScreenGeometry { get; } + void MoveAndResize(Point devicePoint, Size virtualSize); + Point TranslatePoint(Point pt); + Size TranslateSize(Size size); + } + + public class ManagedPopupPositionerScreenInfo + { + public Rect Bounds { get; } + public Rect WorkingArea { get; } + + public ManagedPopupPositionerScreenInfo(Rect bounds, Rect workingArea) + { + Bounds = bounds; + WorkingArea = workingArea; + } + } + + public class ManagedPopupPositioner : IPopupPositioner + { + private readonly IManagedPopupPositionerPopup _popup; + + public ManagedPopupPositioner(IManagedPopupPositionerPopup popup) + { + _popup = popup; + } + + + private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge) + { + double x, y; + if ((edge & PopupPositioningEdge.Left) != 0) + x = anchorRect.X; + else if ((edge & PopupPositioningEdge.Right) != 0) + x = anchorRect.Right; + else + x = anchorRect.X + anchorRect.Width / 2; + + if ((edge & PopupPositioningEdge.Top) != 0) + y = anchorRect.Y; + else if ((edge & PopupPositioningEdge.Bottom) != 0) + y = anchorRect.Bottom; + else + y = anchorRect.Y + anchorRect.Height / 2; + return new Point(x, y); + } + + private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity) + { + double x, y; + if ((gravity & PopupPositioningEdge.Left) != 0) + x = -size.Width; + else if ((gravity & PopupPositioningEdge.Right) != 0) + x = 0; + else + x = -size.Width / 2; + + if ((gravity & PopupPositioningEdge.Top) != 0) + y = -size.Height; + else if ((gravity & PopupPositioningEdge.Bottom) != 0) + y = 0; + else + y = -size.Height / 2; + return anchorPoint + new Point(x, y); + } + + public void Update(PopupPositionerParameters parameters) + { + + Update(_popup.TranslateSize(parameters.Size), parameters.Size, + new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft), + _popup.TranslateSize(parameters.AnchorRectangle.Size)), + parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment, + _popup.TranslatePoint(parameters.Offset)); + } + + + private void Update(Size translatedSize, Size originalSize, + Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity, + PopupPositionerConstraintAdjustment constraintAdjustment, Point offset) + { + var parentGeometry = _popup.ParentClientAreaScreenGeometry; + anchorRect = anchorRect.Translate(parentGeometry.TopLeft); + + Rect GetBounds() + { + var screens = _popup.Screens; + + var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft)) + ?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect)) + ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft)) + ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) + ?? screens.FirstOrDefault(); + return targetScreen?.WorkingArea + ?? new Rect(0, 0, double.MaxValue, double.MaxValue); + } + + var bounds = GetBounds(); + + bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask) + { + if ((edge & PopupPositioningEdge.Left) != 0 + && rc.X < bounds.X) + return false; + + if ((edge & PopupPositioningEdge.Top) != 0 + && rc.Y < bounds.Y) + return false; + + if ((edge & PopupPositioningEdge.Right) != 0 + && rc.Right > bounds.Right) + return false; + + if ((edge & PopupPositioningEdge.Bottom) != 0 + && rc.Bottom > bounds.Bottom) + return false; + + return true; + } + + Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge 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, PopupPositioningEdge.HorizontalMask) + && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0) + { + var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX()); + if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask)) + geo = geo.WithX(flipped.X); + } + + // If sliding is allowed, try moving the rect into the bounds + if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0) + { + geo = geo.WithX(Math.Max(geo.X, bounds.X)); + if (geo.Right > bounds.Right) + geo = geo.WithX(bounds.Right - geo.Width); + } + + // If flipping geometry and anchor is allowed and helps, use the flipped one, + // otherwise leave it as is + if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask) + && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0) + { + var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY()); + if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask)) + geo = geo.WithY(flipped.Y); + } + + // If sliding is allowed, try moving the rect into the bounds + if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0) + { + geo = geo.WithY(Math.Max(geo.Y, bounds.Y)); + if (geo.Bottom > bounds.Bottom) + geo = geo.WithY(bounds.Bottom - geo.Height); + } + + _popup.MoveAndResize(geo.TopLeft, originalSize); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs new file mode 100644 index 0000000000..bb701da651 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform; + +namespace Avalonia.Controls.Primitives.PopupPositioning +{ + /// + /// This class is used to simplify integration of IPopupImpl implementations with popup positioner + /// + public class ManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + { + private readonly IWindowBaseImpl _parent; + + public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); + private readonly MoveResizeDelegate _moveResize; + + public ManagedPopupPositionerPopupImplHelper(IWindowBaseImpl parent, MoveResizeDelegate moveResize) + { + _parent = parent; + _moveResize = moveResize; + } + + public IReadOnlyList Screens => + + _parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo( + s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))).ToList(); + + public Rect ParentClientAreaScreenGeometry + { + get + { + // Popup positioner operates with abstract coordinates, but in our case they are pixel ones + var point = _parent.PointToScreen(default); + var size = PixelSize.FromSize(_parent.ClientSize, _parent.Scaling); + return new Rect(point.X, point.Y, size.Width, size.Height); + + } + } + + public void MoveAndResize(Point devicePoint, Size virtualSize) + { + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling); + } + + public Point TranslatePoint(Point pt) => pt * _parent.Scaling; + + public Size TranslateSize(Size size) => size * _parent.Scaling; + } +} diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index d2e8f1ab92..b7f0c8f47d 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,8 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Controls.Platform; -using Avalonia.Controls.Presenters; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform; @@ -16,9 +17,10 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost + public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { - private IDisposable _presenterSubscription; + private readonly TopLevel _parent; + private PopupPositionerParameters _positionerParameters; /// /// Initializes static members of the class. @@ -31,8 +33,8 @@ namespace Avalonia.Controls.Primitives /// /// Initializes a new instance of the class. /// - public PopupRoot() - : this(null) + public PopupRoot(TopLevel parent, IPopupImpl impl) + : this(parent, impl,null) { } @@ -42,9 +44,10 @@ namespace Avalonia.Controls.Primitives /// /// The dependency resolver to use. If null the default dependency resolver will be used. /// - public PopupRoot(IAvaloniaDependencyResolver dependencyResolver) - : base(PlatformManager.CreatePopup(), dependencyResolver) + public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver) + : base(impl, dependencyResolver) { + _parent = parent; } /// @@ -74,73 +77,61 @@ namespace Avalonia.Controls.Primitives /// public void Dispose() => PlatformImpl?.Dispose(); - /// - /// Moves the Popups position so that it doesnt overlap screen edges. - /// This method can be called immediately after Show has been called. - /// - public void SnapInsideScreenEdges() + private void UpdatePosition() { - var screen = (VisualRoot as WindowBase)?.Screens?.ScreenFromPoint(Position); - - if (screen != null) - { - var scaling = VisualRoot.RenderScaling; - var bounds = PixelRect.FromRect(Bounds, scaling); - var screenX = Position.X + bounds.Width - screen.Bounds.X; - var screenY = Position.Y + bounds.Height - screen.Bounds.Y; - - if (screenX > screen.Bounds.Width) - { - Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); - } - - if (screenY > screen.Bounds.Height) - { - Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); - } - } + PlatformImpl?.PopupPositioner.Update(_positionerParameters); } - /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, + PopupPositioningEdge gravity = PopupPositioningEdge.None) { - base.OnTemplateApplied(e); + _positionerParameters.ConfigurePosition(_parent, target, + placement, offset, anchor, gravity); + + if (_positionerParameters.Size != default) + UpdatePosition(); + } + + public void SetChild(IControl control) => Content = control; - if (Parent?.TemplatedParent != null) + IVisual IPopupHost.HostedVisualTreeRoot => this; + + public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, + StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty) + { + var bindings = new List(); + + void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); + Bind(WidthProperty, widthProperty); + Bind(MinWidthProperty, minWidthProperty); + Bind(MaxWidthProperty, maxWidthProperty); + Bind(HeightProperty, heightProperty); + Bind(MinHeightProperty, minHeightProperty); + Bind(MaxHeightProperty, maxHeightProperty); + Bind(TopmostProperty, topmostProperty); + return Disposable.Create(() => { - if (_presenterSubscription != null) - { - _presenterSubscription.Dispose(); - _presenterSubscription = null; - } - - Presenter?.ApplyTemplate(); - Presenter?.GetObservable(ContentPresenter.ChildProperty) - .Subscribe(SetTemplatedParentAndApplyChildTemplates); - } + foreach (var x in bindings) + x.Dispose(); + }); } - private void SetTemplatedParentAndApplyChildTemplates(IControl control) + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. + protected override Size ArrangeOverride(Size finalSize) { - if (control != null) + using (BeginAutoSizing()) { - var templatedParent = Parent.TemplatedParent; - - if (control.TemplatedParent == null) - { - control.SetValue(TemplatedParentProperty, templatedParent); - } - - control.ApplyTemplate(); - - if (!(control is IPresenter) && control.TemplatedParent == templatedParent) - { - foreach (IControl child in control.GetVisualChildren()) - { - SetTemplatedParentAndApplyChildTemplates(child); - } - } + _positionerParameters.Size = finalSize; + UpdatePosition(); } + + return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); } } } diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs new file mode 100644 index 0000000000..b7229eb121 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using Avalonia.LogicalTree; +using Avalonia.Styling; + +namespace Avalonia.Controls.Primitives +{ + public class VisualLayerManager : Decorator + { + private const int AdornerZIndex = int.MaxValue - 100; + private const int OverlayZIndex = int.MaxValue - 99; + private IStyleHost _styleRoot; + private readonly List _layers = new List(); + + + public bool IsPopup { get; set; } + + public AdornerLayer AdornerLayer + { + get + { + var rv = FindLayer(); + if (rv == null) + AddLayer(rv = new AdornerLayer(), AdornerZIndex); + return rv; + } + } + + public OverlayLayer OverlayLayer + { + get + { + if (IsPopup) + return null; + var rv = FindLayer(); + if(rv == null) + AddLayer(rv = new OverlayLayer(), OverlayZIndex); + return rv; + } + } + + T FindLayer() where T : class + { + foreach (var layer in _layers) + if (layer is T match) + return match; + return null; + } + + void AddLayer(Control layer, int zindex) + { + _layers.Add(layer); + ((ISetLogicalParent)layer).SetParent(this); + layer.ZIndex = zindex; + VisualChildren.Add(layer); + if (((ILogical)this).IsAttachedToLogicalTree) + ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot)); + InvalidateArrange(); + } + + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + _styleRoot = e.Root; + + foreach (var l in _layers) + ((ILogical)l).NotifyAttachedToLogicalTree(e); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _styleRoot = null; + base.OnDetachedFromLogicalTree(e); + foreach (var l in _layers) + ((ILogical)l).NotifyDetachedFromLogicalTree(e); + } + + + protected override Size MeasureOverride(Size availableSize) + { + foreach (var l in _layers) + l.Measure(availableSize); + return base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + foreach (var l in _layers) + l.Arrange(new Rect(finalSize)); + return base.ArrangeOverride(finalSize); + } + } +} diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 28d1ba5e0f..5fe1e3804b 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -4,6 +4,7 @@ using System; using System.Reactive.Linq; using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -60,7 +61,7 @@ namespace Avalonia.Controls private static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); - private PopupRoot _popup; + private IPopupHost _popup; /// /// Initializes static members of the class. @@ -234,19 +235,20 @@ namespace Avalonia.Controls { Close(); - _popup = new PopupRoot { Content = this, }; + _popup = OverlayPopupHost.CreatePopupHost(control, null); + _popup.SetChild(this); ((ISetLogicalParent)_popup).SetParent(control); - _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup, - GetHorizontalOffset(control), GetVerticalOffset(control)); + + _popup.ConfigurePosition(control, GetPlacement(control), + new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); _popup.Show(); - _popup.SnapInsideScreenEdges(); } private void Close() { if (_popup != null) { - _popup.Content = null; + _popup.SetChild(null); _popup.Hide(); _popup = null; } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index d2793fe0dd..ef43746665 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -135,6 +135,12 @@ namespace Avalonia.Controls WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); + + MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); + MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); + MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); + MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); + } /// @@ -155,6 +161,7 @@ namespace Avalonia.Controls impl.Closing = HandleClosing; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); + this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); } /// @@ -239,6 +246,44 @@ namespace Avalonia.Controls set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); } } + /// + /// Gets or sets the window position in screen coordinates. + /// + public PixelPoint Position + { + get { return PlatformImpl?.Position ?? PixelPoint.Origin; } + set + { + PlatformImpl?.Move(value); + } + } + + /// + /// Starts moving a window with left button being held. Should be called from left mouse button press event handler + /// + public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag(); + + /// + /// Starts resizing a window. This function is used if an application has window resizing controls. + /// Should be called from left mouse button press event handler + /// + public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge); + + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. + protected override Size ArrangeOverride(Size finalSize) + { + using (BeginAutoSizing()) + { + PlatformImpl?.Resize(finalSize); + } + + return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); + } + /// Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 40c9fc94d2..a47c55f87c 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -49,10 +49,6 @@ namespace Avalonia.Controls IsVisibleProperty.OverrideDefaultValue(false); IsVisibleProperty.Changed.AddClassHandler(x => x.IsVisibleChanged); - MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); - MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); - MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); - MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); } @@ -67,7 +63,6 @@ namespace Avalonia.Controls impl.Activated = HandleActivated; impl.Deactivated = HandleDeactivated; impl.PositionChanged = HandlePositionChanged; - this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); } /// @@ -96,19 +91,6 @@ namespace Avalonia.Controls get { return _isActive; } private set { SetAndRaise(IsActiveProperty, ref _isActive, value); } } - - /// - /// Gets or sets the window position in screen coordinates. - /// - public PixelPoint Position - { - get { return PlatformImpl?.Position ?? PixelPoint.Origin; } - set - { - if (PlatformImpl is IWindowBaseImpl impl) - impl.Position = value; - } - } public Screens Screens { get; private set; } @@ -208,21 +190,6 @@ namespace Avalonia.Controls return Disposable.Create(() => AutoSizing = false); } - /// - /// Carries out the arrange pass of the window. - /// - /// The final window size. - /// The parameter unchanged. - protected override Size ArrangeOverride(Size finalSize) - { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(finalSize); - } - - return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); - } - /// /// Ensures that the window is initialized. /// @@ -318,16 +285,5 @@ namespace Avalonia.Controls } } } - - /// - /// Starts moving a window with left button being held. Should be called from left mouse button press event handler - /// - public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag(); - - /// - /// Starts resizing a window. This function is used if an application has window resizing controls. - /// Should be called from left mouse button press event handler - /// - public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge); } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index dc01bcb07e..40524ad4b7 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -72,6 +72,11 @@ namespace Avalonia.DesignerSupport.Remote RenderIfNeeded(); } + public void Move(PixelPoint point) + { + + } + public void SetMinMaxSize(Size minSize, Size maxSize) { } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index a7a94130ea..dcfcd42c04 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -40,8 +40,6 @@ namespace Avalonia.DesignerSupport.Remote return s_lastWindow; } - public IPopupImpl CreatePopup() => new WindowStub(); - public static void Initialize(IAvaloniaRemoteTransportConnection transport) { s_transport = transport; diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 9c547279d6..4ce0da60a2 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -5,6 +5,7 @@ using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; @@ -13,7 +14,7 @@ using Avalonia.Rendering; namespace Avalonia.DesignerSupport.Remote { - class WindowStub : IPopupImpl, IWindowImpl + class WindowStub : IWindowImpl, IPopupImpl { public Action Deactivated { get; set; } public Action Activated { get; set; } @@ -29,10 +30,23 @@ namespace Avalonia.DesignerSupport.Remote public Func Closing { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); + public IPopupImpl CreatePopup() => new WindowStub(this); + public PixelPoint Position { get; set; } public Action PositionChanged { get; set; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } + + public WindowStub(IWindowImpl parent = null) + { + if (parent != null) + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, + (_, size, __) => + { + Resize(size); + })); + } + public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); public void Dispose() { @@ -77,6 +91,11 @@ namespace Avalonia.DesignerSupport.Remote { } + public void Move(PixelPoint point) + { + + } + public IScreenImpl Screen { get; } = new ScreenStub(); public void SetMinMaxSize(Size minSize, Size maxSize) @@ -110,6 +129,8 @@ namespace Avalonia.DesignerSupport.Remote public void SetTopmost(bool value) { } + + public IPopupPositioner PopupPositioner { get; } } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index adb27d348d..edde2176bd 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -97,11 +97,6 @@ namespace Avalonia.Native { throw new NotImplementedException(); } - - public IPopupImpl CreatePopup() - { - return new PopupImpl(_factory, _options); - } } public class AvaloniaNativeMacOptions diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 09f822cf46..02810ed155 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -24,6 +24,7 @@ namespace Avalonia { public bool UseDeferredRendering { get; set; } = true; public bool UseGpu { get; set; } = true; + public bool OverlayPopups { get; set; } public string AvaloniaNativeLibraryPath { get; set; } } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index a470caa80e..f776ee0132 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -9,12 +10,26 @@ namespace Avalonia.Native { public class PopupImpl : WindowBaseImpl, IPopupImpl { - public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) + private readonly IAvaloniaNativeFactory _factory; + private readonly AvaloniaNativePlatformOptions _opts; + public PopupImpl(IAvaloniaNativeFactory factory, + AvaloniaNativePlatformOptions opts, + IWindowBaseImpl parent) : base(opts) { + _factory = factory; + _opts = opts; using (var e = new PopupEvents(this)) { Init(factory.CreatePopup(e), factory.CreateScreens()); } + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Position = position; + Resize(size); + //TODO: We ignore the scaling override for now } class PopupEvents : WindowBaseEvents, IAvnWindowEvents @@ -35,5 +50,8 @@ namespace Avalonia.Native { } } + + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this); + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 076fe9ccae..490d5688a8 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -11,9 +11,13 @@ namespace Avalonia.Native { public class WindowImpl : WindowBaseImpl, IWindowImpl { + private readonly IAvaloniaNativeFactory _factory; + private readonly AvaloniaNativePlatformOptions _opts; IAvnWindow _native; public WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) { + _factory = factory; + _opts = opts; using (var e = new WindowEvents(this)) { Init(_native = factory.CreateWindow(e), factory.CreateScreens()); @@ -100,5 +104,9 @@ namespace Avalonia.Native } public Func Closing { get; set; } + public void Move(PixelPoint point) => Position = point; + + public override IPopupImpl CreatePopup() => + _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, this); } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 638879ba14..ae0a2f535b 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -15,7 +15,7 @@ using Avalonia.Threading; namespace Avalonia.Native { - public class WindowBaseImpl : IWindowBaseImpl, + public abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface { IInputRoot _inputRoot; @@ -91,6 +91,7 @@ namespace Avalonia.Native public Action Resized { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice; + public abstract IPopupImpl CreatePopup(); class FramebufferWrapper : ILockedFramebuffer diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index 3d525955b4..c44cc358e8 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -12,11 +12,11 @@ - - + + - + - + diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 0c2d33bc7b..675234c16a 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -40,16 +40,14 @@ StaysOpen="False"> - - - - - + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 9c60b29193..114979fba2 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -19,6 +19,7 @@ + diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml index bc06ab010e..1fd168c009 100644 --- a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml @@ -4,13 +4,13 @@ - + - + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/OverlayPopupHost.xaml b/src/Avalonia.Themes.Default/OverlayPopupHost.xaml new file mode 100644 index 0000000000..35d3a8cff4 --- /dev/null +++ b/src/Avalonia.Themes.Default/OverlayPopupHost.xaml @@ -0,0 +1,14 @@ + diff --git a/src/Avalonia.Themes.Default/PopupRoot.xaml b/src/Avalonia.Themes.Default/PopupRoot.xaml index cc23367ac0..71042f2a98 100644 --- a/src/Avalonia.Themes.Default/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/PopupRoot.xaml @@ -2,11 +2,13 @@ - + + + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 2514422ce8..2a8b5d0fca 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -5,14 +5,14 @@ - + - + diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs index 995781ee9f..d62c2a2e55 100644 --- a/src/Avalonia.Visuals/Media/PixelPoint.cs +++ b/src/Avalonia.Visuals/Media/PixelPoint.cs @@ -59,6 +59,59 @@ namespace Avalonia { return !(left == right); } + + /// + /// Converts the to a . + /// + /// The point. + public static implicit operator PixelVector(PixelPoint p) + { + return new PixelVector(p.X, p.Y); + } + + /// + /// Adds two points. + /// + /// The first point. + /// The second point. + /// A point that is the result of the addition. + public static PixelPoint operator +(PixelPoint a, PixelPoint b) + { + return new PixelPoint(a.X + b.X, a.Y + b.Y); + } + + /// + /// Adds a vector to a point. + /// + /// The point. + /// The vector. + /// A point that is the result of the addition. + public static PixelPoint operator +(PixelPoint a, PixelVector b) + { + return new PixelPoint(a.X + b.X, a.Y + b.Y); + } + + /// + /// Subtracts two points. + /// + /// The first point. + /// The second point. + /// A point that is the result of the subtraction. + public static PixelPoint operator -(PixelPoint a, PixelPoint b) + { + return new PixelPoint(a.X - b.X, a.Y - b.Y); + } + + /// + /// Subtracts a vector from a point. + /// + /// The point. + /// The vector. + /// A point that is the result of the subtraction. + public static PixelPoint operator -(PixelPoint a, PixelVector b) + { + return new PixelPoint(a.X - b.X, a.Y - b.Y); + } /// /// Parses a string. @@ -106,7 +159,7 @@ namespace Avalonia return hash; } } - + /// /// Returns a new with the same Y co-ordinate and the specified X co-ordinate. /// diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index 9c8e5ad1c4..0e2094da07 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -261,6 +261,16 @@ namespace Avalonia { return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom); } + + /// + /// Translates the rectangle by an offset. + /// + /// The offset. + /// The translated rectangle. + public PixelRect Translate(PixelVector offset) + { + return new PixelRect(Position + offset, Size); + } /// /// Gets the union of two rectangles. diff --git a/src/Avalonia.Visuals/Media/PixelVector.cs b/src/Avalonia.Visuals/Media/PixelVector.cs new file mode 100644 index 0000000000..4a623e3bc2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/PixelVector.cs @@ -0,0 +1,203 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using Avalonia.Animation.Animators; +using JetBrains.Annotations; + +namespace Avalonia +{ + /// + /// Defines a vector. + /// + public readonly struct PixelVector + { + /// + /// The X vector. + /// + private readonly int _x; + + /// + /// The Y vector. + /// + private readonly int _y; + + /// + /// Initializes a new instance of the structure. + /// + /// The X vector. + /// The Y vector. + public PixelVector(int x, int y) + { + _x = x; + _y = y; + } + + /// + /// Gets the X vector. + /// + public int X => _x; + + /// + /// Gets the Y vector. + /// + public int Y => _y; + + /// + /// Converts the to a . + /// + /// The vector. + public static explicit operator PixelPoint(PixelVector a) + { + return new PixelPoint(a._x, a._y); + } + + /// + /// Calculates the dot product of two vectors + /// + /// First vector + /// Second vector + /// The dot product + public static int operator *(PixelVector a, PixelVector b) + { + return a.X * b.X + a.Y * b.Y; + } + + /// + /// Scales a vector. + /// + /// The vector + /// The scaling factor. + /// The scaled vector. + public static PixelVector operator *(PixelVector vector, int scale) + { + return new PixelVector(vector._x * scale, vector._y * scale); + } + + /// + /// Scales a vector. + /// + /// The vector + /// The divisor. + /// The scaled vector. + public static PixelVector operator /(PixelVector vector, int scale) + { + return new PixelVector(vector._x / scale, vector._y / scale); + } + + /// + /// Length of the vector + /// + public double Length => Math.Sqrt(X * X + Y * Y); + + /// + /// Negates a vector. + /// + /// The vector. + /// The negated vector. + public static PixelVector operator -(PixelVector a) + { + return new PixelVector(-a._x, -a._y); + } + + /// + /// Adds two vectors. + /// + /// The first vector. + /// The second vector. + /// A vector that is the result of the addition. + public static PixelVector operator +(PixelVector a, PixelVector b) + { + return new PixelVector(a._x + b._x, a._y + b._y); + } + + /// + /// Subtracts two vectors. + /// + /// The first vector. + /// The second vector. + /// A vector that is the result of the subtraction. + public static PixelVector operator -(PixelVector a, PixelVector b) + { + return new PixelVector(a._x - b._x, a._y - b._y); + } + + /// + /// Check if two vectors are equal (bitwise). + /// + /// + /// + public bool Equals(PixelVector other) + { + return _x == other._x && _y == other._y; + } + + /// + /// Check if two vectors are nearly equal (numerically). + /// + /// The other vector. + /// True if vectors are nearly equal. + [Pure] + public bool NearlyEquals(PixelVector other) + { + const float tolerance = float.Epsilon; + + return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + + return obj is PixelVector vector && Equals(vector); + } + + public override int GetHashCode() + { + unchecked + { + return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); + } + } + + public static bool operator ==(PixelVector left, PixelVector right) + { + return left.Equals(right); + } + + public static bool operator !=(PixelVector left, PixelVector right) + { + return !left.Equals(right); + } + + /// + /// Returns the string representation of the point. + /// + /// The string representation of the point. + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); + } + + /// + /// Returns a new vector with the specified X coordinate. + /// + /// The X coordinate. + /// The new vector. + public PixelVector WithX(int x) + { + return new PixelVector(x, _y); + } + + /// + /// Returns a new vector with the specified Y coordinate. + /// + /// The Y coordinate. + /// The new vector. + public PixelVector WithY(int y) + { + return new PixelVector(_x, y); + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7bdc61eb28..e88a7d8db2 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -74,18 +74,13 @@ namespace Avalonia.X11 public IntPtr Display { get; set; } public IWindowImpl CreateWindow() { - return new X11Window(this, false); + return new X11Window(this, null); } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } - - public IPopupImpl CreatePopup() - { - return new X11Window(this, true); - } } } @@ -96,6 +91,7 @@ namespace Avalonia { public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; + public bool OverlayPopups { get; set; } public List GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 18c23aa31e..5481862f23 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive.Disposables; using System.Text; using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; @@ -21,6 +22,7 @@ namespace Avalonia.X11 unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client { private readonly AvaloniaX11Platform _platform; + private readonly IWindowImpl _popupParent; private readonly bool _popup; private readonly X11Info _x11; private bool _invalidated; @@ -38,6 +40,7 @@ namespace Avalonia.X11 private bool _mapped; private HashSet _transientChildren = new HashSet(); private X11Window _transientParent; + private double? _scalingOverride; public object SyncRoot { get; } = new object(); class InputEventContainer @@ -47,10 +50,10 @@ namespace Avalonia.X11 private readonly Queue _inputQueue = new Queue(); private InputEventContainer _lastEvent; private bool _useRenderWindow = false; - public X11Window(AvaloniaX11Platform platform, bool popup) + public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) { _platform = platform; - _popup = popup; + _popup = popupParent != null; _x11 = platform.Info; _mouse = platform.MouseDevice; _keyboard = platform.KeyboardDevice; @@ -66,7 +69,7 @@ namespace Avalonia.X11 | SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity; - if (popup) + if (_popup) { attr.override_redirect = true; valueMask |= SetWindowValuemask.OverrideRedirect; @@ -150,6 +153,8 @@ namespace Avalonia.X11 _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); XFlush(_x11.Display); + if(_popup) + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -453,22 +458,28 @@ namespace Avalonia.X11 } } - private bool UpdateScaling() + private bool UpdateScaling(bool skipResize = false) { lock (SyncRoot) { - var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) - .FirstOrDefault(m => m.Bounds.Contains(Position)); - var newScaling = monitor?.PixelDensity ?? Scaling; + double newScaling; + if (_scalingOverride.HasValue) + newScaling = _scalingOverride.Value; + else + { + var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) + .FirstOrDefault(m => m.Bounds.Contains(Position)); + newScaling = monitor?.PixelDensity ?? Scaling; + } + if (Scaling != newScaling) { - Console.WriteLine( - $"Updating scaling from {Scaling} to {newScaling} as a response to position change to {Position}"); var oldScaledSize = ClientSize; Scaling = newScaling; ScalingChanged?.Invoke(Scaling); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); - Resize(oldScaledSize, true); + if(!skipResize) + Resize(oldScaledSize, true); return true; } @@ -730,6 +741,14 @@ namespace Avalonia.X11 public void Resize(Size clientSize) => Resize(clientSize, false); + public void Move(PixelPoint point) => Position = point; + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Move(position); + _scalingOverride = scaling; + UpdateScaling(true); + Resize(size, true); + } PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling)); @@ -793,7 +812,9 @@ namespace Avalonia.X11 } public IMouseDevice MouseDevice => _mouse; - + public IPopupImpl CreatePopup() + => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); + public void Activate() { if (_x11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero) @@ -937,6 +958,8 @@ namespace Avalonia.X11 { SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)(value ? 0 : 1), _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR, IntPtr.Zero); - } + } + + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 5e2ba51caf..ebaad81fa1 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -59,6 +59,8 @@ namespace Avalonia.LinuxFramebuffer public Size ClientSize => ScaledSize; public IMouseDevice MouseDevice => new MouseDevice(); + public IPopupImpl CreatePopup() => null; + public double Scaling => 1; public IEnumerable Surfaces => new object[] {_outputBackend}; public Action Input { get; set; } diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index a7c1d0a38b..61ccf09e52 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -26,51 +26,64 @@ namespace Avalonia.Skia public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { var session = _surface.BeginDraw(); - var disp = session.Display; - var gl = disp.GlInterface; - gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb); - - var size = session.Size; - var scaling = session.Scaling; - if (size.Width <= 0 || size.Height <= 0 || scaling < 0) - { - throw new InvalidOperationException( - $"Can't create drawing context for surface with {size} size and {scaling} scaling"); - } - - gl.Viewport(0, 0, size.Width, size.Height); - gl.ClearStencil(0); - gl.ClearColor(0, 0, 0, 0); - gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - lock (_grContext) + bool success = false; + try { - _grContext.ResetContext(); - - GRBackendRenderTarget renderTarget = - new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, - new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); - var surface = SKSurface.Create(_grContext, renderTarget, - GRSurfaceOrigin.BottomLeft, - GRPixelConfig.Rgba8888.ToColorType()); + var disp = session.Display; + var gl = disp.GlInterface; + gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb); - var nfo = new DrawingContextImpl.CreateInfo + var size = session.Size; + var scaling = session.Scaling; + if (size.Width <= 0 || size.Height <= 0 || scaling < 0) { - GrContext = _grContext, - Canvas = surface.Canvas, - Dpi = SkiaPlatform.DefaultDpi * scaling, - VisualBrushRenderer = visualBrushRenderer, - DisableTextLcdRendering = true - }; + session.Dispose(); + throw new InvalidOperationException( + $"Can't create drawing context for surface with {size} size and {scaling} scaling"); + } - return new DrawingContextImpl(nfo, Disposable.Create(() => + gl.Viewport(0, 0, size.Width, size.Height); + gl.ClearStencil(0); + gl.ClearColor(0, 0, 0, 0); + gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + lock (_grContext) { + _grContext.ResetContext(); + + GRBackendRenderTarget renderTarget = + new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, + new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); + var surface = SKSurface.Create(_grContext, renderTarget, + GRSurfaceOrigin.BottomLeft, + GRPixelConfig.Rgba8888.ToColorType()); + + var nfo = new DrawingContextImpl.CreateInfo + { + GrContext = _grContext, + Canvas = surface.Canvas, + Dpi = SkiaPlatform.DefaultDpi * scaling, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true + }; + - surface.Canvas.Flush(); - surface.Dispose(); - renderTarget.Dispose(); - _grContext.Flush(); + var ctx = new DrawingContextImpl(nfo, Disposable.Create(() => + { + + surface.Canvas.Flush(); + surface.Dispose(); + renderTarget.Dispose(); + _grContext.Flush(); + session.Dispose(); + })); + success = true; + return ctx; + } + } + finally + { + if(!success) session.Dispose(); - })); } } } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index c89d0a15cf..f698266610 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -240,5 +240,7 @@ namespace Avalonia.Win32.Interop.Wpf return new Vector(1, 1); return new Vector(src.TransformToDevice.M11, src.TransformToDevice.M22); } + + public IPopupImpl CreatePopup() => null; } } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 39f1a95466..c9aa1ce4e7 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -57,5 +58,19 @@ namespace Avalonia.Win32 return base.WndProc(hWnd, msg, wParam, lParam); } } + + public PopupImpl(IWindowBaseImpl parent) + { + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + Move(position); + Resize(size); + //TODO: We ignore the scaling override for now + } + + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c45bf6389e..f20cf394bb 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -41,6 +41,7 @@ namespace Avalonia public bool UseDeferredRendering { get; set; } = true; public bool AllowEglInitialization { get; set; } public bool? EnableMultitouch { get; set; } + public bool OverlayPopups { get; set; } } } @@ -61,6 +62,7 @@ namespace Avalonia.Win32 } public static bool UseDeferredRendering => Options.UseDeferredRendering; + internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } public Size DoubleClickSize => new Size( @@ -210,11 +212,6 @@ namespace Avalonia.Win32 return embedded; } - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } - public IWindowIconImpl LoadIcon(string fileName) { using (var stream = File.OpenRead(fileName)) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2f7805884d..e33e1f11dc 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -131,6 +131,8 @@ namespace Avalonia.Win32 } } + public void Move(PixelPoint point) => Position = point; + public void SetMinMaxSize(Size minSize, Size maxSize) { _minSize = minSize; @@ -248,10 +250,7 @@ namespace Avalonia.Win32 UnmanagedMethods.SetActiveWindow(_hwnd); } - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } + public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); public void Dispose() { diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 15e8b35056..d5f456409f 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -134,5 +134,7 @@ namespace Avalonia.iOS } public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this); + + public IPopupImpl CreatePopup() => null; } } diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 015a122677..ef7dc33f76 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -982,6 +982,8 @@ namespace Avalonia.Controls.UnitTests AutoCompleteBox control = CreateControl(); control.Items = CreateSimpleStringArray(); TextBox textBox = GetTextBox(control); + var window = new Window {Content = control}; + window.ApplyTemplate(); Dispatcher.UIThread.RunJobs(); test.Invoke(control, textBox); } @@ -1027,7 +1029,8 @@ namespace Avalonia.Controls.UnitTests var popup = new Popup { - Name = "PART_Popup" + Name = "PART_Popup", + PlacementTarget = control }.RegisterInNameScope(scope); var panel = new Panel(); diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 58d205deaa..522afc9546 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + new Window { Content = target }.ApplyTemplate(); int openedCount = 0; @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests openedCount++; }; - sut.Open(null); + sut.Open(target); Assert.Equal(1, openedCount); } @@ -53,9 +53,9 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + new Window { Content = target }.ApplyTemplate(); - sut.Open(null); + sut.Open(target); int closedCount = 0; @@ -84,7 +84,8 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + var window = new Window {Content = target}; + window.ApplyTemplate(); _mouse.Click(target, MouseButton.Right); @@ -112,7 +113,8 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - var window = new Window { Content = target }; + var window = new Window {Content = target}; + window.ApplyTemplate(); _mouse.Click(target, MouseButton.Right); @@ -151,7 +153,7 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] + [Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")] public void Cancelling_Closing_Leaves_ContextMenuOpen() { using (Application()) @@ -165,7 +167,9 @@ namespace Avalonia.Controls.UnitTests { ContextMenu = sut }; - new Window { Content = target }; + + var window = new Window {Content = target}; + window.ApplyTemplate(); sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; }; @@ -190,12 +194,12 @@ namespace Avalonia.Controls.UnitTests screenImpl.Setup(x => x.ScreenCount).Returns(1); screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(screen, screen, true) }); - var windowImpl = new Mock(); - windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); - - popupImpl = new Mock(); + popupImpl = MockWindowingPlatform.CreatePopupMock(); popupImpl.SetupGet(x => x.Scaling).Returns(1); + var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object); + windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); + var services = TestServices.StyledWindow.With( inputManager: new InputManager(), windowImpl: windowImpl.Object, diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index 7d05547799..952180d21b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -281,6 +281,37 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = 42; } + [Fact] + public void Should_Set_InheritanceParent_Even_When_LogicalParent_Is_Already_Set() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var (target, host) = CreateTarget(); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + + Assert.Same(logicalParent, child.Parent); + + // InheritanceParent is exposed via StylingParent. + Assert.Same(target, ((IStyledElement)child).StylingParent); + } + + [Fact] + public void Should_Reset_InheritanceParent_When_Child_Removed() + { + var logicalParent = new Canvas(); + var child = new TextBlock(); + var (target, _) = CreateTarget(); + + ((ISetLogicalParent)child).SetParent(logicalParent); + target.Content = child; + target.Content = null; + + // InheritanceParent is exposed via StylingParent. + Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); + } + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() { var templatedParent = new ContentControl diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 059146f17d..0ebe6833d3 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var target = CreateTarget(); + var target = CreateTarget(new Window()); Assert.True(((ILogical)target).IsAttachedToLogicalTree); } @@ -32,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var target = CreateTarget(); + var target = CreateTarget(new Window()); Assert.True(target.Presenter.IsAttachedToLogicalTree); } @@ -43,28 +43,70 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var window = new Window(); var target = new TemplatedControlWithPopup { PopupContent = new Canvas(), }; + window.Content = target; - var root = new TestRoot { Child = target }; - + window.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); - Assert.Equal(target.Popup, ((IStyleHost)target.Popup.PopupRoot).StylingParent); + Assert.Equal(target.Popup, ((IStyleHost)target.Popup.Host).StylingParent); } } + [Fact] + public void PopupRoot_Should_Have_Template_Applied() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var target = new Popup {PlacementMode = PlacementMode.Pointer}; + var child = new Control(); + + window.Content = target; + window.ApplyTemplate(); + target.Open(); + + + Assert.Single(((Visual)target.Host).GetVisualChildren()); + + var templatedChild = ((Visual)target.Host).GetVisualChildren().Single(); + + Assert.IsType(templatedChild); + var contentPresenter = templatedChild.VisualChildren.Single(); + Assert.IsType(contentPresenter); + + + Assert.Equal((PopupRoot)target.Host, ((IControl)templatedChild).TemplatedParent); + Assert.Equal((PopupRoot)target.Host, ((IControl)contentPresenter).TemplatedParent); + } + } + + [Fact] + public void PopupRoot_Should_Have_Null_VisualParent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Popup() {PlacementTarget = new Window()}; + + target.Open(); + + Assert.Null(((Visual)target.Host).GetVisualParent()); + } + } + [Fact] public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new Decorator(); - var target = CreateTarget(); var window = new Window(); + var target = CreateTarget(window); var detachedCount = 0; var attachedCount = 0; @@ -88,8 +130,8 @@ namespace Avalonia.Controls.UnitTests.Primitives using (UnitTestApplication.Start(TestServices.StyledWindow)) { var child = new Decorator(); - var target = CreateTarget(); var window = new Window(); + var target = CreateTarget(window); var detachedCount = 0; var attachedCount = 0; @@ -117,22 +159,23 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var window = new Window(); var target = new TemplatedControlWithPopup { PopupContent = new Canvas(), }; + window.Content = target; - var root = new TestRoot { Child = target }; - + window.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); target.PopupContent = null; } } - private PopupRoot CreateTarget() + private PopupRoot CreateTarget(TopLevel popupParent) { - var result = new PopupRoot + var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup()) { Template = new FuncControlTemplate((parent, scope) => new ContentPresenter @@ -158,6 +201,7 @@ namespace Avalonia.Controls.UnitTests.Primitives new Popup { [!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty], + PlacementTarget = parent }); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 2e22725125..7cb9fccee8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class PopupTests { + protected bool UsePopupHost; + [Fact] public void Setting_Child_Should_Set_Child_Controls_LogicalParent() { @@ -137,20 +139,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new Popup(); - Assert.Null(target.PopupRoot); - } - } - - [Fact] - public void PopupRoot_Should_Have_Null_VisualParent() - { - using (CreateServices()) - { - var target = new Popup(); - - target.Open(); - - Assert.Null(target.PopupRoot.GetVisualParent()); + Assert.Null(((Visual)target.Host)); } } @@ -159,12 +148,12 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); + var target = new Popup() {PlacementTarget = PreparedWindow()}; target.Open(); - Assert.Equal(target, target.PopupRoot.Parent); - Assert.Equal(target, target.PopupRoot.GetLogicalParent()); + Assert.Equal(target, ((Visual)target.Host).Parent); + Assert.Equal(target, ((Visual)target.Host).GetLogicalParent()); } } @@ -173,15 +162,15 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); - var root = new TestRoot { Child = target }; + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; + var root = PreparedWindow(target); target.Open(); - var popupRoot = (ILogical)target.PopupRoot; + var popupRoot = (ILogical)((Visual)target.Host); Assert.True(popupRoot.IsAttachedToLogicalTree); - root.Child = null; + root.Content = null; Assert.False(((ILogical)target).IsAttachedToLogicalTree); } } @@ -191,8 +180,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = new Window(); - var target = new Popup(); + var window = PreparedWindow(); + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; @@ -214,10 +203,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = new Window(); - var target = new Popup(); + var window = PreparedWindow(); + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; + window.ApplyTemplate(); target.Open(); int closedCount = 0; @@ -233,46 +223,28 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - [Fact] - public void PopupRoot_Should_Have_Template_Applied() - { - using (CreateServices()) - { - var window = new Window(); - var target = new Popup(); - var child = new Control(); - - window.Content = target; - target.Open(); - - Assert.Single(target.PopupRoot.GetVisualChildren()); - - var templatedChild = target.PopupRoot.GetVisualChildren().Single(); - Assert.IsType(templatedChild); - Assert.Equal(target.PopupRoot, ((IControl)templatedChild).TemplatedParent); - } - } - + [Fact] public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() { using (CreateServices()) { PopupContentControl target; - var root = new TestRoot + var root = PreparedWindow(target = new PopupContentControl { - Child = target = new PopupContentControl - { - Content = new Border(), - Template = new FuncControlTemplate(PopupContentControlTemplate), - }, - StylingParent = AvaloniaLocator.Current.GetService() - }; + Content = new Border(), + Template = new FuncControlTemplate(PopupContentControlTemplate), + }); + root.Show(); target.ApplyTemplate(); + var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); popup.Open(); - var popupRoot = popup.PopupRoot; + + var popupRoot = (Control)popup.Host; + popupRoot.Measure(Size.Infinity); + popupRoot.Arrange(new Rect(popupRoot.DesiredSize)); var children = popupRoot.GetVisualDescendants().ToList(); var types = children.Select(x => x.GetType().Name).ToList(); @@ -280,6 +252,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new[] { + "VisualLayerManager", "ContentPresenter", "ContentPresenter", "Border", @@ -293,6 +266,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new object[] { + popupRoot, popupRoot, target, null, @@ -301,6 +275,13 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + Window PreparedWindow(object content = null) + { + var w = new Window {Content = content}; + w.ApplyTemplate(); + return w; + } + [Fact] public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() { @@ -311,6 +292,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = child = new TestControl(), DataContext = "foo", + PlacementTarget = PreparedWindow() }; var beginCalled = false; @@ -330,46 +312,32 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(beginCalled); } } - - - private static IDisposable CreateServices() + + [Fact] + public void Popup_Host_Type_Should_Match_Platform_Preference() { - var result = AvaloniaLocator.EnterScope(); - - var styles = new Styles + using (CreateServices()) { - new Style(x => x.OfType()) - { - Setters = new[] - { - new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate(PopupRootTemplate)), - } - }, - }; - - var globalStyles = new Mock(); - globalStyles.Setup(x => x.IsStylesInitialized).Returns(true); - globalStyles.Setup(x => x.Styles).Returns(styles); - - var renderInterface = new Mock(); - - AvaloniaLocator.CurrentMutable - .Bind().ToFunc(() => globalStyles.Object) - .Bind().ToConstant(new WindowingPlatformMock()) - .Bind().ToTransient() - .Bind().ToFunc(() => renderInterface.Object) - .Bind().ToConstant(new InputManager()); - - return result; + var target = new Popup() {PlacementTarget = PreparedWindow()}; + + target.Open(); + if (UsePopupHost) + Assert.IsType(target.Host); + else + Assert.IsType(target.Host); + } } - private static IControl PopupRootTemplate(PopupRoot control, INameScope scope) + private IDisposable CreateServices() { - return new ContentPresenter - { - Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - }.RegisterInNameScope(scope); + return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: + new MockWindowingPlatform(null, + () => + { + if(UsePopupHost) + return null; + return MockWindowingPlatform.CreatePopupMock().Object; + }))); } private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope) @@ -377,6 +345,7 @@ namespace Avalonia.Controls.UnitTests.Primitives return new Popup { Name = "popup", + PlacementTarget = control, Child = new ContentPresenter { [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], @@ -401,4 +370,12 @@ namespace Avalonia.Controls.UnitTests.Primitives } } } + + public class PopupTestsWithPopupRoot : PopupTests + { + public PopupTestsWithPopupRoot() + { + UsePopupHost = true; + } + } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 3ee6a50e69..55e8ae0115 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -20,33 +20,6 @@ namespace Avalonia.Controls.UnitTests { public class WindowBaseTests { - [Fact] - public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var impl = Mock.Of(x => x.Scaling == 1); - - Mock.Get(impl).Setup(x => x.Resize(It.IsAny())).Callback(() => { }); - - var target = new TestWindowBase(impl) - { - Template = CreateTemplate(), - Content = new TextBlock - { - Width = 321, - Height = 432, - }, - IsVisible = true, - }; - - target.LayoutManager.ExecuteInitialLayoutPass(target); - - Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432))); - } - } - - [Fact] public void Activate_Should_Call_Impl_Activate() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f4d9a91d0c..75239f014f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -277,8 +277,7 @@ namespace Avalonia.Controls.UnitTests var screens = new Mock(); screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object }); - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Position); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); windowImpl.Setup(x => x.Scaling).Returns(1); windowImpl.Setup(x => x.Screen).Returns(screens.Object); @@ -302,14 +301,12 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner() { - var parentWindowImpl = new Mock(); - parentWindowImpl.SetupProperty(x => x.Position); + var parentWindowImpl = MockWindowingPlatform.CreateWindowMock(); parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); parentWindowImpl.Setup(x => x.Scaling).Returns(1); - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Position); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); windowImpl.Setup(x => x.Scaling).Returns(1); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index 93cad9a68e..c3bc649abb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Styling; using Avalonia.UnitTests; @@ -59,11 +60,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions new Setter( Window.TemplateProperty, new FuncControlTemplate((x, scope) => - new ContentPresenter + new VisualLayerManager { - Name = "PART_ContentPresenter", - [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], - }.RegisterInNameScope(scope))) + Child = + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], + }.RegisterInNameScope(scope) + })) } }; } diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index ae901ca2f2..272b1fc489 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -5,6 +5,7 @@ false Library false + latest diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 36297bf58b..c33ec72141 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Moq; using Avalonia.Platform; @@ -15,16 +17,48 @@ namespace Avalonia.UnitTests _popupImpl = popupImpl; } + public static Mock CreateWindowMock(Func popupImpl = null) + { + var win = Mock.Of(x => x.Scaling == 1); + var mock = Mock.Get(win); + mock.Setup(x => x.CreatePopup()).Returns(() => + { + if (popupImpl != null) + return popupImpl(); + return CreatePopupMock().Object; + + }); + PixelPoint pos = default; + mock.SetupGet(x => x.Position).Returns(() => pos); + mock.Setup(x => x.Move(It.IsAny())).Callback(new Action(np => pos = np)); + SetupToplevel(mock); + return mock; + } + + static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl + { + mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); + } + + public static Mock CreatePopupMock() + { + var positioner = Mock.Of(); + var popup = Mock.Of(x => x.Scaling == 1); + var mock = Mock.Get(popup); + mock.SetupGet(x => x.PopupPositioner).Returns(positioner); + SetupToplevel(mock); + + return mock; + } + public IWindowImpl CreateWindow() { - return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); + return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object; } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } - - public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } -} \ No newline at end of file +}