Browse Source

Merge pull request #2778 from AvaloniaUI/popup-ng

Popup positioning rework
pull/2828/head
Nikita Tsukanov 7 years ago
committed by GitHub
parent
commit
1a6e21518c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/Android/Avalonia.Android/AndroidPlatform.cs
  2. 112
      src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs
  3. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  4. 2
      src/Avalonia.Controls/ComboBox.cs
  5. 2
      src/Avalonia.Controls/ContextMenu.cs
  6. 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  7. 2
      src/Avalonia.Controls/MenuItem.cs
  8. 2
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  9. 19
      src/Avalonia.Controls/PlacementMode.cs
  10. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  11. 4
      src/Avalonia.Controls/Platform/IPopupImpl.cs
  12. 2
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  13. 24
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  14. 27
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  15. 1
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  16. 5
      src/Avalonia.Controls/Platform/PlatformManager.cs
  17. 1
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  18. 42
      src/Avalonia.Controls/Primitives/AdornerDecorator.cs
  19. 2
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  20. 26
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  21. 38
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  22. 149
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  23. 284
      src/Avalonia.Controls/Primitives/Popup.cs
  24. 358
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  25. 175
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  26. 50
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  27. 119
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  28. 93
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  29. 14
      src/Avalonia.Controls/ToolTip.cs
  30. 45
      src/Avalonia.Controls/Window.cs
  31. 44
      src/Avalonia.Controls/WindowBase.cs
  32. 5
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  33. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  34. 23
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  35. 5
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  36. 1
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  37. 20
      src/Avalonia.Native/PopupImpl.cs
  38. 8
      src/Avalonia.Native/WindowImpl.cs
  39. 3
      src/Avalonia.Native/WindowImplBase.cs
  40. 8
      src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
  41. 18
      src/Avalonia.Themes.Default/ComboBox.xaml
  42. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  43. 6
      src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml
  44. 14
      src/Avalonia.Themes.Default/OverlayPopupHost.xaml
  45. 14
      src/Avalonia.Themes.Default/PopupRoot.xaml
  46. 4
      src/Avalonia.Themes.Default/Window.xaml
  47. 55
      src/Avalonia.Visuals/Media/PixelPoint.cs
  48. 10
      src/Avalonia.Visuals/Media/PixelRect.cs
  49. 203
      src/Avalonia.Visuals/Media/PixelVector.cs
  50. 8
      src/Avalonia.X11/X11Platform.cs
  51. 47
      src/Avalonia.X11/X11Window.cs
  52. 2
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  53. 89
      src/Skia/Avalonia.Skia/GlRenderTarget.cs
  54. 2
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  55. 15
      src/Windows/Avalonia.Win32/PopupImpl.cs
  56. 7
      src/Windows/Avalonia.Win32/Win32Platform.cs
  57. 7
      src/Windows/Avalonia.Win32/WindowImpl.cs
  58. 2
      src/iOS/Avalonia.iOS/TopLevelImpl.cs
  59. 5
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  60. 28
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  61. 31
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
  62. 66
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  63. 155
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  64. 27
      tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
  65. 9
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  66. 13
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
  67. 1
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  68. 42
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

5
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -71,10 +71,5 @@ namespace Avalonia.Android
{
throw new NotSupportedException();
}
public IPopupImpl CreatePopup()
{
return new PopupImpl();
}
}
}

112
src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs

@ -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<PixelPoint> 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<IWindowManager>();
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
}
}
}

2
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);
}
}

2
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))
{

2
src/Avalonia.Controls/ContextMenu.cs

@ -91,6 +91,8 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (IsOpen)
{
return;

1
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;
}
}

2
src/Avalonia.Controls/MenuItem.cs

@ -224,7 +224,7 @@ namespace Avalonia.Controls
public bool IsTopLevel => Parent is Menu;
/// <inheritdoc/>
bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false;
bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
/// <inheritdoc/>
IMenuElement IMenuItem.Parent => Parent as IMenuElement;

