diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index de9ca02ed1..09d2612ac3 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
+using System.Globalization;
using System.Linq;
using System.Threading;
using Avalonia;
@@ -28,15 +29,24 @@ namespace ControlCatalog.NetCore
}
var builder = BuildAvaloniaApp();
+
+ double GetScaling()
+ {
+ var idx = Array.IndexOf(args, "--scaling");
+ if (idx != 0 && args.Length > idx + 1 &&
+ double.TryParse(args[idx + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out var scaling))
+ return scaling;
+ return 1;
+ }
if (args.Contains("--fbdev"))
{
SilenceConsole();
- return builder.StartLinuxFbDev(args);
+ return builder.StartLinuxFbDev(args, scaling: GetScaling());
}
else if (args.Contains("--drm"))
{
SilenceConsole();
- return builder.StartLinuxDrm(args);
+ return builder.StartLinuxDrm(args, scaling: GetScaling());
}
else
return builder.StartWithClassicDesktopLifetime(args);
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
index dfe8be2cec..d0631d2cbd 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
@@ -6,19 +6,20 @@
ItemsRepeater
A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization.
-
+
Stack - Vertical
Stack - Horizontal
UniformGrid - Vertical
UniformGrid - Horizontal
+
-
+
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
index 214de89253..1a607342f3 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
@@ -1,8 +1,11 @@
+using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
+using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@@ -16,7 +19,8 @@ namespace ControlCatalog.Pages
this.InitializeComponent();
_repeater = this.FindControl("repeater");
_scroller = this.FindControl("scroller");
- DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray();
+ _repeater.PointerPressed += RepeaterClick;
+ DataContext = new ItemsRepeaterPageViewModel();
}
private void InitializeComponent()
@@ -67,5 +71,11 @@ namespace ControlCatalog.Pages
break;
}
}
+
+ private void RepeaterClick(object sender, PointerPressedEventArgs e)
+ {
+ var item = (e.Source as TextBlock)?.DataContext as string;
+ ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
+ }
}
}
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml
index 4783c8cfb8..49e9aafc4a 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml
@@ -9,7 +9,20 @@
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
-
+
+
+
+
+
+
+
+
+ Single
+ Multiple
+ Toggle
+ AlwaysSelected
+
+
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
index dbe6c74800..8a67766c76 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
@@ -1,9 +1,9 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using ReactiveUI;
namespace ControlCatalog.Pages
{
@@ -11,9 +11,8 @@ namespace ControlCatalog.Pages
{
public ListBoxPage()
{
- this.InitializeComponent();
- DataContext = Enumerable.Range(1, 10).Select(i => $"Item {i}" )
- .ToArray();
+ InitializeComponent();
+ DataContext = new PageViewModel();
}
private void InitializeComponent()
@@ -21,5 +20,46 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this);
}
+ private class PageViewModel : ReactiveObject
+ {
+ private int _counter;
+ private SelectionMode _selectionMode;
+
+ public PageViewModel()
+ {
+ Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem()));
+ SelectedItems = new ObservableCollection();
+
+ AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
+
+ RemoveItemCommand = ReactiveCommand.Create(() =>
+ {
+ while (SelectedItems.Count > 0)
+ {
+ Items.Remove(SelectedItems[0]);
+ }
+ });
+ }
+
+ public ObservableCollection Items { get; }
+
+ public ObservableCollection SelectedItems { get; }
+
+ public ReactiveCommand AddItemCommand { get; }
+
+ public ReactiveCommand RemoveItemCommand { get; }
+
+ public SelectionMode SelectionMode
+ {
+ get => _selectionMode;
+ set
+ {
+ SelectedItems.Clear();
+ this.RaiseAndSetIfChanged(ref _selectionMode, value);
+ }
+ }
+
+ private string GenerateItem() => $"Item {_counter++}";
+ }
}
}
diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml
index c03edb8b03..3a81e2ed02 100644
--- a/samples/ControlCatalog/Pages/TreeViewPage.xaml
+++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml
@@ -6,16 +6,29 @@
Displays a hierachical tree of data.
-
-
-
-
-
-
-
+ Margin="0,16,0,0"
+ HorizontalAlignment="Center"
+ Spacing="16">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Single
+ Multiple
+ Toggle
+ AlwaysSelected
+
+
diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
index a83f9cf43f..1f35f05f1d 100644
--- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
@@ -1,8 +1,9 @@
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using ReactiveUI;
namespace ControlCatalog.Pages
{
@@ -10,8 +11,8 @@ namespace ControlCatalog.Pages
{
public TreeViewPage()
{
- this.InitializeComponent();
- DataContext = new Node().Children;
+ InitializeComponent();
+ DataContext = new PageViewModel();
}
private void InitializeComponent()
@@ -19,22 +20,96 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this);
}
- public class Node
+ private class PageViewModel : ReactiveObject
{
- private IList _children;
+ private SelectionMode _selectionMode;
+
+ public PageViewModel()
+ {
+ Node root = new Node();
+ Items = root.Children;
+ SelectedItems = new ObservableCollection();
+
+ AddItemCommand = ReactiveCommand.Create(() =>
+ {
+ Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root;
+ parentItem.AddNewItem();
+ });
+
+ RemoveItemCommand = ReactiveCommand.Create(() =>
+ {
+ while (SelectedItems.Count > 0)
+ {
+ Node lastItem = SelectedItems[0];
+ RecursiveRemove(Items, lastItem);
+ SelectedItems.Remove(lastItem);
+ }
+
+ bool RecursiveRemove(ObservableCollection items, Node selectedItem)
+ {
+ if (items.Remove(selectedItem))
+ {
+ return true;
+ }
+
+ foreach (Node item in items)
+ {
+ if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ });
+ }
+
+ public ObservableCollection Items { get; }
+
+ public ObservableCollection SelectedItems { get; }
+
+ public ReactiveCommand AddItemCommand { get; }
+
+ public ReactiveCommand RemoveItemCommand { get; }
+
+ public SelectionMode SelectionMode
+ {
+ get => _selectionMode;
+ set
+ {
+ SelectedItems.Clear();
+ this.RaiseAndSetIfChanged(ref _selectionMode, value);
+ }
+ }
+ }
+
+ private class Node
+ {
+ private int _counter;
+ private ObservableCollection _children;
+
public string Header { get; private set; }
- public IList Children
+
+ public bool AreChildrenInitialized => _children != null;
+
+ public ObservableCollection Children
{
get
{
if (_children == null)
{
- _children = Enumerable.Range(1, 10).Select(i => new Node() {Header = $"Item {i}"})
- .ToArray();
+ _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode()));
}
return _children;
}
}
+
+ public void AddNewItem() => Children.Add(CreateNewNode());
+
+ public override string ToString() => Header;
+
+ private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"};
}
}
}
diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
new file mode 100644
index 0000000000..436a479441
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
@@ -0,0 +1,27 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using ReactiveUI;
+
+namespace ControlCatalog.ViewModels
+{
+ public class ItemsRepeaterPageViewModel : ReactiveObject
+ {
+ private int newItemIndex = 1;
+
+ public ItemsRepeaterPageViewModel()
+ {
+ Items = new ObservableCollection(
+ Enumerable.Range(1, 100000).Select(i => $"Item {i}"));
+ }
+
+ public ObservableCollection Items { get; }
+
+ public string SelectedItem { get; set; }
+
+ public void AddItem()
+ {
+ var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
+ Items.Insert(index + 1, $"New Item {newItemIndex++}");
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index 4e48811c35..c91b58311b 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -71,10 +71,5 @@ namespace Avalonia.Android
{
throw new NotSupportedException();
}
-
- public IPopupImpl CreatePopup()
- {
- return new PopupImpl();
- }
}
}
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs
deleted file mode 100644
index e89414d1f8..0000000000
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using System;
-using Android.Content;
-using Android.Graphics;
-using Android.Runtime;
-using Android.Views;
-using Avalonia.Controls;
-using Avalonia.Platform;
-
-namespace Avalonia.Android.Platform.SkiaPlatform
-{
- class PopupImpl : TopLevelImpl, IPopupImpl
- {
- private PixelPoint _position;
- private bool _isAdded;
- Action IWindowBaseImpl.Activated { get; set; }
- public Action PositionChanged { get; set; }
- public Action Deactivated { get; set; }
-
- public PopupImpl() : base(ActivityTracker.Current, true)
- {
- }
-
- private Size _clientSize = new Size(1, 1);
-
- public void Resize(Size value)
- {
- if (View == null)
- return;
- _clientSize = value;
- UpdateParams();
- }
-
- public void SetMinMaxSize(Size minSize, Size maxSize)
- {
- }
-
- public IScreenImpl Screen { get; }
-
- public PixelPoint Position
- {
- get { return _position; }
- set
- {
- _position = value;
- PositionChanged?.Invoke(_position);
- UpdateParams();
- }
- }
-
- WindowManagerLayoutParams CreateParams() => new WindowManagerLayoutParams(0,
- WindowManagerFlags.NotTouchModal, Format.Translucent)
- {
- Gravity = GravityFlags.Left | GravityFlags.Top,
- WindowAnimations = 0,
- X = (int) _position.X,
- Y = (int) _position.Y,
- Width = Math.Max(1, (int) _clientSize.Width),
- Height = Math.Max(1, (int) _clientSize.Height)
- };
-
- void UpdateParams()
- {
- if (_isAdded)
- ActivityTracker.Current?.WindowManager?.UpdateViewLayout(View, CreateParams());
- }
-
- public override void Show()
- {
- if (_isAdded)
- return;
- ActivityTracker.Current.WindowManager.AddView(View, CreateParams());
- _isAdded = true;
- }
-
- public override void Hide()
- {
- if (_isAdded)
- {
- var wm = View.Context.ApplicationContext.GetSystemService(Context.WindowService)
- .JavaCast();
- wm.RemoveView(View);
- _isAdded = false;
- }
- }
-
- public override void Dispose()
- {
- Hide();
- base.Dispose();
- }
-
-
- public void Activate()
- {
- }
-
- public void BeginMoveDrag()
- {
- //Not supported
- }
-
- public void BeginResizeDrag(WindowEdge edge)
- {
- //Not supported
- }
-
- public void SetTopmost(bool value)
- {
- //Not supported
- }
- }
-}
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index f42faeaa63..0d0d9db252 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -191,6 +191,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
}
}
+ public IPopupImpl CreatePopup() => null;
+
ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface);
}
}
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index f32b8fabc6..a70d26624c 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -202,7 +202,7 @@ namespace Avalonia.Controls
{
if (!e.Handled)
{
- if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot)
+ if (_popup?.IsInsidePopup((IVisual)e.Source) == true)
{
if (UpdateSelectionFromEventSource(e.Source))
{
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index d0804107b3..a5025df82d 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -91,6 +91,8 @@ namespace Avalonia.Controls
/// The control.
public void Open(Control control)
{
+ if (control == null)
+ throw new ArgumentNullException(nameof(control));
if (IsOpen)
{
return;
diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
index 9c53dc0c10..29f0374301 100644
--- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
+++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
@@ -61,5 +61,6 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action Closed { get; set; }
public abstract IMouseDevice MouseDevice { get; }
+ public IPopupImpl CreatePopup() => null;
}
}
diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index bd558af5ef..38cc3f6daf 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -224,7 +224,7 @@ namespace Avalonia.Controls
public bool IsTopLevel => Parent is Menu;
///
- bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false;
+ bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
///
IMenuElement IMenuItem.Parent => Parent as IMenuElement;
diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
index 93873cbf7d..aa91224572 100644
--- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
+++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
@@ -150,7 +150,7 @@ namespace Avalonia.Controls.Notifications
private void Install(Window host)
{
var adornerLayer = host.GetVisualDescendants()
- .OfType()
+ .OfType()
.FirstOrDefault()
?.AdornerLayer;
diff --git a/src/Avalonia.Controls/PlacementMode.cs b/src/Avalonia.Controls/PlacementMode.cs
index db77b6a365..99958c4c9e 100644
--- a/src/Avalonia.Controls/PlacementMode.cs
+++ b/src/Avalonia.Controls/PlacementMode.cs
@@ -23,6 +23,21 @@ namespace Avalonia.Controls
///
/// The popup is placed at the top right of its target.
///
- Right
+ Right,
+
+ ///
+ /// The popup is placed at the top left of its target.
+ ///
+ Left,
+
+ ///
+ /// The popup is placed at the top left of its target.
+ ///
+ Top,
+
+ ///
+ /// The popup is placed according to anchor and gravity rules
+ ///
+ AnchorAndGravity
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 5f63a44717..b0dfa4185e 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -396,7 +396,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void WindowDeactivated(object sender, EventArgs e)
{
- Menu.Close();
+ Menu?.Close();
}
protected void Click(IMenuItem item)
diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs
index 1b606f550b..2978016519 100644
--- a/src/Avalonia.Controls/Platform/IPopupImpl.cs
+++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs
@@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Controls.Primitives.PopupPositioning;
+
namespace Avalonia.Platform
{
///
@@ -8,6 +10,6 @@ namespace Avalonia.Platform
///
public interface IPopupImpl : IWindowBaseImpl
{
-
+ IPopupPositioner PopupPositioner { get; }
}
}
diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs
index 8d8ce35c38..cfbc0b1c4b 100644
--- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs
+++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs
@@ -107,5 +107,7 @@ namespace Avalonia.Platform
///
[CanBeNull]
IMouseDevice MouseDevice { get; }
+
+ IPopupImpl CreatePopup();
}
}
diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
index b37521de30..8c99dffc28 100644
--- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
+++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
@@ -15,21 +15,10 @@ namespace Avalonia.Platform
///
void Hide();
- ///
- /// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
- ///
- void BeginMoveDrag();
-
- ///
- /// Starts resizing a window. This function is used if an application has window resizing controls.
- /// Should be called from left mouse button press event handler
- ///
- void BeginResizeDrag(WindowEdge edge);
-
///
/// Gets the position of the window in device pixels.
///
- PixelPoint Position { get; set; }
+ PixelPoint Position { get; }
///
/// Gets or sets a method called when the window's position changes.
@@ -61,17 +50,6 @@ namespace Avalonia.Platform
///
Size MaxClientSize { get; }
- ///
- /// Sets the client size of the top level.
- ///
- void Resize(Size clientSize);
-
- ///
- /// Minimum width of the window.
- ///
- ///
- void SetMinMaxSize(Size minSize, Size maxSize);
-
///
/// Sets whether this window appears on top of all other windows
///
diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs
index 2ddc5a5c85..bc5d38c845 100644
--- a/src/Avalonia.Controls/Platform/IWindowImpl.cs
+++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs
@@ -57,5 +57,32 @@ namespace Avalonia.Platform
/// Return true to prevent the underlying implementation from closing.
///
Func Closing { get; set; }
+
+ ///
+ /// Starts moving a window with left button being held. Should be called from left mouse button press event handler.
+ ///
+ void BeginMoveDrag();
+
+ ///
+ /// Starts resizing a window. This function is used if an application has window resizing controls.
+ /// Should be called from left mouse button press event handler
+ ///
+ void BeginResizeDrag(WindowEdge edge);
+
+ ///
+ /// Sets the client size of the top level.
+ ///
+ void Resize(Size clientSize);
+
+ ///
+ /// Sets the client size of the top level.
+ ///
+ void Move(PixelPoint point);
+
+ ///
+ /// Minimum width of the window.
+ ///
+ ///
+ void SetMinMaxSize(Size minSize, Size maxSize);
}
}
diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs
index 5c2c1a8da3..a55bd63c6a 100644
--- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs
+++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs
@@ -4,6 +4,5 @@ namespace Avalonia.Platform
{
IWindowImpl CreateWindow();
IEmbeddableWindowImpl CreateEmbeddableWindow();
- IPopupImpl CreatePopup();
}
}
diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs
index fa01b9e839..ef453274b8 100644
--- a/src/Avalonia.Controls/Platform/PlatformManager.cs
+++ b/src/Avalonia.Controls/Platform/PlatformManager.cs
@@ -41,10 +41,5 @@ namespace Avalonia.Controls.Platform
throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered.");
return platform.CreateEmbeddableWindow();
}
-
- public static IPopupImpl CreatePopup()
- {
- return AvaloniaLocator.Current.GetService().CreatePopup();
- }
}
}
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index c2690d503d..e2e73bd465 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -229,6 +229,7 @@ namespace Avalonia.Controls.Presenters
if (oldChild != null)
{
VisualChildren.Remove(oldChild);
+ ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent);
}
if (oldChild?.Parent == this)
diff --git a/src/Avalonia.Controls/Primitives/AdornerDecorator.cs b/src/Avalonia.Controls/Primitives/AdornerDecorator.cs
deleted file mode 100644
index 4608d64806..0000000000
--- a/src/Avalonia.Controls/Primitives/AdornerDecorator.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using Avalonia.LogicalTree;
-
-namespace Avalonia.Controls.Primitives
-{
- public class AdornerDecorator : Decorator
- {
- public AdornerDecorator()
- {
- AdornerLayer = new AdornerLayer();
- ((ISetLogicalParent)AdornerLayer).SetParent(this);
- AdornerLayer.ZIndex = int.MaxValue;
- VisualChildren.Add(AdornerLayer);
- }
-
- protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
- {
- base.OnAttachedToLogicalTree(e);
-
- ((ILogical)AdornerLayer).NotifyAttachedToLogicalTree(e);
- }
-
- public AdornerLayer AdornerLayer
- {
- get;
- }
-
- protected override Size MeasureOverride(Size availableSize)
- {
- AdornerLayer.Measure(availableSize);
- return base.MeasureOverride(availableSize);
- }
-
- protected override Size ArrangeOverride(Size finalSize)
- {
- AdornerLayer.Arrange(new Rect(finalSize));
- return base.ArrangeOverride(finalSize);
- }
- }
-}
diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
index d198570909..ebe5e0a93e 100644
--- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs
+++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
@@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives
public static AdornerLayer GetAdornerLayer(IVisual visual)
{
return visual.GetVisualAncestors()
- .OfType()
+ .OfType()
.FirstOrDefault()
?.AdornerLayer;
}
diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs
new file mode 100644
index 0000000000..74a3ca8818
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs
@@ -0,0 +1,26 @@
+using System;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+ public interface IPopupHost : IDisposable
+ {
+ void SetChild(IControl control);
+ IContentPresenter Presenter { get; }
+ IVisual HostedVisualTreeRoot { get; }
+
+ event EventHandler TemplateApplied;
+
+ void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
+ PopupPositioningEdge anchor = PopupPositioningEdge.None,
+ PopupPositioningEdge gravity = PopupPositioningEdge.None);
+ void Show();
+ void Hide();
+ IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty,
+ StyledProperty minWidthProperty, StyledProperty maxWidthProperty,
+ StyledProperty heightProperty, StyledProperty minHeightProperty,
+ StyledProperty maxHeightProperty, StyledProperty topmostProperty);
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs
new file mode 100644
index 0000000000..487a5e91e4
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs
@@ -0,0 +1,38 @@
+using System.Linq;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+ public class OverlayLayer : Canvas, ICustomSimpleHitTest
+ {
+ public Size AvailableSize { get; private set; }
+ public static OverlayLayer GetOverlayLayer(IVisual visual)
+ {
+ foreach(var v in visual.GetVisualAncestors())
+ if(v is VisualLayerManager vlm)
+ if (vlm.OverlayLayer != null)
+ return vlm.OverlayLayer;
+ if (visual is TopLevel tl)
+ {
+ var layers = tl.GetVisualDescendants().OfType().FirstOrDefault();
+ return layers?.OverlayLayer;
+ }
+
+ return null;
+ }
+
+ public bool HitTest(Point point)
+ {
+ return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ // We are saving it here since child controls might need to know the entire size of the overlay
+ // and Bounds won't be updated in time
+ AvailableSize = finalSize;
+ return base.ArrangeOverride(finalSize);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
new file mode 100644
index 0000000000..3dc9d302db
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+ public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup
+ {
+ private readonly OverlayLayer _overlayLayer;
+ private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters();
+ private ManagedPopupPositioner _positioner;
+ private Point _lastRequestedPosition;
+ private bool _shown;
+
+ public OverlayPopupHost(OverlayLayer overlayLayer)
+ {
+ _overlayLayer = overlayLayer;
+ _positioner = new ManagedPopupPositioner(this);
+ }
+
+ public void SetChild(IControl control)
+ {
+ Content = control;
+ }
+
+ public IVisual HostedVisualTreeRoot => null;
+
+ ///
+ IInteractive IInteractive.InteractiveParent => Parent;
+
+ public void Dispose() => Hide();
+
+
+ public void Show()
+ {
+ _overlayLayer.Children.Add(this);
+ _shown = true;
+ }
+
+ public void Hide()
+ {
+ _overlayLayer.Children.Remove(this);
+ _shown = false;
+ }
+
+ public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty,
+ StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty,
+ StyledProperty maxHeightProperty, StyledProperty topmostProperty)
+ {
+ // Topmost property is not supported
+ var bindings = new List();
+
+ void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
+ Bind(WidthProperty, widthProperty);
+ Bind(MinWidthProperty, minWidthProperty);
+ Bind(MaxWidthProperty, maxWidthProperty);
+ Bind(HeightProperty, heightProperty);
+ Bind(MinHeightProperty, minHeightProperty);
+ Bind(MaxHeightProperty, maxHeightProperty);
+
+ return Disposable.Create(() =>
+ {
+ foreach (var x in bindings)
+ x.Dispose();
+ });
+ }
+
+ public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
+ PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None)
+ {
+ _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor,
+ gravity);
+ UpdatePosition();
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ if (_positionerParameters.Size != finalSize)
+ {
+ _positionerParameters.Size = finalSize;
+ UpdatePosition();
+ }
+ return base.ArrangeOverride(finalSize);
+ }
+
+
+ private void UpdatePosition()
+ {
+ // Don't bother the positioner with layout system artifacts
+ if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0)
+ return;
+ if (_shown)
+ {
+ _positioner.Update(_positionerParameters);
+ }
+ }
+
+ IReadOnlyList IManagedPopupPositionerPopup.Screens
+ {
+ get
+ {
+ var rc = new Rect(default, _overlayLayer.AvailableSize);
+ return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)};
+ }
+ }
+
+ Rect IManagedPopupPositionerPopup.ParentClientAreaScreenGeometry =>
+ new Rect(default, _overlayLayer.Bounds.Size);
+
+ void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize)
+ {
+ _lastRequestedPosition = devicePoint;
+ Dispatcher.UIThread.Post(() =>
+ {
+ OverlayLayer.SetLeft(this, _lastRequestedPosition.X);
+ OverlayLayer.SetTop(this, _lastRequestedPosition.Y);
+ }, DispatcherPriority.Layout);
+ }
+
+ Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt;
+
+ Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size;
+
+ public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver)
+ {
+ var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup();
+ if (platform != null)
+ return new PopupRoot((TopLevel)target.GetVisualRoot(), platform, dependencyResolver);
+
+ var overlayLayer = OverlayLayer.GetOverlayLayer(target);
+ if (overlayLayer == null)
+ throw new InvalidOperationException(
+ "Unable to create IPopupImpl and no overlay layer is found for the target control");
+
+
+ return new OverlayPopupHost(overlayLayer);
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size));
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 058658357f..5ddbed5944 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -2,7 +2,12 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Presenters;
+using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
@@ -42,7 +47,7 @@ namespace Avalonia.Controls.Primitives
/// Defines the property.
///
public static readonly StyledProperty ObeyScreenEdgesProperty =
- AvaloniaProperty.Register(nameof(ObeyScreenEdges));
+ AvaloniaProperty.Register(nameof(ObeyScreenEdges), true);
///
/// Defines the property.
@@ -75,10 +80,12 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register(nameof(Topmost));
private bool _isOpen;
- private PopupRoot _popupRoot;
+ private IPopupHost _popupHost;
private TopLevel _topLevel;
private IDisposable _nonClientListener;
+ private IDisposable _presenterSubscription;
bool _ignoreIsOpenChanged = false;
+ private List _bindings = new List();
///
/// Initializes static members of the class.
@@ -88,7 +95,11 @@ namespace Avalonia.Controls.Primitives
IsHitTestVisibleProperty.OverrideDefaultValue(false);
ChildProperty.Changed.AddClassHandler(x => x.ChildChanged);
IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged);
- TopmostProperty.Changed.AddClassHandler((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue);
+ }
+
+ public Popup()
+ {
+
}
///
@@ -101,10 +112,7 @@ namespace Avalonia.Controls.Primitives
///
public event EventHandler Opened;
- ///
- /// Raised when the popup root has been created, but before it has been shown.
- ///
- public event EventHandler PopupRootCreated;
+ public IPopupHost Host => _popupHost;
///
/// Gets or sets the control to display in the popup.
@@ -147,10 +155,7 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
- ///
- /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary
- /// when its opened at a position where it would otherwise overlap the screen edge.
- ///
+ [Obsolete("This property has no effect")]
public bool ObeyScreenEdges
{
get => GetValue(ObeyScreenEdgesProperty);
@@ -184,11 +189,6 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementTargetProperty, value); }
}
- ///
- /// Gets the root of the popup window.
- ///
- public PopupRoot PopupRoot => _popupRoot;
-
///
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
@@ -211,63 +211,58 @@ namespace Avalonia.Controls.Primitives
///
/// Gets the root of the popup window.
///
- IVisual IVisualTreeHost.Root => _popupRoot;
+ IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot;
///
/// Opens the popup.
///
public void Open()
{
- if (_popupRoot == null)
+ // Popup is currently open
+ if (_topLevel != null)
+ return;
+ CloseCurrent();
+ var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType().FirstOrDefault();
+ if (placementTarget == null)
+ throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null");
+
+ _topLevel = placementTarget.GetVisualRoot() as TopLevel;
+
+ if (_topLevel == null)
{
- _popupRoot = new PopupRoot(DependencyResolver)
- {
- [~ContentControl.ContentProperty] = this[~ChildProperty],
- [~WidthProperty] = this[~WidthProperty],
- [~HeightProperty] = this[~HeightProperty],
- [~MinWidthProperty] = this[~MinWidthProperty],
- [~MaxWidthProperty] = this[~MaxWidthProperty],
- [~MinHeightProperty] = this[~MinHeightProperty],
- [~MaxHeightProperty] = this[~MaxHeightProperty],
- };
-
- ((ISetLogicalParent)_popupRoot).SetParent(this);
+ throw new InvalidOperationException(
+ "Attempted to open a popup not attached to a TopLevel");
}
- _popupRoot.Position = GetPosition();
+ _popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
+
+ _bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
+ HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
- if (_topLevel == null && PlacementTarget != null)
+ _popupHost.SetChild(Child);
+ ((ISetLogicalParent)_popupHost).SetParent(this);
+ _popupHost.ConfigurePosition(placementTarget,
+ PlacementMode, new Point(HorizontalOffset, VerticalOffset));
+ _popupHost.TemplateApplied += RootTemplateApplied;
+
+ var window = _topLevel as Window;
+ if (window != null)
{
- _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel;
+ window.Deactivated += WindowDeactivated;
}
-
- if (_topLevel != null)
+ else
{
- var window = _topLevel as Window;
- if (window != null)
+ var parentPopuproot = _topLevel as PopupRoot;
+ if (parentPopuproot?.Parent is Popup popup)
{
- window.Deactivated += WindowDeactivated;
+ popup.Closed += ParentClosed;
}
- else
- {
- var parentPopuproot = _topLevel as PopupRoot;
- if (parentPopuproot?.Parent is Popup popup)
- {
- popup.Closed += ParentClosed;
- }
- }
- _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
- _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick);
}
+ _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
+ _nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick);
+
- PopupRootCreated?.Invoke(this, EventArgs.Empty);
-
- _popupRoot.Show();
-
- if (ObeyScreenEdges)
- {
- _popupRoot.SnapInsideScreenEdges();
- }
+ _popupHost.Show();
using (BeginIgnoringIsOpen())
{
@@ -282,29 +277,14 @@ namespace Avalonia.Controls.Primitives
///
public void Close()
{
- if (_popupRoot != null)
+ if (_popupHost != null)
{
- if (_topLevel != null)
- {
- _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
- var window = _topLevel as Window;
- if (window != null)
- window.Deactivated -= WindowDeactivated;
- else
- {
- var parentPopuproot = _topLevel as PopupRoot;
- if (parentPopuproot?.Parent is Popup popup)
- {
- popup.Closed -= ParentClosed;
- }
- }
- _nonClientListener?.Dispose();
- _nonClientListener = null;
- }
-
- _popupRoot.Hide();
+ _popupHost.TemplateApplied -= RootTemplateApplied;
}
+ _presenterSubscription?.Dispose();
+
+ CloseCurrent();
using (BeginIgnoringIsOpen())
{
IsOpen = false;
@@ -313,6 +293,41 @@ namespace Avalonia.Controls.Primitives
Closed?.Invoke(this, EventArgs.Empty);
}
+ void CloseCurrent()
+ {
+ if (_topLevel != null)
+ {
+ _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
+ var window = _topLevel as Window;
+ if (window != null)
+ window.Deactivated -= WindowDeactivated;
+ else
+ {
+ var parentPopuproot = _topLevel as PopupRoot;
+ if (parentPopuproot?.Parent is Popup popup)
+ {
+ popup.Closed -= ParentClosed;
+ }
+ }
+ _nonClientListener?.Dispose();
+ _nonClientListener = null;
+
+ _topLevel = null;
+ }
+ if (_popupHost != null)
+ {
+ foreach(var b in _bindings)
+ b.Dispose();
+ _bindings.Clear();
+ _popupHost.SetChild(null);
+ _popupHost.Hide();
+ ((ISetLogicalParent)_popupHost).SetParent(null);
+ _popupHost.Dispose();
+ _popupHost = null;
+ }
+
+ }
+
///
/// Measures the control.
///
@@ -323,27 +338,14 @@ namespace Avalonia.Controls.Primitives
return new Size();
}
- ///
- protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
- {
- base.OnAttachedToLogicalTree(e);
- _topLevel = e.Root as TopLevel;
- }
-
///
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
- _topLevel = null;
-
- if (_popupRoot != null)
- {
- ((ISetLogicalParent)_popupRoot).SetParent(null);
- _popupRoot.Dispose();
- _popupRoot = null;
- }
+ Close();
}
+
///
/// Called when the property changes.
///
@@ -380,49 +382,6 @@ namespace Avalonia.Controls.Primitives
}
}
- ///
- /// Gets the position for the popup based on the placement properties.
- ///
- /// The popup's position in screen coordinates.
- protected virtual PixelPoint GetPosition()
- {
- var result = GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot,
- HorizontalOffset, VerticalOffset);
-
- return result;
- }
-
- internal static PixelPoint GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
- {
- var root = target?.GetVisualRoot();
- var mode = root != null ? placement : PlacementMode.Pointer;
- var scaling = root?.RenderScaling ?? 1;
-
- switch (mode)
- {
- case PlacementMode.Pointer:
- if (popupRoot != null)
- {
- var screenOffset = PixelPoint.FromPoint(new Point(horizontalOffset, verticalOffset), scaling);
- var mouseOffset = ((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default;
- return new PixelPoint(
- screenOffset.X + mouseOffset.X,
- screenOffset.Y + mouseOffset.Y);
- }
-
- return default;
-
- case PlacementMode.Bottom:
- return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? default;
-
- case PlacementMode.Right:
- return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? default;
-
- default:
- throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
- }
- }
-
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
@@ -445,17 +404,62 @@ namespace Avalonia.Controls.Primitives
}
}
- private bool IsChildOrThis(IVisual child)
+ private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e)
{
- IVisual root = child.GetVisualRoot();
- while (root is PopupRoot)
+ _popupHost.TemplateApplied -= RootTemplateApplied;
+
+ if (_presenterSubscription != null)
{
- if (root == PopupRoot) return true;
- root = ((PopupRoot)root).Parent.GetVisualRoot();
+ _presenterSubscription.Dispose();
+ _presenterSubscription = null;
+ }
+
+ // If the Popup appears in a control template, then the child controls
+ // that appear in the popup host need to have their TemplatedParent
+ // properties set.
+ if (TemplatedParent != null)
+ {
+ _popupHost.Presenter?.ApplyTemplate();
+ _popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty)
+ .Subscribe(SetTemplatedParentAndApplyChildTemplates);
+ }
+ }
+
+ private void SetTemplatedParentAndApplyChildTemplates(IControl control)
+ {
+ if (control != null)
+ {
+ var templatedParent = TemplatedParent;
+
+ if (control.TemplatedParent == null)
+ {
+ control.SetValue(TemplatedParentProperty, templatedParent);
+ }
+
+ control.ApplyTemplate();
+
+ if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
+ {
+ foreach (IControl child in control.GetVisualChildren())
+ {
+ SetTemplatedParentAndApplyChildTemplates(child);
+ }
+ }
}
- return false;
}
+ private bool IsChildOrThis(IVisual child)
+ {
+ return _popupHost != null && ((IVisual)_popupHost).FindCommonVisualAncestor(child) == _popupHost;
+ }
+
+ public bool IsInsidePopup(IVisual visual)
+ {
+ return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true;
+ }
+
+ public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver;
+
private void WindowDeactivated(object sender, EventArgs e)
{
if (!StaysOpen)
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
new file mode 100644
index 0000000000..3010a3d8a8
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
@@ -0,0 +1,358 @@
+// The documentation and flag names in this file are initially taken from
+// xdg_shell wayland protocol this API is designed after
+// therefore, I'm including the license from wayland-protocols repo
+
+/*
+Copyright © 2008-2013 Kristian Høgsberg
+Copyright © 2010-2013 Intel Corporation
+Copyright © 2013 Rafael Antognolli
+Copyright © 2013 Jasper St. Pierre
+Copyright © 2014 Jonas Ådahl
+Copyright © 2014 Jason Ekstrand
+Copyright © 2014-2015 Collabora, Ltd.
+Copyright © 2015 Red Hat Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice (including the next
+paragraph) shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+---
+
+The above is the version of the MIT "Expat" License used by X.org:
+
+ http://cgit.freedesktop.org/xorg/xserver/tree/COPYING
+
+
+Adjustments for Avalonia needs:
+Copyright © 2019 Nikita Tsukanov
+
+
+*/
+
+using System;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives.PopupPositioning
+{
+ ///
+ ///
+ /// The IPopupPositioner provides a collection of rules for the placement of a
+ /// a popup relative to its parent. Rules can be defined to ensure
+ /// the popup remains within the visible area's borders, and to
+ /// specify how the popup changes its position, such as sliding along
+ /// an axis, or flipping around a rectangle. These positioner-created rules are
+ /// constrained by the requirement that a popup must intersect with or
+ /// be at least partially adjacent to its parent surface.
+ ///
+ public struct PopupPositionerParameters
+ {
+ private PopupPositioningEdge _gravity;
+ private PopupPositioningEdge _anchor;
+
+ ///
+ /// Set the size of the popup that is to be positioned with the positioner
+ /// object. The size is in scaled coordinates.
+ ///
+ public Size Size { get; set; }
+
+ ///
+ /// Specify the anchor rectangle within the parent that the popup
+ /// will be placed relative to. The rectangle is relative to the
+ /// parent geometry
+ ///
+ /// The anchor rectangle may not extend outside the window geometry of the
+ /// popup's parent. The anchor rectangle is in scaled coordinates
+ ///
+ public Rect AnchorRectangle { get; set; }
+
+
+ ///
+ /// Defines the anchor point for the anchor rectangle. The specified anchor
+ /// is used derive an anchor point that the popup will be
+ /// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or
+ /// 'BottomRight'), the anchor point will be at the specified corner;
+ /// otherwise, the derived anchor point will be centered on the specified
+ /// edge, or in the center of the anchor rectangle if no edge is specified.
+ ///
+ public PopupPositioningEdge Anchor
+ {
+ get => _anchor;
+ set
+ {
+ PopupPositioningEdgeHelper.ValidateEdge(value);
+ _anchor = value;
+ }
+ }
+
+ ///
+ /// Defines in what direction a popup should be positioned, relative to
+ /// the anchor point of the parent. If a corner gravity is
+ /// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup
+ /// will be placed towards the specified gravity; otherwise, the popup
+ /// will be centered over the anchor point on any axis that had no
+ /// gravity specified.
+ ///
+ public PopupPositioningEdge Gravity
+ {
+ get => _gravity;
+ set
+ {
+ PopupPositioningEdgeHelper.ValidateEdge(value);
+ _gravity = value;
+ }
+ }
+
+ ///
+ /// Specify how the popup should be positioned if the originally intended
+ /// position caused the popup to be constrained, meaning at least
+ /// partially outside positioning boundaries set by the positioner. The
+ /// adjustment is set by constructing a bitmask describing the adjustment to
+ /// be made when the popup is constrained on that axis.
+ ///
+ /// If no bit for one axis is set, the positioner will assume that the child
+ /// surface should not change its position on that axis when constrained.
+ ///
+ /// If more than one bit for one axis is set, the order of how adjustments
+ /// are applied is specified in the corresponding adjustment descriptions.
+ ///
+ /// The default adjustment is none.
+ ///
+ public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
+
+ ///
+ /// Specify the popup position offset relative to the position of the
+ /// anchor on the anchor rectangle and the anchor on the popup. For
+ /// example if the anchor of the anchor rectangle is at (x, y), the popup
+ /// has the gravity bottom|right, and the offset is (ox, oy), the calculated
+ /// surface position will be (x + ox, y + oy). The offset position of the
+ /// surface is the one used for constraint testing. See
+ /// set_constraint_adjustment.
+ ///
+ /// An example use case is placing a popup menu on top of a user interface
+ /// element, while aligning the user interface element of the parent surface
+ /// with some user interface element placed somewhere in the popup.
+ ///
+ public Point Offset { get; set; }
+ }
+
+ ///
+ /// The constraint adjustment value define ways how popup position will
+ /// be adjusted if the unadjusted position would result in the popup
+ /// being partly constrained.
+ ///
+ /// Whether a popup is considered 'constrained' is left to the positioner
+ /// to determine. For example, the popup may be partly outside the
+ /// target platform defined 'work area', thus necessitating the popup's
+ /// position be adjusted until it is entirely inside the work area.
+ ///
+ [Flags]
+ public enum PopupPositionerConstraintAdjustment
+ {
+ ///
+ /// Don't alter the surface position even if it is constrained on some
+ /// axis, for example partially outside the edge of an output.
+ ///
+ None = 0,
+
+ ///
+ /// Slide the surface along the x axis until it is no longer constrained.
+ /// First try to slide towards the direction of the gravity on the x axis
+ /// until either the edge in the opposite direction of the gravity is
+ /// unconstrained or the edge in the direction of the gravity is
+ /// constrained.
+ ///
+ /// Then try to slide towards the opposite direction of the gravity on the
+ /// x axis until either the edge in the direction of the gravity is
+ /// unconstrained or the edge in the opposite direction of the gravity is
+ /// constrained.
+ ///
+ SlideX = 1,
+
+
+ ///
+ /// Slide the surface along the y axis until it is no longer constrained.
+ ///
+ /// First try to slide towards the direction of the gravity on the y axis
+ /// until either the edge in the opposite direction of the gravity is
+ /// unconstrained or the edge in the direction of the gravity is
+ /// constrained.
+ ///
+ /// Then try to slide towards the opposite direction of the gravity on the
+ /// y axis until either the edge in the direction of the gravity is
+ /// unconstrained or the edge in the opposite direction of the gravity is
+ /// constrained.
+ /// */
+ ///
+ SlideY = 2,
+
+ ///
+ /// Invert the anchor and gravity on the x axis if the surface is
+ /// constrained on the x axis. For example, if the left edge of the
+ /// surface is constrained, the gravity is 'left' and the anchor is
+ /// 'left', change the gravity to 'right' and the anchor to 'right'.
+ ///
+ /// If the adjusted position also ends up being constrained, the resulting
+ /// position of the flip_x adjustment will be the one before the
+ /// adjustment.
+ ///
+ FlipX = 4,
+
+ ///
+ /// Invert the anchor and gravity on the y axis if the surface is
+ /// constrained on the y axis. For example, if the bottom edge of the
+ /// surface is constrained, the gravity is 'bottom' and the anchor is
+ /// 'bottom', change the gravity to 'top' and the anchor to 'top'.
+ ///
+ /// The adjusted position is calculated given the original anchor
+ /// rectangle and offset, but with the new flipped anchor and gravity
+ /// values.
+ ///
+ /// If the adjusted position also ends up being constrained, the resulting
+ /// position of the flip_y adjustment will be the one before the
+ /// adjustment.
+ ///
+ FlipY = 8,
+ All = SlideX|SlideY|FlipX|FlipY
+ }
+
+ static class PopupPositioningEdgeHelper
+ {
+ public static void ValidateEdge(this PopupPositioningEdge edge)
+ {
+ if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0)
+ ||
+ ((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0))
+ throw new ArgumentException("Opposite edges specified");
+ }
+
+ public static PopupPositioningEdge Flip(this PopupPositioningEdge edge)
+ {
+ var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right;
+ var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom;
+ if ((edge & hmask) != 0)
+ edge ^= hmask;
+ if ((edge & vmask) != 0)
+ edge ^= vmask;
+ return edge;
+ }
+
+ public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge)
+ {
+ if ((edge & PopupPositioningEdge.HorizontalMask) != 0)
+ edge ^= PopupPositioningEdge.HorizontalMask;
+ return edge;
+ }
+
+ public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge)
+ {
+ if ((edge & PopupPositioningEdge.VerticalMask) != 0)
+ edge ^= PopupPositioningEdge.VerticalMask;
+ return edge;
+ }
+
+ }
+
+ [Flags]
+ public enum PopupPositioningEdge
+ {
+ None,
+ Top = 1,
+ Bottom = 2,
+ Left = 4,
+ Right = 8,
+ TopLeft = Top | Left,
+ TopRight = Top | Right,
+ BottomLeft = Bottom | Left,
+ BottomRight = Bottom | Right,
+
+
+ VerticalMask = Top | Bottom,
+ HorizontalMask = Left | Right,
+ AllMask = VerticalMask|HorizontalMask
+ }
+
+ public interface IPopupPositioner
+ {
+ void Update(PopupPositionerParameters parameters);
+ }
+
+ static class PopupPositionerExtensions
+ {
+ public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
+ TopLevel topLevel,
+ IVisual target, PlacementMode placement, Point offset,
+ PopupPositioningEdge anchor, PopupPositioningEdge gravity)
+ {
+ // We need a better way for tracking the last pointer position
+ var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position);
+
+ positionerParameters.Offset = offset;
+ positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All;
+ if (placement == PlacementMode.Pointer)
+ {
+ positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1));
+ positionerParameters.Anchor = PopupPositioningEdge.BottomRight;
+ positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ }
+ else
+ {
+ if (target == null)
+ throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
+ var matrix = target.TransformToVisual(topLevel);
+ if (matrix == null)
+ {
+ if (target.GetVisualRoot() == null)
+ throw new InvalidCastException("Target control is not attached to the visual tree");
+ throw new InvalidCastException("Target control is not in the same tree as the popup parent");
+ }
+
+ positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size)
+ .TransformToAABB(matrix.Value);
+
+ if (placement == PlacementMode.Right)
+ {
+ positionerParameters.Anchor = PopupPositioningEdge.TopRight;
+ positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ }
+ else if (placement == PlacementMode.Bottom)
+ {
+ positionerParameters.Anchor = PopupPositioningEdge.BottomLeft;
+ positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ }
+ else if (placement == PlacementMode.Left)
+ {
+ positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
+ positionerParameters.Gravity = PopupPositioningEdge.BottomLeft;
+ }
+ else if (placement == PlacementMode.Top)
+ {
+ positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
+ positionerParameters.Gravity = PopupPositioningEdge.TopRight;
+ }
+ else if (placement == PlacementMode.AnchorAndGravity)
+ {
+ positionerParameters.Anchor = anchor;
+ positionerParameters.Gravity = gravity;
+ }
+ else
+ throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
+ }
+ }
+ }
+
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
new file mode 100644
index 0000000000..d428952bb9
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Controls.Primitives.PopupPositioning
+{
+ public interface IManagedPopupPositionerPopup
+ {
+ IReadOnlyList Screens { get; }
+ Rect ParentClientAreaScreenGeometry { get; }
+ void MoveAndResize(Point devicePoint, Size virtualSize);
+ Point TranslatePoint(Point pt);
+ Size TranslateSize(Size size);
+ }
+
+ public class ManagedPopupPositionerScreenInfo
+ {
+ public Rect Bounds { get; }
+ public Rect WorkingArea { get; }
+
+ public ManagedPopupPositionerScreenInfo(Rect bounds, Rect workingArea)
+ {
+ Bounds = bounds;
+ WorkingArea = workingArea;
+ }
+ }
+
+ public class ManagedPopupPositioner : IPopupPositioner
+ {
+ private readonly IManagedPopupPositionerPopup _popup;
+
+ public ManagedPopupPositioner(IManagedPopupPositionerPopup popup)
+ {
+ _popup = popup;
+ }
+
+
+ private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge)
+ {
+ double x, y;
+ if ((edge & PopupPositioningEdge.Left) != 0)
+ x = anchorRect.X;
+ else if ((edge & PopupPositioningEdge.Right) != 0)
+ x = anchorRect.Right;
+ else
+ x = anchorRect.X + anchorRect.Width / 2;
+
+ if ((edge & PopupPositioningEdge.Top) != 0)
+ y = anchorRect.Y;
+ else if ((edge & PopupPositioningEdge.Bottom) != 0)
+ y = anchorRect.Bottom;
+ else
+ y = anchorRect.Y + anchorRect.Height / 2;
+ return new Point(x, y);
+ }
+
+ private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity)
+ {
+ double x, y;
+ if ((gravity & PopupPositioningEdge.Left) != 0)
+ x = -size.Width;
+ else if ((gravity & PopupPositioningEdge.Right) != 0)
+ x = 0;
+ else
+ x = -size.Width / 2;
+
+ if ((gravity & PopupPositioningEdge.Top) != 0)
+ y = -size.Height;
+ else if ((gravity & PopupPositioningEdge.Bottom) != 0)
+ y = 0;
+ else
+ y = -size.Height / 2;
+ return anchorPoint + new Point(x, y);
+ }
+
+ public void Update(PopupPositionerParameters parameters)
+ {
+
+ Update(_popup.TranslateSize(parameters.Size), parameters.Size,
+ new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft),
+ _popup.TranslateSize(parameters.AnchorRectangle.Size)),
+ parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment,
+ _popup.TranslatePoint(parameters.Offset));
+ }
+
+
+ private void Update(Size translatedSize, Size originalSize,
+ Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity,
+ PopupPositionerConstraintAdjustment constraintAdjustment, Point offset)
+ {
+ var parentGeometry = _popup.ParentClientAreaScreenGeometry;
+ anchorRect = anchorRect.Translate(parentGeometry.TopLeft);
+
+ Rect GetBounds()
+ {
+ var screens = _popup.Screens;
+
+ var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft))
+ ?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect))
+ ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft))
+ ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry))
+ ?? screens.FirstOrDefault();
+ return targetScreen?.WorkingArea
+ ?? new Rect(0, 0, double.MaxValue, double.MaxValue);
+ }
+
+ var bounds = GetBounds();
+
+ bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask)
+ {
+ if ((edge & PopupPositioningEdge.Left) != 0
+ && rc.X < bounds.X)
+ return false;
+
+ if ((edge & PopupPositioningEdge.Top) != 0
+ && rc.Y < bounds.Y)
+ return false;
+
+ if ((edge & PopupPositioningEdge.Right) != 0
+ && rc.Right > bounds.Right)
+ return false;
+
+ if ((edge & PopupPositioningEdge.Bottom) != 0
+ && rc.Bottom > bounds.Bottom)
+ return false;
+
+ return true;
+ }
+
+ Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) =>
+ new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize);
+
+
+ var geo = GetUnconstrained(anchor, gravity);
+
+ // If flipping geometry and anchor is allowed and helps, use the flipped one,
+ // otherwise leave it as is
+ if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask)
+ && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0)
+ {
+ var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
+ if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask))
+ geo = geo.WithX(flipped.X);
+ }
+
+ // If sliding is allowed, try moving the rect into the bounds
+ if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0)
+ {
+ geo = geo.WithX(Math.Max(geo.X, bounds.X));
+ if (geo.Right > bounds.Right)
+ geo = geo.WithX(bounds.Right - geo.Width);
+ }
+
+ // If flipping geometry and anchor is allowed and helps, use the flipped one,
+ // otherwise leave it as is
+ if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask)
+ && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0)
+ {
+ var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
+ if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask))
+ geo = geo.WithY(flipped.Y);
+ }
+
+ // If sliding is allowed, try moving the rect into the bounds
+ if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0)
+ {
+ geo = geo.WithY(Math.Max(geo.Y, bounds.Y));
+ if (geo.Bottom > bounds.Bottom)
+ geo = geo.WithY(bounds.Bottom - geo.Height);
+ }
+
+ _popup.MoveAndResize(geo.TopLeft, originalSize);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
new file mode 100644
index 0000000000..bb701da651
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Platform;
+
+namespace Avalonia.Controls.Primitives.PopupPositioning
+{
+ ///
+ /// This class is used to simplify integration of IPopupImpl implementations with popup positioner
+ ///
+ public class ManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup
+ {
+ private readonly IWindowBaseImpl _parent;
+
+ public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling);
+ private readonly MoveResizeDelegate _moveResize;
+
+ public ManagedPopupPositionerPopupImplHelper(IWindowBaseImpl parent, MoveResizeDelegate moveResize)
+ {
+ _parent = parent;
+ _moveResize = moveResize;
+ }
+
+ public IReadOnlyList Screens =>
+
+ _parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo(
+ s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))).ToList();
+
+ public Rect ParentClientAreaScreenGeometry
+ {
+ get
+ {
+ // Popup positioner operates with abstract coordinates, but in our case they are pixel ones
+ var point = _parent.PointToScreen(default);
+ var size = PixelSize.FromSize(_parent.ClientSize, _parent.Scaling);
+ return new Rect(point.X, point.Y, size.Width, size.Height);
+
+ }
+ }
+
+ public void MoveAndResize(Point devicePoint, Size virtualSize)
+ {
+ _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling);
+ }
+
+ public Point TranslatePoint(Point pt) => pt * _parent.Scaling;
+
+ public Size TranslateSize(Size size) => size * _parent.Scaling;
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index d2e8f1ab92..b7f0c8f47d 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -2,8 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using Avalonia.Controls.Platform;
-using Avalonia.Controls.Presenters;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform;
@@ -16,9 +17,10 @@ namespace Avalonia.Controls.Primitives
///
/// The root window of a .
///
- public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost
+ public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost
{
- private IDisposable _presenterSubscription;
+ private readonly TopLevel _parent;
+ private PopupPositionerParameters _positionerParameters;
///
/// Initializes static members of the class.
@@ -31,8 +33,8 @@ namespace Avalonia.Controls.Primitives
///
/// Initializes a new instance of the class.
///
- public PopupRoot()
- : this(null)
+ public PopupRoot(TopLevel parent, IPopupImpl impl)
+ : this(parent, impl,null)
{
}
@@ -42,9 +44,10 @@ namespace Avalonia.Controls.Primitives
///
/// The dependency resolver to use. If null the default dependency resolver will be used.
///
- public PopupRoot(IAvaloniaDependencyResolver dependencyResolver)
- : base(PlatformManager.CreatePopup(), dependencyResolver)
+ public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver)
+ : base(impl, dependencyResolver)
{
+ _parent = parent;
}
///
@@ -74,73 +77,61 @@ namespace Avalonia.Controls.Primitives
///
public void Dispose() => PlatformImpl?.Dispose();
- ///
- /// Moves the Popups position so that it doesnt overlap screen edges.
- /// This method can be called immediately after Show has been called.
- ///
- public void SnapInsideScreenEdges()
+ private void UpdatePosition()
{
- var screen = (VisualRoot as WindowBase)?.Screens?.ScreenFromPoint(Position);
-
- if (screen != null)
- {
- var scaling = VisualRoot.RenderScaling;
- var bounds = PixelRect.FromRect(Bounds, scaling);
- var screenX = Position.X + bounds.Width - screen.Bounds.X;
- var screenY = Position.Y + bounds.Height - screen.Bounds.Y;
-
- if (screenX > screen.Bounds.Width)
- {
- Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
- }
-
- if (screenY > screen.Bounds.Height)
- {
- Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
- }
- }
+ PlatformImpl?.PopupPositioner.Update(_positionerParameters);
}
- ///
- protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+ public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
+ PopupPositioningEdge anchor = PopupPositioningEdge.None,
+ PopupPositioningEdge gravity = PopupPositioningEdge.None)
{
- base.OnTemplateApplied(e);
+ _positionerParameters.ConfigurePosition(_parent, target,
+ placement, offset, anchor, gravity);
+
+ if (_positionerParameters.Size != default)
+ UpdatePosition();
+ }
+
+ public void SetChild(IControl control) => Content = control;
- if (Parent?.TemplatedParent != null)
+ IVisual IPopupHost.HostedVisualTreeRoot => this;
+
+ public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty,
+ StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty,
+ StyledProperty maxHeightProperty, StyledProperty topmostProperty)
+ {
+ var bindings = new List();
+
+ void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
+ Bind(WidthProperty, widthProperty);
+ Bind(MinWidthProperty, minWidthProperty);
+ Bind(MaxWidthProperty, maxWidthProperty);
+ Bind(HeightProperty, heightProperty);
+ Bind(MinHeightProperty, minHeightProperty);
+ Bind(MaxHeightProperty, maxHeightProperty);
+ Bind(TopmostProperty, topmostProperty);
+ return Disposable.Create(() =>
{
- if (_presenterSubscription != null)
- {
- _presenterSubscription.Dispose();
- _presenterSubscription = null;
- }
-
- Presenter?.ApplyTemplate();
- Presenter?.GetObservable(ContentPresenter.ChildProperty)
- .Subscribe(SetTemplatedParentAndApplyChildTemplates);
- }
+ foreach (var x in bindings)
+ x.Dispose();
+ });
}
- private void SetTemplatedParentAndApplyChildTemplates(IControl control)
+ ///
+ /// Carries out the arrange pass of the window.
+ ///
+ /// The final window size.
+ /// The parameter unchanged.
+ protected override Size ArrangeOverride(Size finalSize)
{
- if (control != null)
+ using (BeginAutoSizing())
{
- var templatedParent = Parent.TemplatedParent;
-
- if (control.TemplatedParent == null)
- {
- control.SetValue(TemplatedParentProperty, templatedParent);
- }
-
- control.ApplyTemplate();
-
- if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
- {
- foreach (IControl child in control.GetVisualChildren())
- {
- SetTemplatedParentAndApplyChildTemplates(child);
- }
- }
+ _positionerParameters.Size = finalSize;
+ UpdatePosition();
}
+
+ return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
}
}
diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
new file mode 100644
index 0000000000..b7229eb121
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using Avalonia.LogicalTree;
+using Avalonia.Styling;
+
+namespace Avalonia.Controls.Primitives
+{
+ public class VisualLayerManager : Decorator
+ {
+ private const int AdornerZIndex = int.MaxValue - 100;
+ private const int OverlayZIndex = int.MaxValue - 99;
+ private IStyleHost _styleRoot;
+ private readonly List _layers = new List();
+
+
+ public bool IsPopup { get; set; }
+
+ public AdornerLayer AdornerLayer
+ {
+ get
+ {
+ var rv = FindLayer();
+ if (rv == null)
+ AddLayer(rv = new AdornerLayer(), AdornerZIndex);
+ return rv;
+ }
+ }
+
+ public OverlayLayer OverlayLayer
+ {
+ get
+ {
+ if (IsPopup)
+ return null;
+ var rv = FindLayer();
+ if(rv == null)
+ AddLayer(rv = new OverlayLayer(), OverlayZIndex);
+ return rv;
+ }
+ }
+
+ T FindLayer() where T : class
+ {
+ foreach (var layer in _layers)
+ if (layer is T match)
+ return match;
+ return null;
+ }
+
+ void AddLayer(Control layer, int zindex)
+ {
+ _layers.Add(layer);
+ ((ISetLogicalParent)layer).SetParent(this);
+ layer.ZIndex = zindex;
+ VisualChildren.Add(layer);
+ if (((ILogical)this).IsAttachedToLogicalTree)
+ ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot));
+ InvalidateArrange();
+ }
+
+
+ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToLogicalTree(e);
+ _styleRoot = e.Root;
+
+ foreach (var l in _layers)
+ ((ILogical)l).NotifyAttachedToLogicalTree(e);
+ }
+
+ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ _styleRoot = null;
+ base.OnDetachedFromLogicalTree(e);
+ foreach (var l in _layers)
+ ((ILogical)l).NotifyDetachedFromLogicalTree(e);
+ }
+
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ foreach (var l in _layers)
+ l.Measure(availableSize);
+ return base.MeasureOverride(availableSize);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ foreach (var l in _layers)
+ l.Arrange(new Rect(finalSize));
+ return base.ArrangeOverride(finalSize);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
index 44783e2c97..257c1b2399 100644
--- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
@@ -707,9 +707,9 @@ namespace Avalonia.Controls
}
}
- private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure();
+ private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure();
- private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange();
+ private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange();
private VirtualizingLayoutContext GetLayoutContext()
{
diff --git a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
index 732ba8501c..02ead7ef36 100644
--- a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
+++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
@@ -35,9 +35,11 @@ namespace Avalonia.Controls
{
Contract.Requires(source != null);
- _inner = source as IList;
-
- if (_inner == null && source is IEnumerable
diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Visuals/Media/PixelPoint.cs
index 995781ee9f..d62c2a2e55 100644
--- a/src/Avalonia.Visuals/Media/PixelPoint.cs
+++ b/src/Avalonia.Visuals/Media/PixelPoint.cs
@@ -59,6 +59,59 @@ namespace Avalonia
{
return !(left == right);
}
+
+ ///
+ /// Converts the to a .
+ ///
+ /// The point.
+ public static implicit operator PixelVector(PixelPoint p)
+ {
+ return new PixelVector(p.X, p.Y);
+ }
+
+ ///
+ /// Adds two points.
+ ///
+ /// The first point.
+ /// The second point.
+ /// A point that is the result of the addition.
+ public static PixelPoint operator +(PixelPoint a, PixelPoint b)
+ {
+ return new PixelPoint(a.X + b.X, a.Y + b.Y);
+ }
+
+ ///
+ /// Adds a vector to a point.
+ ///
+ /// The point.
+ /// The vector.
+ /// A point that is the result of the addition.
+ public static PixelPoint operator +(PixelPoint a, PixelVector b)
+ {
+ return new PixelPoint(a.X + b.X, a.Y + b.Y);
+ }
+
+ ///
+ /// Subtracts two points.
+ ///
+ /// The first point.
+ /// The second point.
+ /// A point that is the result of the subtraction.
+ public static PixelPoint operator -(PixelPoint a, PixelPoint b)
+ {
+ return new PixelPoint(a.X - b.X, a.Y - b.Y);
+ }
+
+ ///
+ /// Subtracts a vector from a point.
+ ///
+ /// The point.
+ /// The vector.
+ /// A point that is the result of the subtraction.
+ public static PixelPoint operator -(PixelPoint a, PixelVector b)
+ {
+ return new PixelPoint(a.X - b.X, a.Y - b.Y);
+ }
///
/// Parses a string.
@@ -106,7 +159,7 @@ namespace Avalonia
return hash;
}
}
-
+
///
/// Returns a new with the same Y co-ordinate and the specified X co-ordinate.
///
diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs
index 9c8e5ad1c4..0e2094da07 100644
--- a/src/Avalonia.Visuals/Media/PixelRect.cs
+++ b/src/Avalonia.Visuals/Media/PixelRect.cs
@@ -261,6 +261,16 @@ namespace Avalonia
{
return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom);
}
+
+ ///
+ /// Translates the rectangle by an offset.
+ ///
+ /// The offset.
+ /// The translated rectangle.
+ public PixelRect Translate(PixelVector offset)
+ {
+ return new PixelRect(Position + offset, Size);
+ }
///
/// Gets the union of two rectangles.
diff --git a/src/Avalonia.Visuals/Media/PixelVector.cs b/src/Avalonia.Visuals/Media/PixelVector.cs
new file mode 100644
index 0000000000..4a623e3bc2
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/PixelVector.cs
@@ -0,0 +1,203 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Globalization;
+using Avalonia.Animation.Animators;
+using JetBrains.Annotations;
+
+namespace Avalonia
+{
+ ///
+ /// Defines a vector.
+ ///
+ public readonly struct PixelVector
+ {
+ ///
+ /// The X vector.
+ ///
+ private readonly int _x;
+
+ ///
+ /// The Y vector.
+ ///
+ private readonly int _y;
+
+ ///
+ /// Initializes a new instance of the structure.
+ ///
+ /// The X vector.
+ /// The Y vector.
+ public PixelVector(int x, int y)
+ {
+ _x = x;
+ _y = y;
+ }
+
+ ///
+ /// Gets the X vector.
+ ///
+ public int X => _x;
+
+ ///
+ /// Gets the Y vector.
+ ///
+ public int Y => _y;
+
+ ///
+ /// Converts the to a .
+ ///
+ /// The vector.
+ public static explicit operator PixelPoint(PixelVector a)
+ {
+ return new PixelPoint(a._x, a._y);
+ }
+
+ ///
+ /// Calculates the dot product of two vectors
+ ///
+ /// First vector
+ /// Second vector
+ /// The dot product
+ public static int operator *(PixelVector a, PixelVector b)
+ {
+ return a.X * b.X + a.Y * b.Y;
+ }
+
+ ///
+ /// Scales a vector.
+ ///
+ /// The vector
+ /// The scaling factor.
+ /// The scaled vector.
+ public static PixelVector operator *(PixelVector vector, int scale)
+ {
+ return new PixelVector(vector._x * scale, vector._y * scale);
+ }
+
+ ///
+ /// Scales a vector.
+ ///
+ /// The vector
+ /// The divisor.
+ /// The scaled vector.
+ public static PixelVector operator /(PixelVector vector, int scale)
+ {
+ return new PixelVector(vector._x / scale, vector._y / scale);
+ }
+
+ ///
+ /// Length of the vector
+ ///
+ public double Length => Math.Sqrt(X * X + Y * Y);
+
+ ///
+ /// Negates a vector.
+ ///
+ /// The vector.
+ /// The negated vector.
+ public static PixelVector operator -(PixelVector a)
+ {
+ return new PixelVector(-a._x, -a._y);
+ }
+
+ ///
+ /// Adds two vectors.
+ ///
+ /// The first vector.
+ /// The second vector.
+ /// A vector that is the result of the addition.
+ public static PixelVector operator +(PixelVector a, PixelVector b)
+ {
+ return new PixelVector(a._x + b._x, a._y + b._y);
+ }
+
+ ///
+ /// Subtracts two vectors.
+ ///
+ /// The first vector.
+ /// The second vector.
+ /// A vector that is the result of the subtraction.
+ public static PixelVector operator -(PixelVector a, PixelVector b)
+ {
+ return new PixelVector(a._x - b._x, a._y - b._y);
+ }
+
+ ///
+ /// Check if two vectors are equal (bitwise).
+ ///
+ ///
+ ///
+ public bool Equals(PixelVector other)
+ {
+ return _x == other._x && _y == other._y;
+ }
+
+ ///
+ /// Check if two vectors are nearly equal (numerically).
+ ///
+ /// The other vector.
+ /// True if vectors are nearly equal.
+ [Pure]
+ public bool NearlyEquals(PixelVector other)
+ {
+ const float tolerance = float.Epsilon;
+
+ return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+
+ return obj is PixelVector vector && Equals(vector);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (_x.GetHashCode() * 397) ^ _y.GetHashCode();
+ }
+ }
+
+ public static bool operator ==(PixelVector left, PixelVector right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(PixelVector left, PixelVector right)
+ {
+ return !left.Equals(right);
+ }
+
+ ///
+ /// Returns the string representation of the point.
+ ///
+ /// The string representation of the point.
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y);
+ }
+
+ ///
+ /// Returns a new vector with the specified X coordinate.
+ ///
+ /// The X coordinate.
+ /// The new vector.
+ public PixelVector WithX(int x)
+ {
+ return new PixelVector(x, _y);
+ }
+
+ ///
+ /// Returns a new vector with the specified Y coordinate.
+ ///
+ /// The Y coordinate.
+ /// The new vector.
+ public PixelVector WithY(int y)
+ {
+ return new PixelVector(_x, y);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
index 709a935450..f579bf0a62 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
@@ -179,7 +179,12 @@ namespace Avalonia.Rendering.SceneGraph
/// The scene that the node is a part of.
public void SortChildren(Scene scene)
{
- var keys = new List();
+ if (_children == null || _children.Count <= 1)
+ {
+ return;
+ }
+
+ var keys = new List(Visual.VisualChildren.Count);
for (var i = 0; i < Visual.VisualChildren.Count; ++i)
{
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index 7bdc61eb28..e88a7d8db2 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -74,18 +74,13 @@ namespace Avalonia.X11
public IntPtr Display { get; set; }
public IWindowImpl CreateWindow()
{
- return new X11Window(this, false);
+ return new X11Window(this, null);
}
public IEmbeddableWindowImpl CreateEmbeddableWindow()
{
throw new NotSupportedException();
}
-
- public IPopupImpl CreatePopup()
- {
- return new X11Window(this, true);
- }
}
}
@@ -96,6 +91,7 @@ namespace Avalonia
{
public bool UseEGL { get; set; }
public bool UseGpu { get; set; } = true;
+ public bool OverlayPopups { get; set; }
public List GlxRendererBlacklist { get; set; } = new List
{
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 18c23aa31e..5481862f23 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Reactive.Disposables;
using System.Text;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.OpenGL;
@@ -21,6 +22,7 @@ namespace Avalonia.X11
unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client
{
private readonly AvaloniaX11Platform _platform;
+ private readonly IWindowImpl _popupParent;
private readonly bool _popup;
private readonly X11Info _x11;
private bool _invalidated;
@@ -38,6 +40,7 @@ namespace Avalonia.X11
private bool _mapped;
private HashSet _transientChildren = new HashSet();
private X11Window _transientParent;
+ private double? _scalingOverride;
public object SyncRoot { get; } = new object();
class InputEventContainer
@@ -47,10 +50,10 @@ namespace Avalonia.X11
private readonly Queue _inputQueue = new Queue();
private InputEventContainer _lastEvent;
private bool _useRenderWindow = false;
- public X11Window(AvaloniaX11Platform platform, bool popup)
+ public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent)
{
_platform = platform;
- _popup = popup;
+ _popup = popupParent != null;
_x11 = platform.Info;
_mouse = platform.MouseDevice;
_keyboard = platform.KeyboardDevice;
@@ -66,7 +69,7 @@ namespace Avalonia.X11
| SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore
| SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity;
- if (popup)
+ if (_popup)
{
attr.override_redirect = true;
valueMask |= SetWindowValuemask.OverrideRedirect;
@@ -150,6 +153,8 @@ namespace Avalonia.X11
_xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
XNames.XNClientWindow, _handle, IntPtr.Zero);
XFlush(_x11.Display);
+ if(_popup)
+ PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
}
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
@@ -453,22 +458,28 @@ namespace Avalonia.X11
}
}
- private bool UpdateScaling()
+ private bool UpdateScaling(bool skipResize = false)
{
lock (SyncRoot)
{
- var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
- .FirstOrDefault(m => m.Bounds.Contains(Position));
- var newScaling = monitor?.PixelDensity ?? Scaling;
+ double newScaling;
+ if (_scalingOverride.HasValue)
+ newScaling = _scalingOverride.Value;
+ else
+ {
+ var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
+ .FirstOrDefault(m => m.Bounds.Contains(Position));
+ newScaling = monitor?.PixelDensity ?? Scaling;
+ }
+
if (Scaling != newScaling)
{
- Console.WriteLine(
- $"Updating scaling from {Scaling} to {newScaling} as a response to position change to {Position}");
var oldScaledSize = ClientSize;
Scaling = newScaling;
ScalingChanged?.Invoke(Scaling);
SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
- Resize(oldScaledSize, true);
+ if(!skipResize)
+ Resize(oldScaledSize, true);
return true;
}
@@ -730,6 +741,14 @@ namespace Avalonia.X11
public void Resize(Size clientSize) => Resize(clientSize, false);
+ public void Move(PixelPoint point) => Position = point;
+ private void MoveResize(PixelPoint position, Size size, double scaling)
+ {
+ Move(position);
+ _scalingOverride = scaling;
+ UpdateScaling(true);
+ Resize(size, true);
+ }
PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling));
@@ -793,7 +812,9 @@ namespace Avalonia.X11
}
public IMouseDevice MouseDevice => _mouse;
-
+ public IPopupImpl CreatePopup()
+ => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this);
+
public void Activate()
{
if (_x11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero)
@@ -937,6 +958,8 @@ namespace Avalonia.X11
{
SendNetWMMessage(_x11.Atoms._NET_WM_STATE,
(IntPtr)(value ? 0 : 1), _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR, IntPtr.Zero);
- }
+ }
+
+ public IPopupPositioner PopupPositioner { get; }
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
index 5e2ba51caf..2dc112f3d3 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
@@ -59,7 +59,9 @@ namespace Avalonia.LinuxFramebuffer
public Size ClientSize => ScaledSize;
public IMouseDevice MouseDevice => new MouseDevice();
- public double Scaling => 1;
+ public IPopupImpl CreatePopup() => null;
+
+ public double Scaling => _outputBackend.Scaling;
public IEnumerable Surfaces => new object[] {_outputBackend};
public Action Input { get; set; }
public Action Paint { get; set; }
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
index 2cc1f65202..8fc555aac2 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
@@ -107,11 +107,12 @@ namespace Avalonia.LinuxFramebuffer
public static class LinuxFramebufferPlatformExtensions
{
- public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null)
- where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev));
+ public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1)
+ where T : AppBuilderBase, new() =>
+ StartLinuxDirect(builder, args, new FbdevOutput(fbdev) {Scaling = scaling});
- public static int StartLinuxDrm(this T builder, string[] args, string card = null)
- where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card));
+ public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1)
+ where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling});
public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend)
where T : AppBuilderBase, new()
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
index 6a76977352..273265a6dc 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
@@ -14,7 +14,7 @@ namespace Avalonia.LinuxFramebuffer.Output
private DrmCard _card;
private readonly EglGlPlatformSurface _eglPlatformSurface;
public PixelSize PixelSize => _mode.Resolution;
-
+ public double Scaling { get; set; }
public DrmOutput(string path = null)
{
var card = new DrmCard(path);
@@ -233,7 +233,7 @@ namespace Avalonia.LinuxFramebuffer.Output
public PixelSize Size => _parent._mode.Resolution;
- public double Scaling => 1;
+ public double Scaling => _parent.Scaling;
}
public IGlPlatformSurfaceRenderingSession BeginDraw()
@@ -241,6 +241,8 @@ namespace Avalonia.LinuxFramebuffer.Output
_parent._deferredContext.MakeCurrent(_parent._eglSurface);
return new RenderSession(_parent);
}
+
+
}
IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext;
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
index 3021c29015..b83fe6cbe8 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
@@ -9,16 +9,15 @@ namespace Avalonia.LinuxFramebuffer
{
public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend
{
- private readonly Vector _dpi;
private int _fd;
private fb_fix_screeninfo _fixedInfo;
private fb_var_screeninfo _varInfo;
private IntPtr _mappedLength;
private IntPtr _mappedAddress;
+ public double Scaling { get; set; }
- public FbdevOutput(string fileName = null, Vector? dpi = null)
+ public FbdevOutput(string fileName = null)
{
- _dpi = dpi ?? new Vector(96, 96);
fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
_fd = NativeUnsafeMethods.open(fileName, 2, 0);
if (_fd <= 0)
@@ -101,7 +100,7 @@ namespace Avalonia.LinuxFramebuffer
{
if (_fd <= 0)
throw new ObjectDisposedException("LinuxFramebuffer");
- return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, _dpi);
+ return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, new Vector(96, 96) * Scaling);
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs
index 01690f07ac..17a39b0219 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs
@@ -3,5 +3,6 @@ namespace Avalonia.LinuxFramebuffer.Output
public interface IOutputBackend
{
PixelSize PixelSize { get; }
+ double Scaling { get; set; }
}
}
diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs
index a7c1d0a38b..61ccf09e52 100644
--- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs
+++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs
@@ -26,51 +26,64 @@ namespace Avalonia.Skia
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
var session = _surface.BeginDraw();
- var disp = session.Display;
- var gl = disp.GlInterface;
- gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb);
-
- var size = session.Size;
- var scaling = session.Scaling;
- if (size.Width <= 0 || size.Height <= 0 || scaling < 0)
- {
- throw new InvalidOperationException(
- $"Can't create drawing context for surface with {size} size and {scaling} scaling");
- }
-
- gl.Viewport(0, 0, size.Width, size.Height);
- gl.ClearStencil(0);
- gl.ClearColor(0, 0, 0, 0);
- gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- lock (_grContext)
+ bool success = false;
+ try
{
- _grContext.ResetContext();
-
- GRBackendRenderTarget renderTarget =
- new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize,
- new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat()));
- var surface = SKSurface.Create(_grContext, renderTarget,
- GRSurfaceOrigin.BottomLeft,
- GRPixelConfig.Rgba8888.ToColorType());
+ var disp = session.Display;
+ var gl = disp.GlInterface;
+ gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb);
- var nfo = new DrawingContextImpl.CreateInfo
+ var size = session.Size;
+ var scaling = session.Scaling;
+ if (size.Width <= 0 || size.Height <= 0 || scaling < 0)
{
- GrContext = _grContext,
- Canvas = surface.Canvas,
- Dpi = SkiaPlatform.DefaultDpi * scaling,
- VisualBrushRenderer = visualBrushRenderer,
- DisableTextLcdRendering = true
- };
+ session.Dispose();
+ throw new InvalidOperationException(
+ $"Can't create drawing context for surface with {size} size and {scaling} scaling");
+ }
- return new DrawingContextImpl(nfo, Disposable.Create(() =>
+ gl.Viewport(0, 0, size.Width, size.Height);
+ gl.ClearStencil(0);
+ gl.ClearColor(0, 0, 0, 0);
+ gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ lock (_grContext)
{
+ _grContext.ResetContext();
+
+ GRBackendRenderTarget renderTarget =
+ new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize,
+ new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat()));
+ var surface = SKSurface.Create(_grContext, renderTarget,
+ GRSurfaceOrigin.BottomLeft,
+ GRPixelConfig.Rgba8888.ToColorType());
+
+ var nfo = new DrawingContextImpl.CreateInfo
+ {
+ GrContext = _grContext,
+ Canvas = surface.Canvas,
+ Dpi = SkiaPlatform.DefaultDpi * scaling,
+ VisualBrushRenderer = visualBrushRenderer,
+ DisableTextLcdRendering = true
+ };
+
- surface.Canvas.Flush();
- surface.Dispose();
- renderTarget.Dispose();
- _grContext.Flush();
+ var ctx = new DrawingContextImpl(nfo, Disposable.Create(() =>
+ {
+
+ surface.Canvas.Flush();
+ surface.Dispose();
+ renderTarget.Dispose();
+ _grContext.Flush();
+ session.Dispose();
+ }));
+ success = true;
+ return ctx;
+ }
+ }
+ finally
+ {
+ if(!success)
session.Dispose();
- }));
}
}
}
diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
index c89d0a15cf..f698266610 100644
--- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
+++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
@@ -240,5 +240,7 @@ namespace Avalonia.Win32.Interop.Wpf
return new Vector(1, 1);
return new Vector(src.TransformToDevice.M11, src.TransformToDevice.M22);
}
+
+ public IPopupImpl CreatePopup() => null;
}
}
diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs
index 39f1a95466..c9aa1ce4e7 100644
--- a/src/Windows/Avalonia.Win32/PopupImpl.cs
+++ b/src/Windows/Avalonia.Win32/PopupImpl.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Platform;
using Avalonia.Win32.Interop;
@@ -57,5 +58,19 @@ namespace Avalonia.Win32
return base.WndProc(hWnd, msg, wParam, lParam);
}
}
+
+ public PopupImpl(IWindowBaseImpl parent)
+ {
+ PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize));
+ }
+
+ private void MoveResize(PixelPoint position, Size size, double scaling)
+ {
+ Move(position);
+ Resize(size);
+ //TODO: We ignore the scaling override for now
+ }
+
+ public IPopupPositioner PopupPositioner { get; }
}
}
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index c45bf6389e..f20cf394bb 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -41,6 +41,7 @@ namespace Avalonia
public bool UseDeferredRendering { get; set; } = true;
public bool AllowEglInitialization { get; set; }
public bool? EnableMultitouch { get; set; }
+ public bool OverlayPopups { get; set; }
}
}
@@ -61,6 +62,7 @@ namespace Avalonia.Win32
}
public static bool UseDeferredRendering => Options.UseDeferredRendering;
+ internal static bool UseOverlayPopups => Options.OverlayPopups;
public static Win32PlatformOptions Options { get; private set; }
public Size DoubleClickSize => new Size(
@@ -210,11 +212,6 @@ namespace Avalonia.Win32
return embedded;
}
- public IPopupImpl CreatePopup()
- {
- return new PopupImpl();
- }
-
public IWindowIconImpl LoadIcon(string fileName)
{
using (var stream = File.OpenRead(fileName))
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 2f7805884d..e33e1f11dc 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -131,6 +131,8 @@ namespace Avalonia.Win32
}
}
+ public void Move(PixelPoint point) => Position = point;
+
public void SetMinMaxSize(Size minSize, Size maxSize)
{
_minSize = minSize;
@@ -248,10 +250,7 @@ namespace Avalonia.Win32
UnmanagedMethods.SetActiveWindow(_hwnd);
}
- public IPopupImpl CreatePopup()
- {
- return new PopupImpl();
- }
+ public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this);
public void Dispose()
{
diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs
index 15e8b35056..d5f456409f 100644
--- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs
+++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs
@@ -134,5 +134,7 @@ namespace Avalonia.iOS
}
public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this);
+
+ public IPopupImpl CreatePopup() => null;
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
index 015a122677..ef7dc33f76 100644
--- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
@@ -982,6 +982,8 @@ namespace Avalonia.Controls.UnitTests
AutoCompleteBox control = CreateControl();
control.Items = CreateSimpleStringArray();
TextBox textBox = GetTextBox(control);
+ var window = new Window {Content = control};
+ window.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
test.Invoke(control, textBox);
}
@@ -1027,7 +1029,8 @@ namespace Avalonia.Controls.UnitTests
var popup =
new Popup
{
- Name = "PART_Popup"
+ Name = "PART_Popup",
+ PlacementTarget = control
}.RegisterInNameScope(scope);
var panel = new Panel();
diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
index 58d205deaa..522afc9546 100644
--- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
@@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests
ContextMenu = sut
};
- new Window { Content = target };
+ new Window { Content = target }.ApplyTemplate();
int openedCount = 0;
@@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests
openedCount++;
};
- sut.Open(null);
+ sut.Open(target);
Assert.Equal(1, openedCount);
}
@@ -53,9 +53,9 @@ namespace Avalonia.Controls.UnitTests
ContextMenu = sut
};
- new Window { Content = target };
+ new Window { Content = target }.ApplyTemplate();
- sut.Open(null);
+ sut.Open(target);
int closedCount = 0;
@@ -84,7 +84,8 @@ namespace Avalonia.Controls.UnitTests
ContextMenu = sut
};
- new Window { Content = target };
+ var window = new Window {Content = target};
+ window.ApplyTemplate();
_mouse.Click(target, MouseButton.Right);
@@ -112,7 +113,8 @@ namespace Avalonia.Controls.UnitTests
ContextMenu = sut
};
- var window = new Window { Content = target };
+ var window = new Window {Content = target};
+ window.ApplyTemplate();
_mouse.Click(target, MouseButton.Right);
@@ -151,7 +153,7 @@ namespace Avalonia.Controls.UnitTests
}
}
- [Fact]
+ [Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")]
public void Cancelling_Closing_Leaves_ContextMenuOpen()
{
using (Application())
@@ -165,7 +167,9 @@ namespace Avalonia.Controls.UnitTests
{
ContextMenu = sut
};
- new Window { Content = target };
+
+ var window = new Window {Content = target};
+ window.ApplyTemplate();
sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; };
@@ -190,12 +194,12 @@ namespace Avalonia.Controls.UnitTests
screenImpl.Setup(x => x.ScreenCount).Returns(1);
screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(screen, screen, true) });
- var windowImpl = new Mock();
- windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
-
- popupImpl = new Mock();
+ popupImpl = MockWindowingPlatform.CreatePopupMock();
popupImpl.SetupGet(x => x.Scaling).Returns(1);
+ var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object);
+ windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
+
var services = TestServices.StyledWindow.With(
inputManager: new InputManager(),
windowImpl: windowImpl.Object,
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
index 7d05547799..952180d21b 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
@@ -281,6 +281,37 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Content = 42;
}
+ [Fact]
+ public void Should_Set_InheritanceParent_Even_When_LogicalParent_Is_Already_Set()
+ {
+ var logicalParent = new Canvas();
+ var child = new TextBlock();
+ var (target, host) = CreateTarget();
+
+ ((ISetLogicalParent)child).SetParent(logicalParent);
+ target.Content = child;
+
+ Assert.Same(logicalParent, child.Parent);
+
+ // InheritanceParent is exposed via StylingParent.
+ Assert.Same(target, ((IStyledElement)child).StylingParent);
+ }
+
+ [Fact]
+ public void Should_Reset_InheritanceParent_When_Child_Removed()
+ {
+ var logicalParent = new Canvas();
+ var child = new TextBlock();
+ var (target, _) = CreateTarget();
+
+ ((ISetLogicalParent)child).SetParent(logicalParent);
+ target.Content = child;
+ target.Content = null;
+
+ // InheritanceParent is exposed via StylingParent.
+ Assert.Same(logicalParent, ((IStyledElement)child).StylingParent);
+ }
+
(ContentPresenter presenter, ContentControl templatedParent) CreateTarget()
{
var templatedParent = new ContentControl
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
index 059146f17d..0ebe6833d3 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
@@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var target = CreateTarget();
+ var target = CreateTarget(new Window());
Assert.True(((ILogical)target).IsAttachedToLogicalTree);
}
@@ -32,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var target = CreateTarget();
+ var target = CreateTarget(new Window());
Assert.True(target.Presenter.IsAttachedToLogicalTree);
}
@@ -43,28 +43,70 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
+ var window = new Window();
var target = new TemplatedControlWithPopup
{
PopupContent = new Canvas(),
};
+ window.Content = target;
- var root = new TestRoot { Child = target };
-
+ window.ApplyTemplate();
target.ApplyTemplate();
target.Popup.Open();
- Assert.Equal(target.Popup, ((IStyleHost)target.Popup.PopupRoot).StylingParent);
+ Assert.Equal(target.Popup, ((IStyleHost)target.Popup.Host).StylingParent);
}
}
+ [Fact]
+ public void PopupRoot_Should_Have_Template_Applied()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var window = new Window();
+ var target = new Popup {PlacementMode = PlacementMode.Pointer};
+ var child = new Control();
+
+ window.Content = target;
+ window.ApplyTemplate();
+ target.Open();
+
+
+ Assert.Single(((Visual)target.Host).GetVisualChildren());
+
+ var templatedChild = ((Visual)target.Host).GetVisualChildren().Single();
+
+ Assert.IsType(templatedChild);
+ var contentPresenter = templatedChild.VisualChildren.Single();
+ Assert.IsType(contentPresenter);
+
+
+ Assert.Equal((PopupRoot)target.Host, ((IControl)templatedChild).TemplatedParent);
+ Assert.Equal((PopupRoot)target.Host, ((IControl)contentPresenter).TemplatedParent);
+ }
+ }
+
+ [Fact]
+ public void PopupRoot_Should_Have_Null_VisualParent()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new Popup() {PlacementTarget = new Window()};
+
+ target.Open();
+
+ Assert.Null(((Visual)target.Host).GetVisualParent());
+ }
+ }
+
[Fact]
public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Decorator();
- var target = CreateTarget();
var window = new Window();
+ var target = CreateTarget(window);
var detachedCount = 0;
var attachedCount = 0;
@@ -88,8 +130,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Decorator();
- var target = CreateTarget();
var window = new Window();
+ var target = CreateTarget(window);
var detachedCount = 0;
var attachedCount = 0;
@@ -117,22 +159,23 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
+ var window = new Window();
var target = new TemplatedControlWithPopup
{
PopupContent = new Canvas(),
};
+ window.Content = target;
- var root = new TestRoot { Child = target };
-
+ window.ApplyTemplate();
target.ApplyTemplate();
target.Popup.Open();
target.PopupContent = null;
}
}
- private PopupRoot CreateTarget()
+ private PopupRoot CreateTarget(TopLevel popupParent)
{
- var result = new PopupRoot
+ var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup())
{
Template = new FuncControlTemplate((parent, scope) =>
new ContentPresenter
@@ -158,6 +201,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
new Popup
{
[!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty],
+ PlacementTarget = parent
});
}
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
index 2e22725125..7cb9fccee8 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
@@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
public class PopupTests
{
+ protected bool UsePopupHost;
+
[Fact]
public void Setting_Child_Should_Set_Child_Controls_LogicalParent()
{
@@ -137,20 +139,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
var target = new Popup();
- Assert.Null(target.PopupRoot);
- }
- }
-
- [Fact]
- public void PopupRoot_Should_Have_Null_VisualParent()
- {
- using (CreateServices())
- {
- var target = new Popup();
-
- target.Open();
-
- Assert.Null(target.PopupRoot.GetVisualParent());
+ Assert.Null(((Visual)target.Host));
}
}
@@ -159,12 +148,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (CreateServices())
{
- var target = new Popup();
+ var target = new Popup() {PlacementTarget = PreparedWindow()};
target.Open();
- Assert.Equal(target, target.PopupRoot.Parent);
- Assert.Equal(target, target.PopupRoot.GetLogicalParent());
+ Assert.Equal(target, ((Visual)target.Host).Parent);
+ Assert.Equal(target, ((Visual)target.Host).GetLogicalParent());
}
}
@@ -173,15 +162,15 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (CreateServices())
{
- var target = new Popup();
- var root = new TestRoot { Child = target };
+ var target = new Popup() {PlacementMode = PlacementMode.Pointer};
+ var root = PreparedWindow(target);
target.Open();
- var popupRoot = (ILogical)target.PopupRoot;
+ var popupRoot = (ILogical)((Visual)target.Host);
Assert.True(popupRoot.IsAttachedToLogicalTree);
- root.Child = null;
+ root.Content = null;
Assert.False(((ILogical)target).IsAttachedToLogicalTree);
}
}
@@ -191,8 +180,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (CreateServices())
{
- var window = new Window();
- var target = new Popup();
+ var window = PreparedWindow();
+ var target = new Popup() {PlacementMode = PlacementMode.Pointer};
window.Content = target;
@@ -214,10 +203,11 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (CreateServices())
{
- var window = new Window();
- var target = new Popup();
+ var window = PreparedWindow();
+ var target = new Popup() {PlacementMode = PlacementMode.Pointer};
window.Content = target;
+ window.ApplyTemplate();
target.Open();
int closedCount = 0;
@@ -233,46 +223,28 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
- [Fact]
- public void PopupRoot_Should_Have_Template_Applied()
- {
- using (CreateServices())
- {
- var window = new Window();
- var target = new Popup();
- var child = new Control();
-
- window.Content = target;
- target.Open();
-
- Assert.Single(target.PopupRoot.GetVisualChildren());
-
- var templatedChild = target.PopupRoot.GetVisualChildren().Single();
- Assert.IsType(templatedChild);
- Assert.Equal(target.PopupRoot, ((IControl)templatedChild).TemplatedParent);
- }
- }
-
+
[Fact]
public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent()
{
using (CreateServices())
{
PopupContentControl target;
- var root = new TestRoot
+ var root = PreparedWindow(target = new PopupContentControl
{
- Child = target = new PopupContentControl
- {
- Content = new Border(),
- Template = new FuncControlTemplate(PopupContentControlTemplate),
- },
- StylingParent = AvaloniaLocator.Current.GetService()
- };
+ Content = new Border(),
+ Template = new FuncControlTemplate(PopupContentControlTemplate),
+ });
+ root.Show();
target.ApplyTemplate();
+
var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup");
popup.Open();
- var popupRoot = popup.PopupRoot;
+
+ var popupRoot = (Control)popup.Host;
+ popupRoot.Measure(Size.Infinity);
+ popupRoot.Arrange(new Rect(popupRoot.DesiredSize));
var children = popupRoot.GetVisualDescendants().ToList();
var types = children.Select(x => x.GetType().Name).ToList();
@@ -280,6 +252,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(
new[]
{
+ "VisualLayerManager",
"ContentPresenter",
"ContentPresenter",
"Border",
@@ -293,6 +266,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(
new object[]
{
+ popupRoot,
popupRoot,
target,
null,
@@ -301,6 +275,13 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
+ Window PreparedWindow(object content = null)
+ {
+ var w = new Window {Content = content};
+ w.ApplyTemplate();
+ return w;
+ }
+
[Fact]
public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit()
{
@@ -311,6 +292,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Child = child = new TestControl(),
DataContext = "foo",
+ PlacementTarget = PreparedWindow()
};
var beginCalled = false;
@@ -330,46 +312,32 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.False(beginCalled);
}
}
-
-
- private static IDisposable CreateServices()
+
+ [Fact]
+ public void Popup_Host_Type_Should_Match_Platform_Preference()
{
- var result = AvaloniaLocator.EnterScope();
-
- var styles = new Styles
+ using (CreateServices())
{
- new Style(x => x.OfType())
- {
- Setters = new[]
- {
- new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate(PopupRootTemplate)),
- }
- },
- };
-
- var globalStyles = new Mock();
- globalStyles.Setup(x => x.IsStylesInitialized).Returns(true);
- globalStyles.Setup(x => x.Styles).Returns(styles);
-
- var renderInterface = new Mock();
-
- AvaloniaLocator.CurrentMutable
- .Bind().ToFunc(() => globalStyles.Object)
- .Bind().ToConstant(new WindowingPlatformMock())
- .Bind().ToTransient()
- .Bind().ToFunc(() => renderInterface.Object)
- .Bind().ToConstant(new InputManager());
-
- return result;
+ var target = new Popup() {PlacementTarget = PreparedWindow()};
+
+ target.Open();
+ if (UsePopupHost)
+ Assert.IsType(target.Host);
+ else
+ Assert.IsType(target.Host);
+ }
}
- private static IControl PopupRootTemplate(PopupRoot control, INameScope scope)
+ private IDisposable CreateServices()
{
- return new ContentPresenter
- {
- Name = "PART_ContentPresenter",
- [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty],
- }.RegisterInNameScope(scope);
+ return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
+ new MockWindowingPlatform(null,
+ () =>
+ {
+ if(UsePopupHost)
+ return null;
+ return MockWindowingPlatform.CreatePopupMock().Object;
+ })));
}
private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope)
@@ -377,6 +345,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
return new Popup
{
Name = "popup",
+ PlacementTarget = control,
Child = new ContentPresenter
{
[~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty],
@@ -401,4 +370,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
}
+
+ public class PopupTestsWithPopupRoot : PopupTests
+ {
+ public PopupTestsWithPopupRoot()
+ {
+ UsePopupHost = true;
+ }
+ }
}
diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
index 3ee6a50e69..55e8ae0115 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
@@ -20,33 +20,6 @@ namespace Avalonia.Controls.UnitTests
{
public class WindowBaseTests
{
- [Fact]
- public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
- {
- var impl = Mock.Of(x => x.Scaling == 1);
-
- Mock.Get(impl).Setup(x => x.Resize(It.IsAny())).Callback(() => { });
-
- var target = new TestWindowBase(impl)
- {
- Template = CreateTemplate(),
- Content = new TextBlock
- {
- Width = 321,
- Height = 432,
- },
- IsVisible = true,
- };
-
- target.LayoutManager.ExecuteInitialLayoutPass(target);
-
- Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432)));
- }
- }
-
-
[Fact]
public void Activate_Should_Call_Impl_Activate()
{
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index f4d9a91d0c..75239f014f 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -277,8 +277,7 @@ namespace Avalonia.Controls.UnitTests
var screens = new Mock();
screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object });
- var windowImpl = new Mock();
- windowImpl.SetupProperty(x => x.Position);
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
windowImpl.Setup(x => x.Scaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(screens.Object);
@@ -302,14 +301,12 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner()
{
- var parentWindowImpl = new Mock();
- parentWindowImpl.SetupProperty(x => x.Position);
+ var parentWindowImpl = MockWindowingPlatform.CreateWindowMock();
parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
parentWindowImpl.Setup(x => x.Scaling).Returns(1);
- var windowImpl = new Mock();
- windowImpl.SetupProperty(x => x.Position);
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200));
windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
windowImpl.Setup(x => x.Scaling).Returns(1);
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs
index 6ffaaaee5c..b424003ed6 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs
@@ -3,7 +3,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Converters
{
- public class ConverterTests
+ public class ConverterTests : XamlTestBase
{
[Fact]
public void Bug_2228_Relative_Uris_Should_Be_Correctly_Parsed()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
index bb44d069b5..cdd40ed80f 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
@@ -11,7 +11,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public Orientation? Orientation { get; set; }
}
- public class NullableConverterTests
+ public class NullableConverterTests : XamlTestBase
{
[Fact]
public void Nullable_Types_Should_Still_Be_Converted_Properly()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs
index 6f2c4363e2..5e698117c3 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs
@@ -8,7 +8,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Converters
{
- public class ValueConverterTests
+ public class ValueConverterTests : XamlTestBase
{
[Fact]
public void ValueConverter_Special_Values_Work()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
index e412657711..5972920af3 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
@@ -8,7 +8,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
- public class BindingTests
+ public class BindingTests : XamlTestBase
{
[Fact]
public void Binding_With_Null_Path_Works()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs
index 0d96df8eb8..db45f1989b 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs
@@ -10,7 +10,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
- public class BindingTests_Method
+ public class BindingTests_Method : XamlTestBase
{
[Fact]
public void Binding_Method_To_Command_Works()
@@ -102,4 +102,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
public string Value { get; private set; } = "Not called";
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs
index a9bea01fde..86ca351d67 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs
@@ -10,7 +10,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
- public class BindingTests_TemplatedParent
+ public class BindingTests_TemplatedParent : XamlTestBase
{
[Fact]
public void TemplateBinding_With_Null_Path_Works()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
index dcecfe3b22..c3bc649abb 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Text;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Styling;
using Avalonia.UnitTests;
@@ -10,7 +11,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{
- public class BindingExtensionTests
+ public class BindingExtensionTests : XamlTestBase
{
[Fact]
@@ -59,11 +60,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
new Setter(
Window.TemplateProperty,
new FuncControlTemplate((x, scope) =>
- new ContentPresenter
+ new VisualLayerManager
{
- Name = "PART_ContentPresenter",
- [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
- }.RegisterInNameScope(scope)))
+ Child =
+ new ContentPresenter
+ {
+ Name = "PART_ContentPresenter",
+ [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
+ }.RegisterInNameScope(scope)
+ }))
}
};
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
index ed70cd6fe8..96955539c1 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
@@ -15,7 +15,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{
- public class DynamicResourceExtensionTests
+ public class DynamicResourceExtensionTests : XamlTestBase
{
[Fact]
public void DynamicResource_Can_Be_Assigned_To_Property()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
index a35c7bdd9b..7ab6c2de40 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MakrupExtensions
{
public class ResourceIncludeTests
{
- public class StaticResourceExtensionTests
+ public class StaticResourceExtensionTests : XamlTestBase
{
[Fact]
public void ResourceInclude_Loads_ResourceDictionary()
@@ -52,4 +52,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.MakrupExtensions
}
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
index 7a96b9f989..58985af0ad 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
@@ -14,7 +14,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{
- public class StaticResourceExtensionTests
+ public class StaticResourceExtensionTests : XamlTestBase
{
[Fact]
public void StaticResource_Can_Be_Assigned_To_Property()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs
index f4c3302d52..2dc6c4a7fb 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs
@@ -12,7 +12,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests
{
- public class StyleTests
+ public class StyleTests : XamlTestBase
{
[Fact]
public void Binding_Should_Be_Assigned_To_Setter_Value_Instead_Of_Bound()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
index d74eed992e..f4d4a9dd2a 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
@@ -22,7 +22,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
- public class BasicTests
+ public class BasicTests : XamlTestBase
{
[Fact]
public void Simple_Property_Is_Set()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
index 3930608515..7281542bc1 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
@@ -8,7 +8,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
- public class BindingTests
+ public class BindingTests : XamlTestBase
{
[Fact]
public void Binding_To_DataContext_Works()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs
index c6fe79bc0c..86b874f75c 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs
@@ -8,7 +8,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
- public class BindingTests_RelativeSource
+ public class BindingTests_RelativeSource : XamlTestBase
{
[Fact]
public void Binding_To_DataContext_Works()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
index bd9d99ff23..0850f3fa78 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
@@ -4,14 +4,13 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
-using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
- public class ControlBindingTests
+ public class ControlBindingTests : XamlTestBase
{
[Fact]
public void Binding_ProgressBar_Value_To_Invalid_Value_Uses_FallbackValue()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
index 6b67303b07..4f2886582d 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
@@ -8,7 +8,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
- public class DataTemplateTests
+ public class DataTemplateTests : XamlTestBase
{
[Fact]
public void DataTemplate_Can_Contain_Name()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs
index 44697f5937..dcb6533b5e 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs
@@ -9,7 +9,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
- public class EventTests
+ public class EventTests : XamlTestBase
{
[Fact]
public void Event_Is_Attached()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
index 8dd1d24dd6..b76022852c 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
@@ -12,7 +12,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
- public class StyleTests
+ public class StyleTests : XamlTestBase
{
[Fact]
public void Color_Can_Be_Added_To_Style_Resources()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs
index 4134f5be23..f5fed02899 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs
@@ -4,14 +4,13 @@
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Data;
-using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
- public class TreeDataTemplateTests
+ public class TreeDataTemplateTests : XamlTestBase
{
[Fact]
public void Binding_Should_Be_Assigned_To_ItemsSource_Instead_Of_Bound()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs
index 1f135f8e76..4ff9e3db38 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs
@@ -5,10 +5,7 @@ using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
-using Avalonia.Controls.Presenters;
using Avalonia.Data.Converters;
-using Avalonia.Input;
-using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.UnitTests;
@@ -18,7 +15,7 @@ using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests
{
- public class XamlIlTests
+ public class XamlIlTests : XamlTestBase
{
[Fact]
public void Binding_Button_IsPressed_ShouldWork()
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs b/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs
new file mode 100644
index 0000000000..5172b2e830
--- /dev/null
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Xaml.UnitTests
+{
+ public class XamlTestBase
+ {
+ public XamlTestBase()
+ {
+ // Ensure necessary assemblies are loaded.
+ var _ = typeof(TemplateBinding);
+ }
+ }
+}
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs
index 876f37cc9e..56b14c3936 100644
--- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs
+++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs
@@ -60,6 +60,28 @@ namespace Avalonia.ReactiveUI.UnitTests
}
}
+ [Fact]
+ public void AutoSuspendHelper_Should_Throw_When_Not_Supported_Lifetime_Is_Used()
+ {
+ using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
+ using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents())
+ {
+ var application = AvaloniaLocator.Current.GetService();
+ application.ApplicationLifetime = lifetime;
+ Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime));
+ }
+ }
+
+ [Fact]
+ public void AutoSuspendHelper_Should_Throw_When_Lifetime_Is_Null()
+ {
+ using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
+ {
+ var application = AvaloniaLocator.Current.GetService();
+ Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime));
+ }
+ }
+
[Fact]
public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized()
{
@@ -82,17 +104,5 @@ namespace Avalonia.ReactiveUI.UnitTests
Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState().Example);
}
}
-
- [Fact]
- public void AutoSuspendHelper_Should_Throw_For_Not_Supported_Lifetimes()
- {
- using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
- using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents())
- {
- var application = AvaloniaLocator.Current.GetService();
- application.ApplicationLifetime = lifetime;
- Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime));
- }
- }
}
}
\ No newline at end of file
diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
index ae901ca2f2..272b1fc489 100644
--- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
+++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
@@ -5,6 +5,7 @@
false
Library
false
+ latest
diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
index 36297bf58b..c33ec72141 100644
--- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
+++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
@@ -1,4 +1,6 @@
using System;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Input;
using Moq;
using Avalonia.Platform;
@@ -15,16 +17,48 @@ namespace Avalonia.UnitTests
_popupImpl = popupImpl;
}
+ public static Mock CreateWindowMock(Func popupImpl = null)
+ {
+ var win = Mock.Of(x => x.Scaling == 1);
+ var mock = Mock.Get(win);
+ mock.Setup(x => x.CreatePopup()).Returns(() =>
+ {
+ if (popupImpl != null)
+ return popupImpl();
+ return CreatePopupMock().Object;
+
+ });
+ PixelPoint pos = default;
+ mock.SetupGet(x => x.Position).Returns(() => pos);
+ mock.Setup(x => x.Move(It.IsAny())).Callback(new Action(np => pos = np));
+ SetupToplevel(mock);
+ return mock;
+ }
+
+ static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl
+ {
+ mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
+ }
+
+ public static Mock CreatePopupMock()
+ {
+ var positioner = Mock.Of();
+ var popup = Mock.Of(x => x.Scaling == 1);
+ var mock = Mock.Get(popup);
+ mock.SetupGet(x => x.PopupPositioner).Returns(positioner);
+ SetupToplevel(mock);
+
+ return mock;
+ }
+
public IWindowImpl CreateWindow()
{
- return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1);
+ return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object;
}
public IEmbeddableWindowImpl CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
-
- public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1);
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs
index 1101ccacba..24ba2d1c48 100644
--- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs
@@ -92,5 +92,14 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Assert.Same(node1.DrawOperations[0].Item, node2.DrawOperations[0].Item);
Assert.NotSame(node1.DrawOperations[0], node2.DrawOperations[0]);
}
+
+ [Fact]
+ public void SortChildren_Does_Not_Throw_On_Null_Children()
+ {
+ var node = new VisualNode(Mock.Of(), null);
+ var scene = new Scene(Mock.Of());
+
+ node.SortChildren(scene);
+ }
}
}