From 9343ba4c23f993b023106d7513c2e80d20afda66 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 27 Jul 2019 13:05:19 +0300 Subject: [PATCH] Wired up the popup positioner Tests are failing because they are trying create popups out of a thin air --- src/Avalonia.Controls/PlacementMode.cs | 19 ++- src/Avalonia.Controls/Platform/IPopupImpl.cs | 4 +- .../Platform/IWindowBaseImpl.cs | 24 +--- src/Avalonia.Controls/Platform/IWindowImpl.cs | 27 +++++ src/Avalonia.Controls/Primitives/Popup.cs | 60 +--------- src/Avalonia.Controls/Primitives/PopupRoot.cs | 111 +++++++++++++----- src/Avalonia.Controls/ToolTip.cs | 6 +- src/Avalonia.Controls/Window.cs | 45 +++++++ src/Avalonia.Controls/WindowBase.cs | 49 +------- .../Remote/PreviewerWindowImpl.cs | 5 + .../Remote/PreviewerWindowingPlatform.cs | 2 - src/Avalonia.DesignerSupport/Remote/Stubs.cs | 23 +++- src/Avalonia.Native/PopupImpl.cs | 16 ++- src/Avalonia.Native/WindowImpl.cs | 4 +- src/Avalonia.X11/X11Window.cs | 34 +++++- src/Windows/Avalonia.Win32/PopupImpl.cs | 15 +++ src/Windows/Avalonia.Win32/WindowImpl.cs | 4 +- .../WindowBaseTests.cs | 27 ----- .../WindowTests.cs | 25 ++++ 19 files changed, 304 insertions(+), 196 deletions(-) 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/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/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/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 895094eded..f9ec9796fb 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -42,7 +42,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. @@ -147,10 +147,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); @@ -241,8 +238,9 @@ namespace Avalonia.Controls.Primitives ((ISetLogicalParent)_popupRoot).SetParent(this); } - _popupRoot.Position = GetPosition(); - + _popupRoot.ConfigurePosition(PlacementTarget ?? this.GetVisualParent(), + PlacementMode, new Point(HorizontalOffset, VerticalOffset)); + var window = _topLevel as Window; if (window != null) { @@ -263,11 +261,6 @@ namespace Avalonia.Controls.Primitives _popupRoot.Show(); - if (ObeyScreenEdges) - { - _popupRoot.SnapInsideScreenEdges(); - } - using (BeginIgnoringIsOpen()) { IsOpen = true; @@ -379,49 +372,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; diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 47863932d1..efe4d09b3d 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Controls.Platform; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform; @@ -18,7 +19,9 @@ namespace Avalonia.Controls.Primitives /// public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost { + private readonly TopLevel _parent; private IDisposable _presenterSubscription; + private PopupPositionerParameters _positionerParameters; /// /// Initializes static members of the class. @@ -45,6 +48,7 @@ namespace Avalonia.Controls.Primitives public PopupRoot(TopLevel parent, IAvaloniaDependencyResolver dependencyResolver) : base(parent.PlatformImpl.CreatePopup(), dependencyResolver) { + _parent = parent; } /// @@ -74,33 +78,6 @@ 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() - { - 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)); - } - } - } - /// protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { @@ -142,5 +119,85 @@ namespace Avalonia.Controls.Primitives } } } + + void UpdatePosition() + { + PlatformImpl?.PopupPositioner.Update(_positionerParameters); + } + + public void ConfigurePosition(Control target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, + PopupPositioningEdge gravity = PopupPositioningEdge.None) + { + // We need a better way for tracking the last pointer position + var pointer = _parent.PointToClient(_parent.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(_parent); + if (matrix == null) + 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"); + } + + if (_positionerParameters.Size != default) + UpdatePosition(); + } + + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. + protected override Size ArrangeOverride(Size finalSize) + { + using (BeginAutoSizing()) + { + _positionerParameters.Size = finalSize; + UpdatePosition(); + } + + return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); + } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 8c23f4abdc..da537a2e65 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -237,10 +237,10 @@ namespace Avalonia.Controls _popup = new PopupRoot((TopLevel)control.GetVisualRoot()) {Content = 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() 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..53e43e4ec4 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; } @@ -193,6 +175,11 @@ namespace Avalonia.Controls } } + protected internal virtual void OnBeforeShow() + { + + } + /// /// Begins an auto-resize operation. /// @@ -208,21 +195,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 +290,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 ddb8b62b6a..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,12 +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() => null; + 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() { @@ -79,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) @@ -112,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/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 976208b058..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; @@ -11,7 +12,9 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; - public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) + public PopupImpl(IAvaloniaNativeFactory factory, + AvaloniaNativePlatformOptions opts, + IWindowBaseImpl parent) : base(opts) { _factory = factory; _opts = opts; @@ -19,6 +22,14 @@ namespace Avalonia.Native { 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 @@ -40,6 +51,7 @@ namespace Avalonia.Native } } - public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts); + 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 c7857898d2..e4c158eeb3 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -104,6 +104,8 @@ namespace Avalonia.Native } public Func Closing { get; set; } - public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts); + public void Move(PixelPoint point) => Position = point; + + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this); } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index a1e386892b..7ca2672d2b 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; @@ -39,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 @@ -151,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 @@ -454,13 +458,20 @@ 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( @@ -469,7 +480,8 @@ namespace Avalonia.X11 Scaling = newScaling; ScalingChanged?.Invoke(Scaling); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); - Resize(oldScaledSize, true); + if(!skipResize) + Resize(oldScaledSize, true); return true; } @@ -731,6 +743,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)); @@ -939,6 +959,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/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/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2f7805884d..21625af84a 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; @@ -250,7 +252,7 @@ namespace Avalonia.Win32 public IPopupImpl CreatePopup() { - return new PopupImpl(); + return new PopupImpl(this); } public void Dispose() 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..cbcf08049e 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -17,6 +17,31 @@ namespace Avalonia.Controls.UnitTests { public class WindowTests { + [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 Window(impl) + { + 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 Setting_Title_Should_Set_Impl_Title() {