2
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -150,7 +150,7 @@ namespace Avalonia.Controls.Notifications
private void Install(Window host)
{
var adornerLayer = host.GetVisualDescendants()
.OfType<AdornerDecorator>()
.OfType<VisualLayerManager>()
.FirstOrDefault()
?.AdornerLayer;

19
src/Avalonia.Controls/PlacementMode.cs

@ -23,6 +23,21 @@ namespace Avalonia.Controls
/// <summary>
/// The popup is placed at the top right of its target.
/// </summary>
Right
Right,
/// <summary>
/// The popup is placed at the top left of its target.
/// </summary>
Left,
/// <summary>
/// The popup is placed at the top left of its target.
/// </summary>
Top,
/// <summary>
/// The popup is placed according to anchor and gravity rules
/// </summary>
AnchorAndGravity
}
}
}

2
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)

4
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
{
/// <summary>
@ -8,6 +10,6 @@ namespace Avalonia.Platform
/// </summary>
public interface IPopupImpl : IWindowBaseImpl
{
IPopupPositioner PopupPositioner { get; }
}
}

2
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -107,5 +107,7 @@ namespace Avalonia.Platform
/// </summary>
[CanBeNull]
IMouseDevice MouseDevice { get; }
IPopupImpl CreatePopup();
}
}

24
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@ -15,21 +15,10 @@ namespace Avalonia.Platform
/// </summary>
void Hide();
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
/// </summary>
void BeginMoveDrag();
/// <summary>
/// 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
/// </summary>
void BeginResizeDrag(WindowEdge edge);
/// <summary>
/// Gets the position of the window in device pixels.
/// </summary>
PixelPoint Position { get; set; }
PixelPoint Position { get; }
/// <summary>
/// Gets or sets a method called when the window's position changes.
@ -61,17 +50,6 @@ namespace Avalonia.Platform
/// </summary>
Size MaxClientSize { get; }
/// <summary>
/// Sets the client size of the top level.
/// </summary>
void Resize(Size clientSize);
/// <summary>
/// Minimum width of the window.
/// </summary>
///
void SetMinMaxSize(Size minSize, Size maxSize);
/// <summary>
/// Sets whether this window appears on top of all other windows
/// </summary>

27
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -57,5 +57,32 @@ namespace Avalonia.Platform
/// Return true to prevent the underlying implementation from closing.
/// </summary>
Func<bool> Closing { get; set; }
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
/// </summary>
void BeginMoveDrag();
/// <summary>
/// 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
/// </summary>
void BeginResizeDrag(WindowEdge edge);
/// <summary>
/// Sets the client size of the top level.
/// </summary>
void Resize(Size clientSize);
/// <summary>
/// Sets the client size of the top level.
/// </summary>
void Move(PixelPoint point);
/// <summary>
/// Minimum width of the window.
/// </summary>
///
void SetMinMaxSize(Size minSize, Size maxSize);
}
}

1
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

@ -4,6 +4,5 @@ namespace Avalonia.Platform
{
IWindowImpl CreateWindow();
IEmbeddableWindowImpl CreateEmbeddableWindow();
IPopupImpl CreatePopup();
}
}

5
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<IWindowingPlatform>().CreatePopup();
}
}
}

1
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)

42
src/Avalonia.Controls/Primitives/AdornerDecorator.cs

@ -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);
}
}
}

2
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives
public static AdornerLayer GetAdornerLayer(IVisual visual)
{
return visual.GetVisualAncestors()
.OfType<AdornerDecorator>()
.OfType<VisualLayerManager>()
.FirstOrDefault()
?.AdornerLayer;
}

26
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<TemplateAppliedEventArgs> 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<double> widthProperty,
StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty,
StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty);
}
}

38
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<VisualLayerManager>().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);
}
}
}

149
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;
/// <inheritdoc/>
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<double> widthProperty, StyledProperty<double> minWidthProperty,
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
{
// Topmost property is not supported
var bindings = new List<IDisposable>();
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<ManagedPopupPositionerScreenInfo> 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));
}
}
}

284
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 <see cref="ObeyScreenEdges"/> property.
/// </summary>
public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges));
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
@ -75,10 +80,12 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, bool>(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<IDisposable> _bindings = new List<IDisposable>();
/// <summary>
/// Initializes static members of the <see cref="Popup"/> class.
@ -88,7 +95,11 @@ namespace Avalonia.Controls.Primitives
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
ChildProperty.Changed.AddClassHandler<Popup>(x => x.ChildChanged);
IsOpenProperty.Changed.AddClassHandler<Popup>(x => x.IsOpenChanged);
TopmostProperty.Changed.AddClassHandler<Popup>((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue);
}
public Popup()
{
}
/// <summary>
@ -101,10 +112,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public event EventHandler Opened;
/// <summary>
/// Raised when the popup root has been created, but before it has been shown.
/// </summary>
public event EventHandler PopupRootCreated;
public IPopupHost Host => _popupHost;
/// <summary>
/// Gets or sets the control to display in the popup.
@ -147,10 +155,7 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// 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.
/// </summary>
[Obsolete("This property has no effect")]
public bool ObeyScreenEdges
{
get => GetValue(ObeyScreenEdgesProperty);
@ -184,11 +189,6 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementTargetProperty, value); }
}
/// <summary>
/// Gets the root of the popup window.
/// </summary>
public PopupRoot PopupRoot => _popupRoot;
/// <summary>
/// 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
/// <summary>
/// Gets the root of the popup window.
/// </summary>
IVisual IVisualTreeHost.Root => _popupRoot;
IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot;
/// <summary>
/// Opens the popup.
/// </summary>
public void Open()
{
if (_popupRoot == null)
// Popup is currently open
if (_topLevel != null)
return;
CloseCurrent();
var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().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
/// </summary>
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;
}
}
/// <summary>
/// Measures the control.
/// </summary>
@ -323,27 +338,14 @@ namespace Avalonia.Controls.Primitives
return new Size();
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_topLevel = e.Root as TopLevel;
}
/// <inheritdoc/>
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
_topLevel = null;
if (_popupRoot != null)
{
((ISetLogicalParent)_popupRoot).SetParent(null);
_popupRoot.Dispose();
_popupRoot = null;
}
Close();
}
/// <summary>
/// Called when the <see cref="IsOpen"/> property changes.
/// </summary>
@ -380,49 +382,6 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Gets the position for the popup based on the placement properties.
/// </summary>
/// <returns>The popup's position in screen coordinates.</returns>
protected virtual PixelPoint GetPosition()
{
var result = GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), 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)

358
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
{
/// <summary>
///
/// 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.
/// </summary>
public struct PopupPositionerParameters
{
private PopupPositioningEdge _gravity;
private PopupPositioningEdge _anchor;
/// <summary>
/// Set the size of the popup that is to be positioned with the positioner
/// object. The size is in scaled coordinates.
/// </summary>
public Size Size { get; set; }
/// <summary>
/// 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
/// </summary>
public Rect AnchorRectangle { get; set; }
/// <summary>
/// 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.
/// </summary>
public PopupPositioningEdge Anchor
{
get => _anchor;
set
{
PopupPositioningEdgeHelper.ValidateEdge(value);
_anchor = value;
}
}
/// <summary>
/// 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.
/// </summary>
public PopupPositioningEdge Gravity
{
get => _gravity;
set
{
PopupPositioningEdgeHelper.ValidateEdge(value);
_gravity = value;
}
}
/// <summary>
/// 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.
/// </summary>
public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
/// <summary>
/// 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.
/// </summary>
public Point Offset { get; set; }
}
/// <summary>
/// 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.
/// </summary>
[Flags]
public enum PopupPositionerConstraintAdjustment
{
/// <summary>
/// Don't alter the surface position even if it is constrained on some
/// axis, for example partially outside the edge of an output.
/// </summary>
None = 0,
/// <summary>
/// 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.
/// </summary>
SlideX = 1,
/// <summary>
/// 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.
/// */
/// </summary>
SlideY = 2,
/// <summary>
/// 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.
/// </summary>
FlipX = 4,
/// <summary>
/// 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.
/// </summary>
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");
}
}
}
}

175
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<ManagedPopupPositionerScreenInfo> 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);
}
}
}

50
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
{
/// <summary>
/// This class is used to simplify integration of IPopupImpl implementations with popup positioner
/// </summary>
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<ManagedPopupPositionerScreenInfo> 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;
}
}

119
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
/// <summary>
/// The root window of a <see cref="Popup"/>.
/// </summary>
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;
/// <summary>
/// Initializes static members of the <see cref="PopupRoot"/> class.
@ -31,8 +33,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Initializes a new instance of the <see cref="PopupRoot"/> class.
/// </summary>
public PopupRoot()
: this(null)
public PopupRoot(TopLevel parent, IPopupImpl impl)
: this(parent, impl,null)
{
}
@ -42,9 +44,10 @@ namespace Avalonia.Controls.Primitives
/// <param name="dependencyResolver">
/// The dependency resolver to use. If null the default dependency resolver will be used.
/// </param>
public PopupRoot(IAvaloniaDependencyResolver dependencyResolver)
: base(PlatformManager.CreatePopup(), dependencyResolver)
public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver)
: base(impl, dependencyResolver)
{
_parent = parent;
}
/// <summary>
@ -74,73 +77,61 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/>
public void Dispose() => PlatformImpl?.Dispose();
/// <summary>
/// Moves the Popups position so that it doesnt overlap screen edges.
/// This method can be called immediately after Show has been called.
/// </summary>
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);
}
/// <inheritdoc/>
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<double> widthProperty, StyledProperty<double> minWidthProperty,
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
{
var bindings = new List<IDisposable>();
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)
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
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));
}
}
}

93
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<Control> _layers = new List<Control>();
public bool IsPopup { get; set; }
public AdornerLayer AdornerLayer
{
get
{
var rv = FindLayer<AdornerLayer>();
if (rv == null)
AddLayer(rv = new AdornerLayer(), AdornerZIndex);
return rv;
}
}
public OverlayLayer OverlayLayer
{
get
{
if (IsPopup)
return null;
var rv = FindLayer<OverlayLayer>();
if(rv == null)
AddLayer(rv = new OverlayLayer(), OverlayZIndex);
return rv;
}
}
T FindLayer<T>() 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);
}
}
}

14
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<ToolTip> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip>("ToolTip");
private PopupRoot _popup;
private IPopupHost _popup;
/// <summary>
/// Initializes static members of the <see cref="ToolTip"/> 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;
}

45
src/Avalonia.Controls/Window.cs

@ -135,6 +135,12 @@ namespace Avalonia.Controls
WindowStateProperty.Changed.AddClassHandler<Window>(
(w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; });
MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
MaxHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
}
/// <summary>
@ -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));
}
/// <summary>
@ -239,6 +246,44 @@ namespace Avalonia.Controls
set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); }
}
/// <summary>
/// Gets or sets the window position in screen coordinates.
/// </summary>
public PixelPoint Position
{
get { return PlatformImpl?.Position ?? PixelPoint.Origin; }
set
{
PlatformImpl?.Move(value);
}
}
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler
/// </summary>
public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag();
/// <summary>
/// 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
/// </summary>
public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge);
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(finalSize);
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;

44
src/Avalonia.Controls/WindowBase.cs

@ -49,10 +49,6 @@ namespace Avalonia.Controls
IsVisibleProperty.OverrideDefaultValue<WindowBase>(false);
IsVisibleProperty.Changed.AddClassHandler<WindowBase>(x => x.IsVisibleChanged);
MinWidthProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
MinHeightProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
MaxWidthProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
MaxHeightProperty.Changed.AddClassHandler<WindowBase>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
TopmostProperty.Changed.AddClassHandler<WindowBase>((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));
}
/// <summary>
@ -96,19 +91,6 @@ namespace Avalonia.Controls
get { return _isActive; }
private set { SetAndRaise(IsActiveProperty, ref _isActive, value); }
}
/// <summary>
/// Gets or sets the window position in screen coordinates.
/// </summary>
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);
}
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(finalSize);
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
/// <summary>
/// Ensures that the window is initialized.
/// </summary>
@ -318,16 +285,5 @@ namespace Avalonia.Controls
}
}
}
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler
/// </summary>
public void BeginMoveDrag() => PlatformImpl?.BeginMoveDrag();
/// <summary>
/// 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
/// </summary>
public void BeginResizeDrag(WindowEdge edge) => PlatformImpl?.BeginResizeDrag(edge);
}
}

5
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)
{
}

2
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;

23
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<bool> 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<PixelPoint> PositionChanged { get; set; }
public WindowState WindowState { get; set; }
public Action<WindowState> 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

5
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

1
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; }
}

20
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; }
}
}

8
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<bool> Closing { get; set; }
public void Move(PixelPoint point) => Position = point;
public override IPopupImpl CreatePopup() =>
_opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, this);
}
}

3
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<Size> Resized { get; set; }
public Action Closed { get; set; }
public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice;
public abstract IPopupImpl CreatePopup();
class FramebufferWrapper : ILockedFramebuffer

8
src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj

@ -12,11 +12,11 @@
<ProjectReference Include="..\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
<AvaloniaResource Include="DefaultTheme.xaml"/>
<AvaloniaResource Include="Accents/*.xaml"/>
<AvaloniaResource Include="DefaultTheme.xaml" />
<AvaloniaResource Include="Accents/*.xaml" />
<!-- Compatibility with old apps, probably need to replace with AvaloniaResource -->
<EmbeddedResource Include="**/*.xaml"/>
<EmbeddedResource Include="**/*.xaml" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets"/>
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\Rx.props" />
</Project>

18
src/Avalonia.Themes.Default/ComboBox.xaml

@ -40,16 +40,14 @@
StaysOpen="False">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<AdornerDecorator Margin="-1 -1 0 0">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"
/>
</ScrollViewer>
</AdornerDecorator>
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"
/>
</ScrollViewer>
</Border>
</Popup>
</Grid>

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -19,6 +19,7 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.Menu.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ContextMenu.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.MenuItem.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.OverlayPopupHost.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.PopupRoot.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ProgressBar.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.RadioButton.xaml?assembly=Avalonia.Themes.Default"/>

6
src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml

@ -4,13 +4,13 @@
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<AdornerDecorator>
<VisualLayerManager>
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"/>
</AdornerDecorator>
</VisualLayerManager>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>

14
src/Avalonia.Themes.Default/OverlayPopupHost.xaml

@ -0,0 +1,14 @@
<Style xmlns="https://github.com/avaloniaui" Selector="OverlayPopupHost">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="Template">
<ControlTemplate>
<VisualLayerManager IsPopup="True" Margin="-1 -1 0 0">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</ControlTemplate>
</Setter>
</Style>

14
src/Avalonia.Themes.Default/PopupRoot.xaml

@ -2,11 +2,13 @@
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
<VisualLayerManager IsPopup="True" Margin="-1 -1 0 0">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</ControlTemplate>
</Setter>
</Style>
</Style>

4
src/Avalonia.Themes.Default/Window.xaml

@ -5,14 +5,14 @@
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<AdornerDecorator>
<VisualLayerManager>
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</AdornerDecorator>
</VisualLayerManager>
</Border>
</ControlTemplate>
</Setter>

55
src/Avalonia.Visuals/Media/PixelPoint.cs

@ -59,6 +59,59 @@ namespace Avalonia
{
return !(left == right);
}
/// <summary>
/// Converts the <see cref="Point"/> to a <see cref="Vector"/>.
/// </summary>
/// <param name="p">The point.</param>
public static implicit operator PixelVector(PixelPoint p)
{
return new PixelVector(p.X, p.Y);
}
/// <summary>
/// Adds two points.
/// </summary>
/// <param name="a">The first point.</param>
/// <param name="b">The second point.</param>
/// <returns>A point that is the result of the addition.</returns>
public static PixelPoint operator +(PixelPoint a, PixelPoint b)
{
return new PixelPoint(a.X + b.X, a.Y + b.Y);
}
/// <summary>
/// Adds a vector to a point.
/// </summary>
/// <param name="a">The point.</param>
/// <param name="b">The vector.</param>
/// <returns>A point that is the result of the addition.</returns>
public static PixelPoint operator +(PixelPoint a, PixelVector b)
{
return new PixelPoint(a.X + b.X, a.Y + b.Y);
}
/// <summary>
/// Subtracts two points.
/// </summary>
/// <param name="a">The first point.</param>
/// <param name="b">The second point.</param>
/// <returns>A point that is the result of the subtraction.</returns>
public static PixelPoint operator -(PixelPoint a, PixelPoint b)
{
return new PixelPoint(a.X - b.X, a.Y - b.Y);
}
/// <summary>
/// Subtracts a vector from a point.
/// </summary>
/// <param name="a">The point.</param>
/// <param name="b">The vector.</param>
/// <returns>A point that is the result of the subtraction.</returns>
public static PixelPoint operator -(PixelPoint a, PixelVector b)
{
return new PixelPoint(a.X - b.X, a.Y - b.Y);
}
/// <summary>
/// Parses a <see cref="PixelPoint"/> string.
@ -106,7 +159,7 @@ namespace Avalonia
return hash;
}
}
/// <summary>
/// Returns a new <see cref="PixelPoint"/> with the same Y co-ordinate and the specified X co-ordinate.
/// </summary>

10
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);
}
/// <summary>
/// Translates the rectangle by an offset.
/// </summary>
/// <param name="offset">The offset.</param>
/// <returns>The translated rectangle.</returns>
public PixelRect Translate(PixelVector offset)
{
return new PixelRect(Position + offset, Size);
}
/// <summary>
/// Gets the union of two rectangles.

203
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
{
/// <summary>
/// Defines a vector.
/// </summary>
public readonly struct PixelVector
{
/// <summary>
/// The X vector.
/// </summary>
private readonly int _x;
/// <summary>
/// The Y vector.
/// </summary>
private readonly int _y;
/// <summary>
/// Initializes a new instance of the <see cref="PixelVector"/> structure.
/// </summary>
/// <param name="x">The X vector.</param>
/// <param name="y">The Y vector.</param>
public PixelVector(int x, int y)
{
_x = x;
_y = y;
}
/// <summary>
/// Gets the X vector.
/// </summary>
public int X => _x;
/// <summary>
/// Gets the Y vector.
/// </summary>
public int Y => _y;
/// <summary>
/// Converts the <see cref="PixelVector"/> to a <see cref="PixelPoint"/>.
/// </summary>
/// <param name="a">The vector.</param>
public static explicit operator PixelPoint(PixelVector a)
{
return new PixelPoint(a._x, a._y);
}
/// <summary>
/// Calculates the dot product of two vectors
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>The dot product</returns>
public static int operator *(PixelVector a, PixelVector b)
{
return a.X * b.X + a.Y * b.Y;
}
/// <summary>
/// Scales a vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scale">The scaling factor.</param>
/// <returns>The scaled vector.</returns>
public static PixelVector operator *(PixelVector vector, int scale)
{
return new PixelVector(vector._x * scale, vector._y * scale);
}
/// <summary>
/// Scales a vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scale">The divisor.</param>
/// <returns>The scaled vector.</returns>
public static PixelVector operator /(PixelVector vector, int scale)
{
return new PixelVector(vector._x / scale, vector._y / scale);
}
/// <summary>
/// Length of the vector
/// </summary>
public double Length => Math.Sqrt(X * X + Y * Y);
/// <summary>
/// Negates a vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The negated vector.</returns>
public static PixelVector operator -(PixelVector a)
{
return new PixelVector(-a._x, -a._y);
}
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the addition.</returns>
public static PixelVector operator +(PixelVector a, PixelVector b)
{
return new PixelVector(a._x + b._x, a._y + b._y);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the subtraction.</returns>
public static PixelVector operator -(PixelVector a, PixelVector b)
{
return new PixelVector(a._x - b._x, a._y - b._y);
}
/// <summary>
/// Check if two vectors are equal (bitwise).
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals(PixelVector other)
{
return _x == other._x && _y == other._y;
}
/// <summary>
/// Check if two vectors are nearly equal (numerically).
/// </summary>
/// <param name="other">The other vector.</param>
/// <returns>True if vectors are nearly equal.</returns>
[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);
}
/// <summary>
/// Returns the string representation of the point.
/// </summary>
/// <returns>The string representation of the point.</returns>
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y);
}
/// <summary>
/// Returns a new vector with the specified X coordinate.
/// </summary>
/// <param name="x">The X coordinate.</param>
/// <returns>The new vector.</returns>
public PixelVector WithX(int x)
{
return new PixelVector(x, _y);
}
/// <summary>
/// Returns a new vector with the specified Y coordinate.
/// </summary>
/// <param name="y">The Y coordinate.</param>
/// <returns>The new vector.</returns>
public PixelVector WithY(int y)
{
return new PixelVector(_x, y);
}
}
}

8
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<string> GlxRendererBlacklist { get; set; } = new List<string>
{

47
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<X11Window> _transientChildren = new HashSet<X11Window>();
private X11Window _transientParent;
private double? _scalingOverride;
public object SyncRoot { get; } = new object();
class InputEventContainer
@ -47,10 +50,10 @@ namespace Avalonia.X11
private readonly Queue<InputEventContainer> _inputQueue = new Queue<InputEventContainer>();
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; }
}
}

2
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<object> Surfaces => new object[] {_outputBackend};
public Action<RawInputEventArgs> Input { get; set; }

89
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();
}));
}
}
}

2
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;
}
}

15
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; }
}
}

7
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))

7
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()
{

2
src/iOS/Avalonia.iOS/TopLevelImpl.cs

@ -134,5 +134,7 @@ namespace Avalonia.iOS
}
public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this);
public IPopupImpl CreatePopup() => null;
}
}

5
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();

28
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<IWindowImpl>();
windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
popupImpl = new Mock<IPopupImpl>();
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,

31
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

66
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<VisualLayerManager>(templatedChild);
var contentPresenter = templatedChild.VisualChildren.Single();
Assert.IsType<ContentPresenter>(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<PopupRoot>((parent, scope) =>
new ContentPresenter
@ -158,6 +201,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
new Popup
{
[!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty],
PlacementTarget = parent
});
}

155
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<ContentPresenter>(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<PopupContentControl>(PopupContentControlTemplate),
},
StylingParent = AvaloniaLocator.Current.GetService<IGlobalStyles>()
};
Content = new Border(),
Template = new FuncControlTemplate<PopupContentControl>(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<PopupRoot>())
{
Setters = new[]
{
new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate<PopupRoot>(PopupRootTemplate)),
}
},
};
var globalStyles = new Mock<IGlobalStyles>();
globalStyles.Setup(x => x.IsStylesInitialized).Returns(true);
globalStyles.Setup(x => x.Styles).Returns(styles);
var renderInterface = new Mock<IPlatformRenderInterface>();
AvaloniaLocator.CurrentMutable
.Bind<IGlobalStyles>().ToFunc(() => globalStyles.Object)
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformMock())
.Bind<IStyler>().ToTransient<Styler>()
.Bind<IPlatformRenderInterface>().ToFunc(() => renderInterface.Object)
.Bind<IInputManager>().ToConstant(new InputManager());
return result;
var target = new Popup() {PlacementTarget = PreparedWindow()};
target.Open();
if (UsePopupHost)
Assert.IsType<OverlayPopupHost>(target.Host);
else
Assert.IsType<PopupRoot>(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;
}
}
}

27
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<IWindowBaseImpl>(x => x.Scaling == 1);
Mock.Get(impl).Setup(x => x.Resize(It.IsAny<Size>())).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()
{

9
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -277,8 +277,7 @@ namespace Avalonia.Controls.UnitTests
var screens = new Mock<IScreenImpl>();
screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object });
var windowImpl = new Mock<IWindowImpl>();
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<IWindowImpl>();
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<IWindowImpl>();
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);

13
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<Window>((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)
}))
}
};
}

1
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@ -5,6 +5,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<OutputType>Library</OutputType>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

42
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<IWindowImpl> CreateWindowMock(Func<IPopupImpl> popupImpl = null)
{
var win = Mock.Of<IWindowImpl>(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<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));
SetupToplevel(mock);
return mock;
}
static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
{
mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
}
public static Mock<IPopupImpl> CreatePopupMock()
{
var positioner = Mock.Of<IPopupPositioner>();
var popup = Mock.Of<IPopupImpl>(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<IWindowImpl>(x => x.Scaling == 1);
return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object;
}
public IEmbeddableWindowImpl CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of<IPopupImpl>(x => x.Scaling == 1);
}
}
}

Loading…
Cancel
Save