diff --git a/.editorconfig b/.editorconfig
index 5f08d1e940..f6bce9cb76 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
+space_within_single_line_array_initializer_braces = true
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false
# Xaml files
[*.xaml]
-indent_size = 4
+indent_size = 2
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 7e3532ee23..92e4afdca8 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -134,3 +134,4 @@ jobs:
pathToPublish: '$(Build.SourcesDirectory)/artifacts/zip'
artifactName: 'Samples'
condition: succeeded()
+
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index cf8e0fd13a..c03ad0fefd 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
+
diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm
index b491cf2a92..b62fe8a968 100644
--- a/native/Avalonia.Native/src/OSX/AvnString.mm
+++ b/native/Avalonia.Native/src/OSX/AvnString.mm
@@ -11,14 +11,26 @@
class AvnStringImpl : public virtual ComSingleObject
{
private:
- NSString* _string;
+ int _length;
+ const char* _cstring;
public:
FORWARD_IUNKNOWN()
AvnStringImpl(NSString* string)
+ {
+ auto cstring = [string cStringUsingEncoding:NSUTF8StringEncoding];
+ _length = (int)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+
+ _cstring = (const char*)malloc(_length + 5);
+
+ memset((void*)_cstring, 0, _length + 5);
+ memcpy((void*)_cstring, (void*)cstring, _length);
+ }
+
+ virtual ~AvnStringImpl()
{
- _string = string;
+ free((void*)_cstring);
}
virtual HRESULT Pointer(void**retOut) override
@@ -30,7 +42,7 @@ public:
return E_POINTER;
}
- *retOut = (void*)_string.UTF8String;
+ *retOut = (void*)_cstring;
return S_OK;
}
@@ -43,7 +55,7 @@ public:
return E_POINTER;
}
- *retOut = (int)_string.length;
+ *retOut = _length;
return S_OK;
}
diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm
index 53c1fe3c2c..6e4d3ce668 100644
--- a/native/Avalonia.Native/src/OSX/clipboard.mm
+++ b/native/Avalonia.Native/src/OSX/clipboard.mm
@@ -8,6 +8,13 @@ class Clipboard : public ComSingleObject
{
public:
FORWARD_IUNKNOWN()
+
+ Clipboard()
+ {
+ NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
+ [pasteBoard stringForType:NSPasteboardTypeString];
+ }
+
virtual HRESULT GetText (IAvnString**ppv) override
{
@autoreleasepool
@@ -39,7 +46,9 @@ public:
{
@autoreleasepool
{
- [[NSPasteboard generalPasteboard] clearContents];
+ NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
+ [pasteBoard clearContents];
+ [pasteBoard setString:@"" forType:NSPasteboardTypeString];
}
return S_OK;
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 40321496c0..09d2612ac3 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -1,9 +1,11 @@
using System;
using System.Diagnostics;
+using System.Globalization;
using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Skia;
using Avalonia.ReactiveUI;
@@ -27,10 +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"))
{
- System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer());
- return builder.StartLinuxFramebuffer(args);
+ SilenceConsole();
+ return builder.StartLinuxFbDev(args, scaling: GetScaling());
+ }
+ else if (args.Contains("--drm"))
+ {
+ SilenceConsole();
+ return builder.StartLinuxDrm(args, scaling: GetScaling());
}
else
return builder.StartWithClassicDesktopLifetime(args);
@@ -51,11 +67,14 @@ namespace ControlCatalog.NetCore
.UseSkia()
.UseReactiveUI();
- static void ConsoleSilencer()
+ static void SilenceConsole()
{
- Console.CursorVisible = false;
- while (true)
- Console.ReadKey(true);
+ new Thread(() =>
+ {
+ Console.CursorVisible = false;
+ while (true)
+ Console.ReadKey(true);
+ }) {IsBackground = true}.Start();
}
}
}
diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs
index 749f83c1ab..2e7218b956 100644
--- a/samples/ControlCatalog/DecoratedWindow.xaml.cs
+++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs
@@ -34,7 +34,7 @@ namespace ControlCatalog
SetupSide("Left", StandardCursorType.LeftSide, WindowEdge.West);
SetupSide("Right", StandardCursorType.RightSide, WindowEdge.East);
SetupSide("Top", StandardCursorType.TopSide, WindowEdge.North);
- SetupSide("Bottom", StandardCursorType.BottomSize, WindowEdge.South);
+ SetupSide("Bottom", StandardCursorType.BottomSide, WindowEdge.South);
SetupSide("TopLeft", StandardCursorType.TopLeftCorner, WindowEdge.NorthWest);
SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast);
SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest);
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 8699508320..c35f8a3c0c 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -6,10 +6,13 @@
Foreground="{DynamicResource ThemeForegroundBrush}"
FontSize="{DynamicResource FontSizeNormal}">
-
- Light
- Dark
-
+
+
+
@@ -21,11 +24,17 @@
-
+
+
+
+
+
@@ -41,6 +50,12 @@
+
+
+ Light
+ Dark
+
+
diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml
index 6a9e865e26..9527ac3b4e 100644
--- a/samples/ControlCatalog/MainWindow.xaml
+++ b/samples/ControlCatalog/MainWindow.xaml
@@ -1,4 +1,5 @@
+
+
+ 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
new file mode 100644
index 0000000000..1a607342f3
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
@@ -0,0 +1,81 @@
+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
+{
+ public class ItemsRepeaterPage : UserControl
+ {
+ private ItemsRepeater _repeater;
+ private ScrollViewer _scroller;
+
+ public ItemsRepeaterPage()
+ {
+ this.InitializeComponent();
+ _repeater = this.FindControl("repeater");
+ _scroller = this.FindControl("scroller");
+ _repeater.PointerPressed += RepeaterClick;
+ DataContext = new ItemsRepeaterPageViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void LayoutChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_repeater == null)
+ {
+ return;
+ }
+
+ var comboBox = (ComboBox)sender;
+
+ switch (comboBox.SelectedIndex)
+ {
+ case 0:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _repeater.Layout = new StackLayout { Orientation = Orientation.Vertical };
+ break;
+ case 1:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal };
+ break;
+ case 2:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _repeater.Layout = new UniformGridLayout
+ {
+ Orientation = Orientation.Vertical,
+ MinItemWidth = 200,
+ MinItemHeight = 200,
+ };
+ break;
+ case 3:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _repeater.Layout = new UniformGridLayout
+ {
+ Orientation = Orientation.Horizontal,
+ MinItemWidth = 200,
+ MinItemHeight = 200,
+ };
+ 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/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs
index a1359519e6..60e946dfbe 100644
--- a/samples/ControlCatalog/Pages/PointersPage.cs
+++ b/samples/ControlCatalog/Pages/PointersPage.cs
@@ -69,16 +69,25 @@ namespace ControlCatalog.Pages
{
UpdatePointer(e);
e.Pointer.Capture(this);
+ e.Handled = true;
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
UpdatePointer(e);
+ e.Handled = true;
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ _pointers.Remove(e.Pointer);
+ e.Handled = true;
+ InvalidateVisual();
+ }
+
+ protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
_pointers.Remove(e.Pointer);
InvalidateVisual();
diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs
index b9b384e8fe..13c1667ed2 100644
--- a/samples/ControlCatalog/Pages/ScreenPage.cs
+++ b/samples/ControlCatalog/Pages/ScreenPage.cs
@@ -22,7 +22,10 @@ namespace ControlCatalog.Pages
public override void Render(DrawingContext context)
{
base.Render(context);
- Window w = (Window)VisualRoot;
+ if (!(VisualRoot is Window w))
+ {
+ return;
+ }
var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling;
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/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml
index 3513e94107..26d25a6266 100644
--- a/samples/ControlCatalog/SideBar.xaml
+++ b/samples/ControlCatalog/SideBar.xaml
@@ -24,7 +24,8 @@
Name="PART_ScrollViewer"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
- Background="{TemplateBinding Background}">
+ Background="{TemplateBinding Background}"
+ DockPanel.Dock="Left">
-
-
+
+
+
+
+
@@ -58,6 +64,8 @@
+
+
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs
index 8232697c18..395196d926 100644
--- a/src/Avalonia.Controls/Calendar/CalendarItem.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs
@@ -4,6 +4,7 @@
// All other rights reserved.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Data;
@@ -193,6 +194,9 @@ namespace Avalonia.Controls.Primitives
{
if (MonthView != null)
{
+ var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth;
+ var children = new List(childCount);
+
for (int i = 0; i < Calendar.RowsPerMonth; i++)
{
if (_dayTitleTemplate != null)
@@ -201,7 +205,7 @@ namespace Avalonia.Controls.Primitives
cell.DataContext = string.Empty;
cell.SetValue(Grid.RowProperty, 0);
cell.SetValue(Grid.ColumnProperty, i);
- MonthView.Children.Add(cell);
+ children.Add(cell);
}
}
@@ -222,13 +226,18 @@ namespace Avalonia.Controls.Primitives
cell.PointerEnter += Cell_MouseEnter;
cell.PointerLeave += Cell_MouseLeave;
cell.Click += Cell_Click;
- MonthView.Children.Add(cell);
+ children.Add(cell);
}
}
+
+ MonthView.Children.AddRange(children);
}
if (YearView != null)
{
+ var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear;
+ var children = new List(childCount);
+
CalendarButton month;
for (int i = 0; i < Calendar.RowsPerYear; i++)
{
@@ -246,9 +255,11 @@ namespace Avalonia.Controls.Primitives
month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp;
month.PointerEnter += Month_MouseEnter;
month.PointerLeave += Month_MouseLeave;
- YearView.Children.Add(month);
+ children.Add(month);
}
}
+
+ YearView.Children.AddRange(children);
}
}
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 58b4324a3e..a5025df82d 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
@@ -90,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/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs
index 304a760216..28b9b3a38f 100644
--- a/src/Avalonia.Controls/GridSplitter.cs
+++ b/src/Avalonia.Controls/GridSplitter.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs
new file mode 100644
index 0000000000..6b5cb2ee25
--- /dev/null
+++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs
@@ -0,0 +1,9 @@
+namespace Avalonia.Controls
+{
+ public interface IScrollAnchorProvider
+ {
+ IControl CurrentAnchor { get; }
+ void RegisterAnchorCandidate(IControl element);
+ void UnregisterAnchorCandidate(IControl element);
+ }
+}
diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs
index fa6f5787be..ff6cd482df 100644
--- a/src/Avalonia.Controls/Image.cs
+++ b/src/Avalonia.Controls/Image.cs
@@ -96,7 +96,7 @@ namespace Avalonia.Controls
}
}
- return result.Constrain(availableSize);
+ return result;
}
///
diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs
index 07372eb714..1430c39c76 100644
--- a/src/Avalonia.Controls/LayoutTransformControl.cs
+++ b/src/Avalonia.Controls/LayoutTransformControl.cs
@@ -45,7 +45,7 @@ namespace Avalonia.Controls
}
///
- /// Utilize the for layout transforms.
+ /// Utilize the for layout transforms.
///
public bool UseRenderTransform
{
diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs
index b60a97e1c8..6ec97aa04e 100644
--- a/src/Avalonia.Controls/Menu.cs
+++ b/src/Avalonia.Controls/Menu.cs
@@ -5,6 +5,7 @@ using Avalonia.Controls.Platform;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
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/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs
index 25b29e37e6..b826fb982e 100644
--- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs
+++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs
@@ -150,6 +150,7 @@ namespace Avalonia.Controls.Mixins
if (oldValue is IControl child)
{
logicalChildren.Remove(child);
+ ((ISetInheritanceParent)child).SetParent(child.Parent);
}
child = newValue as IControl;
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/Panel.cs b/src/Avalonia.Controls/Panel.cs
index 0f365fcb08..a4c674a03b 100644
--- a/src/Avalonia.Controls/Panel.cs
+++ b/src/Avalonia.Controls/Panel.cs
@@ -112,7 +112,7 @@ namespace Avalonia.Controls
case NotifyCollectionChangedAction.Add:
controls = e.NewItems.OfType().ToList();
LogicalChildren.InsertRange(e.NewStartingIndex, controls);
- VisualChildren.AddRange(e.NewItems.OfType());
+ VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType());
break;
case NotifyCollectionChangedAction.Move:
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/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
index 76f17332bf..85916bcdd0 100644
--- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs
+++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
@@ -33,9 +33,10 @@ namespace Avalonia.Platform
_dragDrop = AvaloniaLocator.Current.GetService();
}
- public async Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+ public async Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
{
Dispatcher.UIThread.VerifyAccess();
+ triggerEvent.Pointer.Capture(null);
if (_draggedData == null)
{
_draggedData = data;
diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
index bb357453ff..cb1291410a 100644
--- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
+++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
@@ -9,94 +9,69 @@ using Avalonia.Threading;
namespace Avalonia.Controls.Platform
{
- public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer
+ public class InternalPlatformThreadingInterface : IPlatformThreadingInterface
{
public InternalPlatformThreadingInterface()
{
TlsCurrentThreadIsLoopThread = true;
- StartTimer(
- DispatcherPriority.Render,
- new TimeSpan(0, 0, 0, 0, 66),
- () => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)));
}
private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
- private readonly AutoResetEvent _queued = new AutoResetEvent(false);
- private readonly Queue _actions = new Queue();
public void RunLoop(CancellationToken cancellationToken)
{
- var handles = new[] {_signaled, _queued};
while (true)
{
- if (0 == WaitHandle.WaitAny(handles))
- Signaled?.Invoke(null);
- else
- {
- while (true)
- {
- Action item;
- lock (_actions)
- if (_actions.Count == 0)
- break;
- else
- item = _actions.Dequeue();
- item();
- }
- }
+ Signaled?.Invoke(null);
+ _signaled.WaitOne();
}
}
- public void Send(Action cb)
- {
- lock (_actions)
- {
- _actions.Enqueue(cb);
- _queued.Set();
- }
- }
- class WatTimer : IDisposable
+ class TimerImpl : IDisposable
{
- private readonly IDisposable _timer;
+ private readonly DispatcherPriority _priority;
+ private readonly TimeSpan _interval;
+ private readonly Action _tick;
+ private Timer _timer;
private GCHandle _handle;
- public WatTimer(IDisposable timer)
+ public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick)
{
- _timer = timer;
+ _priority = priority;
+ _interval = interval;
+ _tick = tick;
+ _timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1));
_handle = GCHandle.Alloc(_timer);
}
+ private void OnTimer(object state)
+ {
+ if (_timer == null)
+ return;
+ Dispatcher.UIThread.Post(() =>
+ {
+
+ if (_timer == null)
+ return;
+ _tick();
+ _timer?.Change(_interval, TimeSpan.FromMilliseconds(-1));
+ });
+ }
+
+
public void Dispose()
{
_handle.Free();
_timer.Dispose();
+ _timer = null;
}
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
- return new WatTimer(new System.Threading.Timer(delegate
- {
- var tcs = new TaskCompletionSource();
- Send(() =>
- {
- try
- {
- tick();
- }
- finally
- {
- tcs.SetResult(0);
- }
- });
-
-
- tcs.Task.Wait();
- }, null, TimeSpan.Zero, interval));
-
-
+ return new TimerImpl(priority, interval, tick);
}
public void Signal(DispatcherPriority prio)
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..1072b21b1b 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -237,7 +237,7 @@ namespace Avalonia.Controls.Presenters
// template.
LogicalChildren.Remove(oldChild);
}
- else
+ else if (TemplatedParent != null)
{
// If we're in a ContentControl's template then invoke ChildChanging to let
// ContentControlMixin handle removing the logical child.
@@ -248,6 +248,10 @@ namespace Avalonia.Controls.Presenters
newChild,
BindingPriority.LocalValue));
}
+ else if (oldChild != null)
+ {
+ ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent);
+ }
}
// Set the DataContext if the data isn't a control.
@@ -433,6 +437,7 @@ namespace Avalonia.Controls.Presenters
{
VisualChildren.Remove(Child);
LogicalChildren.Remove(Child);
+ ((ISetInheritanceParent)Child).SetParent(Child.Parent);
Child = null;
_dataTemplate = null;
}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
index 46da8fe3f8..ae52e733b7 100644
--- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
@@ -8,6 +8,7 @@ using System.Reactive.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
+using Avalonia.Layout;
namespace Avalonia.Controls.Presenters
{
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index b3345ec101..debbb81264 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -49,6 +49,14 @@ namespace Avalonia.Controls.Presenters
AffectsRender(PasswordCharProperty,
SelectionBrushProperty, SelectionForegroundBrushProperty,
SelectionStartProperty, SelectionEndProperty);
+
+ Observable.Merge(
+ SelectionStartProperty.Changed,
+ SelectionEndProperty.Changed,
+ PasswordCharProperty.Changed
+ ).AddClassHandler((x,_) => x.InvalidateFormattedText());
+
+ CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue));
}
public TextPresenter()
@@ -56,17 +64,6 @@ namespace Avalonia.Controls.Presenters
_caretTimer = new DispatcherTimer();
_caretTimer.Interval = TimeSpan.FromMilliseconds(500);
_caretTimer.Tick += CaretTimerTick;
-
- Observable.Merge(
- this.GetObservable(SelectionStartProperty),
- this.GetObservable(SelectionEndProperty))
- .Subscribe(_ => InvalidateFormattedText());
-
- this.GetObservable(CaretIndexProperty)
- .Subscribe(CaretIndexChanged);
-
- this.GetObservable(PasswordCharProperty)
- .Subscribe(_ => InvalidateFormattedText());
}
public int CaretIndex
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/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs
index e1b3061b54..c6119e89dc 100644
--- a/src/Avalonia.Controls/Primitives/ScrollBar.cs
+++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs
@@ -7,6 +7,7 @@ using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Input;
+using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 188685f796..c8c15bc079 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -333,6 +333,11 @@ namespace Avalonia.Controls.Primitives
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(Items, SelectedItem);
+
+ if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
+ {
+ SelectedIndex = 0;
+ }
break;
}
}
diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs
index a61757e628..ec0dbd124c 100644
--- a/src/Avalonia.Controls/Primitives/TabStrip.cs
+++ b/src/Avalonia.Controls/Primitives/TabStrip.cs
@@ -4,6 +4,7 @@
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Input;
+using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{
diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs
index c96fea6c25..21a7dd68f8 100644
--- a/src/Avalonia.Controls/Primitives/Track.cs
+++ b/src/Avalonia.Controls/Primitives/Track.cs
@@ -3,6 +3,7 @@
using System;
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.Metadata;
namespace Avalonia.Controls.Primitives
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/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs
index a0f51099cd..29e3a17f74 100644
--- a/src/Avalonia.Controls/ProgressBar.cs
+++ b/src/Avalonia.Controls/ProgressBar.cs
@@ -3,6 +3,7 @@
using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
@@ -33,8 +34,8 @@ namespace Avalonia.Controls
static ProgressBar()
{
- PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
- PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
+ PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
+ PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged);
@@ -120,4 +121,4 @@ namespace Avalonia.Controls
UpdateIndicator(Bounds.Size);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
new file mode 100644
index 0000000000..04d859c742
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
@@ -0,0 +1,54 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using Avalonia.Controls.Templates;
+
+namespace Avalonia.Controls
+{
+ internal class ItemTemplateWrapper
+ {
+ private readonly IDataTemplate _dataTemplate;
+
+ public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
+
+ public IControl GetElement(IControl parent, object data)
+ {
+ var selectedTemplate = _dataTemplate;
+ var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
+ IControl element = null;
+
+ if (recyclePool != null)
+ {
+ // try to get an element from the recycle pool.
+ element = recyclePool.TryGetElement(string.Empty, parent);
+ }
+
+ if (element == null)
+ {
+ // no element was found in recycle pool, create a new element
+ element = selectedTemplate.Build(data);
+
+ // Associate template with element
+ element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate);
+ }
+
+ return element;
+ }
+
+ public void RecycleElement(IControl parent, IControl element)
+ {
+ var selectedTemplate = _dataTemplate;
+ var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
+ if (recyclePool == null)
+ {
+ // No Recycle pool in the template, create one.
+ recyclePool = new RecyclePool();
+ RecyclePool.SetPoolInstance(selectedTemplate, recyclePool);
+ }
+
+ recyclePool.PutElement(element, "" /* key */, parent);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
new file mode 100644
index 0000000000..257c1b2399
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
@@ -0,0 +1,724 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+using Avalonia.Layout;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Represents a data-driven collection control that incorporates a flexible layout system,
+ /// custom views, and virtualization.
+ ///
+ public class ItemsRepeater : Panel
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty HorizontalCacheLengthProperty =
+ AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ItemTemplateProperty =
+ ItemsControl.ItemTemplateProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ItemsProperty =
+ ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty LayoutProperty =
+ AvaloniaProperty.Register(nameof(Layout), new StackLayout());
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty VerticalCacheLengthProperty =
+ AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0);
+
+ private static readonly AttachedProperty VirtualizationInfoProperty =
+ AvaloniaProperty.RegisterAttached("VirtualizationInfo");
+
+ internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1);
+ internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0);
+
+ private readonly ViewManager _viewManager;
+ private readonly ViewportManager _viewportManager;
+ private IEnumerable _items;
+ private VirtualizingLayoutContext _layoutContext;
+ private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
+ private bool _isLayoutInProgress;
+ private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
+ private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
+ private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ItemsRepeater()
+ {
+ _viewManager = new ViewManager(this);
+ _viewportManager = new ViewportManager(this);
+ KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once);
+ OnLayoutChanged(null, Layout);
+ }
+
+ static ItemsRepeater()
+ {
+ ClipToBoundsProperty.OverrideDefaultValue(true);
+ }
+
+ ///
+ /// Gets or sets the layout used to size and position elements in the ItemsRepeater.
+ ///
+ ///
+ /// The layout used to size and position elements. The default is a StackLayout with
+ /// vertical orientation.
+ ///
+ public AttachedLayout Layout
+ {
+ get => GetValue(LayoutProperty);
+ set => SetValue(LayoutProperty, value);
+ }
+
+ ///
+ /// Gets or sets an object source used to generate the content of the ItemsRepeater.
+ ///
+ public IEnumerable Items
+ {
+ get => _items;
+ set => SetAndRaise(ItemsProperty, ref _items, value);
+ }
+
+ ///
+ /// Gets or sets the template used to display each item.
+ ///
+ public IDataTemplate ItemTemplate
+ {
+ get => GetValue(ItemTemplateProperty);
+ set => SetValue(ItemTemplateProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value that indicates the size of the buffer used to realize items when
+ /// panning or scrolling horizontally.
+ ///
+ public double HorizontalCacheLength
+ {
+ get => GetValue(HorizontalCacheLengthProperty);
+ set => SetValue(HorizontalCacheLengthProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value that indicates the size of the buffer used to realize items when
+ /// panning or scrolling vertically.
+ ///
+ public double VerticalCacheLength
+ {
+ get => GetValue(VerticalCacheLengthProperty);
+ set => SetValue(VerticalCacheLengthProperty, value);
+ }
+
+ ///
+ /// Gets a standardized view of the supported interactions between a given Items object and
+ /// the ItemsRepeater control and its associated components.
+ ///
+ public ItemsSourceView ItemsSourceView { get; private set; }
+
+ internal ItemTemplateWrapper ItemTemplateShim { get; set; }
+ internal Point LayoutOrigin { get; set; }
+ internal object LayoutState { get; set; }
+ internal IControl MadeAnchor => _viewportManager.MadeAnchor;
+ internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow();
+ internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor;
+
+ private bool IsProcessingCollectionChange => _processingItemsSourceChange != null;
+
+ private LayoutContext LayoutContext
+ {
+ get
+ {
+ if (_layoutContext == null)
+ {
+ _layoutContext = new RepeaterLayoutContext(this);
+ }
+
+ return _layoutContext;
+ }
+ }
+
+ ///
+ /// Occurs each time an element is cleared and made available to be re-used.
+ ///
+ ///
+ /// This event is raised immediately each time an element is cleared, such as when it falls
+ /// outside the range of realized items. Elements are cleared when they become available
+ /// for re-use.
+ ///
+ public event EventHandler ElementClearing;
+
+ ///
+ /// Occurs for each realized when the index for the item it
+ /// represents has changed.
+ ///
+ ///
+ /// When you use ItemsRepeater to build a more complex control that supports specific
+ /// interactions on the child elements (such as selection or click), it is useful to be
+ /// able to keep an up-to-date identifier for the backing data item.
+ ///
+ /// This event is raised for each realized IControl where the index for the item it
+ /// represents has changed. For example, when another item is added or removed in the data
+ /// source, the index for items that come after in the ordering will be impacted.
+ ///
+ public event EventHandler ElementIndexChanged;
+
+ ///
+ /// Occurs each time an element is prepared for use.
+ ///
+ ///
+ /// The prepared element might be newly created or an existing element that is being re-
+ /// used.
+ ///
+ public event EventHandler ElementPrepared;
+
+ ///
+ /// Retrieves the index of the item from the data source that corresponds to the specified
+ /// .
+ ///
+ ///
+ /// The element that corresponds to the item to get the index of.
+ ///
+ ///
+ /// The index of the item from the data source that corresponds to the specified UIElement,
+ /// or -1 if the element is not supported.
+ ///
+ public int GetElementIndex(IControl element) => GetElementIndexImpl(element);
+
+ ///
+ /// Retrieves the realized UIElement that corresponds to the item at the specified index in
+ /// the data source.
+ ///
+ /// The index of the item.
+ ///
+ /// he UIElement that corresponds to the item at the specified index if the item is
+ /// realized, or null if the item is not realized.
+ ///
+ public IControl TryGetElement(int index) => GetElementFromIndexImpl(index);
+
+ internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true);
+
+ internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false);
+
+ internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index);
+
+ internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element)
+ {
+ var value = element.GetValue(VirtualizationInfoProperty);
+ return value;
+ }
+
+ internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element)
+ {
+ if (TryGetVirtualizationInfo(element) != null)
+ {
+ throw new InvalidOperationException("VirtualizationInfo already created.");
+ }
+
+ var result = new VirtualizationInfo();
+ element.SetValue(VirtualizationInfoProperty, result);
+ return result;
+ }
+
+ internal static VirtualizationInfo GetVirtualizationInfo(IControl element)
+ {
+ var result = element.GetValue(VirtualizationInfoProperty);
+
+ if (result == null)
+ {
+ result = new VirtualizationInfo();
+ element.SetValue(VirtualizationInfoProperty, result);
+ }
+
+ return result;
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ if (_isLayoutInProgress)
+ {
+ throw new AvaloniaInternalException("Reentrancy detected during layout.");
+ }
+
+ if (IsProcessingCollectionChange)
+ {
+ throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
+ }
+
+ _viewportManager.OnOwnerMeasuring();
+
+ _isLayoutInProgress = true;
+
+ try
+ {
+ _viewManager.PrunePinnedElements();
+ var extent = new Rect();
+ var desiredSize = new Size();
+ var layout = Layout;
+
+ if (layout != null)
+ {
+ var layoutContext = GetLayoutContext();
+
+ desiredSize = layout.Measure(layoutContext, availableSize);
+ extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height);
+
+ // Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not
+ // call GetElementAt(index).
+ foreach (var element in Children)
+ {
+ var virtInfo = GetVirtualizationInfo(element);
+
+ if (virtInfo.Owner == ElementOwner.Layout &&
+ virtInfo.AutoRecycleCandidate &&
+ !virtInfo.KeepAlive)
+ {
+ ClearElementImpl(element);
+ }
+ }
+ }
+
+ _viewportManager.SetLayoutExtent(extent);
+ return desiredSize;
+ }
+ finally
+ {
+ _isLayoutInProgress = false;
+ }
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ if (_isLayoutInProgress)
+ {
+ throw new AvaloniaInternalException("Reentrancy detected during layout.");
+ }
+
+ if (IsProcessingCollectionChange)
+ {
+ throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
+ }
+
+ _isLayoutInProgress = true;
+
+ try
+ {
+ var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default;
+
+ // The view manager might clear elements during this call.
+ // That's why we call it before arranging cleared elements
+ // off screen.
+ _viewManager.OnOwnerArranged();
+
+ foreach (var element in Children)
+ {
+ var virtInfo = GetVirtualizationInfo(element);
+ virtInfo.KeepAlive = false;
+
+ if (virtInfo.Owner == ElementOwner.ElementFactory ||
+ virtInfo.Owner == ElementOwner.PinnedPool)
+ {
+ // Toss it away. And arrange it with size 0 so that XYFocus won't use it.
+ element.Arrange(new Rect(
+ ClearedElementsArrangePosition.X - element.DesiredSize.Width,
+ ClearedElementsArrangePosition.Y - element.DesiredSize.Height,
+ 0,
+ 0));
+ }
+ else
+ {
+ var newBounds = element.Bounds;
+ virtInfo.ArrangeBounds = newBounds;
+ }
+ }
+
+ _viewportManager.OnOwnerArranged();
+
+ return arrangeSize;
+ }
+ finally
+ {
+ _isLayoutInProgress = false;
+ }
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ InvalidateMeasure();
+ _viewportManager.ResetScrollers();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _viewportManager.ResetScrollers();
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+ {
+ var property = args.Property;
+
+ if (property == ItemsProperty)
+ {
+ var newValue = (IEnumerable)args.NewValue;
+ var newDataSource = newValue as ItemsSourceView;
+ if (newValue != null && newDataSource == null)
+ {
+ newDataSource = new ItemsSourceView(newValue);
+ }
+
+ OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
+ }
+ else if (property == ItemTemplateProperty)
+ {
+ OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue);
+ }
+ else if (property == LayoutProperty)
+ {
+ OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue);
+ }
+ else if (property == HorizontalCacheLengthProperty)
+ {
+ _viewportManager.HorizontalCacheLength = (double)args.NewValue;
+ }
+ else if (property == VerticalCacheLengthProperty)
+ {
+ _viewportManager.VerticalCacheLength = (double)args.NewValue;
+ }
+ else
+ {
+ base.OnPropertyChanged(args);
+ }
+ }
+
+ internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
+ {
+ var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle);
+ return element;
+ }
+
+ internal void ClearElementImpl(IControl element)
+ {
+ // Clearing an element due to a collection change
+ // is more strict in that pinned elements will be forcibly
+ // unpinned and sent back to the view generator.
+ var isClearedDueToCollectionChange =
+ _processingItemsSourceChange != null &&
+ (_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove ||
+ _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace ||
+ _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset);
+
+ _viewManager.ClearElement(element, isClearedDueToCollectionChange);
+ _viewportManager.OnElementCleared(element);
+ }
+
+ private int GetElementIndexImpl(IControl element)
+ {
+ var virtInfo = TryGetVirtualizationInfo(element);
+ return _viewManager.GetElementIndex(virtInfo);
+ }
+
+ private IControl GetElementFromIndexImpl(int index)
+ {
+ IControl result = null;
+
+ var children = Children;
+ for (var i = 0; i < children.Count && result == null; ++i)
+ {
+ var element = children[i];
+ var virtInfo = TryGetVirtualizationInfo(element);
+ if (virtInfo?.IsRealized == true && virtInfo.Index == index)
+ {
+ result = element;
+ }
+ }
+
+ return result;
+ }
+
+ private IControl GetOrCreateElementImpl(int index)
+ {
+ if (index >= 0 && index >= ItemsSourceView.Count)
+ {
+ throw new ArgumentException("Argument index is invalid.", "index");
+ }
+
+ if (_isLayoutInProgress)
+ {
+ throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout.");
+ }
+
+ var element = GetElementFromIndexImpl(index);
+ bool isAnchorOutsideRealizedRange = element == null;
+
+ if (isAnchorOutsideRealizedRange)
+ {
+ if (Layout == null)
+ {
+ throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout.");
+ }
+
+ element = (IControl)GetLayoutContext().GetOrCreateElementAt(index);
+ element.Measure(Size.Infinity);
+ }
+
+ _viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange);
+ InvalidateMeasure();
+
+ return element;
+ }
+
+ internal void OnElementPrepared(IControl element, int index)
+ {
+ _viewportManager.OnElementPrepared(element);
+ if (ElementPrepared != null)
+ {
+ if (_elementPreparedArgs == null)
+ {
+ _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);
+ }
+ else
+ {
+ _elementPreparedArgs.Update(element, index);
+ }
+
+ ElementPrepared(this, _elementPreparedArgs);
+ }
+ }
+
+ internal void OnElementClearing(IControl element)
+ {
+ if (ElementClearing != null)
+ {
+ if (_elementClearingArgs == null)
+ {
+ _elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element);
+ }
+ else
+ {
+ _elementClearingArgs.Update(element);
+ }
+
+ ElementClearing(this, _elementClearingArgs);
+ }
+ }
+
+ internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
+ {
+ if (ElementIndexChanged != null)
+ {
+ if (_elementIndexChangedArgs == null)
+ {
+ _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex);
+ }
+ else
+ {
+ _elementIndexChangedArgs.Update(element, oldIndex, newIndex);
+ }
+
+ ElementIndexChanged(this, _elementIndexChangedArgs);
+ }
+ }
+
+ private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)
+ {
+ if (_isLayoutInProgress)
+ {
+ throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
+ }
+
+ ItemsSourceView?.Dispose();
+ ItemsSourceView = newValue;
+
+ if (oldValue != null)
+ {
+ oldValue.CollectionChanged -= OnItemsSourceViewChanged;
+ }
+
+ if (newValue != null)
+ {
+ newValue.CollectionChanged += OnItemsSourceViewChanged;
+ }
+
+ if (Layout != null)
+ {
+ if (Layout is VirtualizingLayout virtualLayout)
+ {
+ var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
+ }
+ else if (Layout is NonVirtualizingLayout nonVirtualLayout)
+ {
+ // Walk through all the elements and make sure they are cleared for
+ // non-virtualizing layouts.
+ foreach (var element in Children)
+ {
+ if (GetVirtualizationInfo(element).IsRealized)
+ {
+ ClearElementImpl(element);
+ }
+ }
+ }
+
+ InvalidateMeasure();
+ }
+ }
+
+ private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue)
+ {
+ if (_isLayoutInProgress && oldValue != null)
+ {
+ throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout.");
+ }
+
+ // Since the ItemTemplate has changed, we need to re-evaluate all the items that
+ // have already been created and are now in the tree. The easiest way to do that
+ // would be to do a reset.. Note that this has to be done before we change the template
+ // so that the cleared elements go back into the old template.
+ if (Layout != null)
+ {
+ if (Layout is VirtualizingLayout virtualLayout)
+ {
+ var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ _processingItemsSourceChange = args;
+
+ try
+ {
+ virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
+ }
+ finally
+ {
+ _processingItemsSourceChange = null;
+ }
+ }
+ else if (Layout is NonVirtualizingLayout)
+ {
+ // Walk through all the elements and make sure they are cleared for
+ // non-virtualizing layouts.
+ foreach (var element in Children)
+ {
+ if (GetVirtualizationInfo(element).IsRealized)
+ {
+ ClearElementImpl(element);
+ }
+ }
+ }
+ }
+
+ ItemTemplateShim = new ItemTemplateWrapper(newValue);
+
+ InvalidateMeasure();
+ }
+
+ private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue)
+ {
+ if (_isLayoutInProgress)
+ {
+ throw new InvalidOperationException("Layout cannot be changed during layout.");
+ }
+
+ _viewManager.OnLayoutChanging();
+
+ if (oldValue != null)
+ {
+ oldValue.UninitializeForContext(LayoutContext);
+ oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
+ oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout;
+
+ // Walk through all the elements and make sure they are cleared
+ foreach (var element in Children)
+ {
+ if (GetVirtualizationInfo(element).IsRealized)
+ {
+ ClearElementImpl(element);
+ }
+ }
+
+ LayoutState = null;
+ }
+
+ if (newValue != null)
+ {
+ newValue.InitializeForContext(LayoutContext);
+ newValue.MeasureInvalidated += InvalidateMeasureForLayout;
+ newValue.ArrangeInvalidated += InvalidateArrangeForLayout;
+ }
+
+ bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;
+ _viewportManager.OnLayoutChanged(isVirtualizingLayout);
+ InvalidateMeasure();
+ }
+
+ private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args)
+ {
+ if (_isLayoutInProgress)
+ {
+ // Bad things will follow if the data changes while we are in the middle of a layout pass.
+ throw new InvalidOperationException("Changes in data source are not allowed during layout.");
+ }
+
+ if (IsProcessingCollectionChange)
+ {
+ throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source.");
+ }
+
+ _processingItemsSourceChange = args;
+
+ try
+ {
+ _viewManager.OnItemsSourceChanged(sender, args);
+
+ if (Layout != null)
+ {
+ if (Layout is VirtualizingLayout virtualLayout)
+ {
+ virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args);
+ }
+ else
+ {
+ // NonVirtualizingLayout
+ InvalidateMeasure();
+ }
+ }
+ }
+ finally
+ {
+ _processingItemsSourceChange = null;
+ }
+ }
+
+ private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure();
+
+ private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange();
+
+ private VirtualizingLayoutContext GetLayoutContext()
+ {
+ if (_layoutContext == null)
+ {
+ _layoutContext = new RepeaterLayoutContext(this);
+ }
+
+ return _layoutContext;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs
new file mode 100644
index 0000000000..75d50e52a6
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs
@@ -0,0 +1,24 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ItemsRepeaterElementClearingEventArgs : EventArgs
+ {
+ internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element;
+
+ ///
+ /// Gets the element that is being cleared for re-use.
+ ///
+ public IControl Element { get; private set; }
+
+ internal void Update(IControl element) => Element = element;
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
new file mode 100644
index 0000000000..7ca68140b2
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
@@ -0,0 +1,44 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
+ {
+ internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex)
+ {
+ Element = element;
+ NewIndex = newIndex;
+ OldIndex = oldIndex;
+ }
+
+ ///
+ /// Get the element for which the index changed.
+ ///
+ public IControl Element { get; private set; }
+
+ ///
+ /// Gets the index of the element after the change.
+ ///
+ public int NewIndex { get; private set; }
+
+ ///
+ /// Gets the index of the element before the change.
+ ///
+ public int OldIndex { get; private set; }
+
+ internal void Update(IControl element, int newIndex, int oldIndex)
+ {
+ Element = element;
+ NewIndex = newIndex;
+ OldIndex = oldIndex;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs
new file mode 100644
index 0000000000..5a30dbcf2a
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs
@@ -0,0 +1,35 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ItemsRepeaterElementPreparedEventArgs
+ {
+ internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index)
+ {
+ Element = element;
+ Index = index;
+ }
+
+ ///
+ /// Gets the prepared element.
+ ///
+ public IControl Element { get; private set; }
+
+ ///
+ /// Gets the index of the item the element was prepared for.
+ ///
+ public int Index { get; private set; }
+
+ internal void Update(IControl element, int index)
+ {
+ Element = element;
+ Index = index;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
new file mode 100644
index 0000000000..02ead7ef36
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
@@ -0,0 +1,145 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Represents a standardized view of the supported interactions between a given ItemsSource
+ /// object and an control.
+ ///
+ ///
+ /// Components written to work with ItemsRepeater should consume the
+ /// via ItemsSourceView since this provides a normalized
+ /// view of the Items. That way, each component does not need to know if the source is an
+ /// IEnumerable, an IList, or something else.
+ ///
+ public class ItemsSourceView : INotifyCollectionChanged, IDisposable
+ {
+ private readonly IList _inner;
+ private INotifyCollectionChanged _notifyCollectionChanged;
+ private int _cachedSize = -1;
+
+ ///
+ /// Initializes a new instance of the ItemsSourceView class for the specified data source.
+ ///
+ /// The data source.
+ public ItemsSourceView(IEnumerable source)
+ {
+ Contract.Requires(source != null);
+
+ if (source is IList list)
+ {
+ _inner = list;
+ }
+ else if (source is IEnumerable
diff --git a/src/Avalonia.Visuals/Media/BrushExtensions.cs b/src/Avalonia.Visuals/Media/BrushExtensions.cs
index 522953eb04..87e698e705 100644
--- a/src/Avalonia.Visuals/Media/BrushExtensions.cs
+++ b/src/Avalonia.Visuals/Media/BrushExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Media.Immutable;
namespace Avalonia.Media
{
@@ -23,27 +24,33 @@ namespace Avalonia.Media
}
///
- /// Converts a pen to a pen with an immutable brush
+ /// Converts a dash style to an immutable dash style.
+ ///
+ /// The dash style.
+ ///
+ /// The result of calling if the style is mutable,
+ /// otherwise .
+ ///
+ public static ImmutableDashStyle ToImmutable(this IDashStyle style)
+ {
+ Contract.Requires(style != null);
+
+ return style as ImmutableDashStyle ?? ((DashStyle)style).ToImmutable();
+ }
+
+ ///
+ /// Converts a pen to an immutable pen.
///
/// The pen.
///
- /// A copy of the pen with an immutable brush, or if the pen's brush
- /// is already immutable or null.
+ /// The result of calling if the brush is mutable,
+ /// otherwise .
///
- public static Pen ToImmutable(this Pen pen)
+ public static ImmutablePen ToImmutable(this IPen pen)
{
Contract.Requires(pen != null);
- var brush = pen.Brush?.ToImmutable();
- return ReferenceEquals(pen.Brush, brush) ?
- pen :
- new Pen(
- brush,
- thickness: pen.Thickness,
- dashStyle: pen.DashStyle,
- lineCap: pen.LineCap,
- lineJoin: pen.LineJoin,
- miterLimit: pen.MiterLimit);
+ return pen as ImmutablePen ?? ((Pen)pen).ToImmutable();
}
}
}
diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs
index c7e1db57b2..7784c73736 100644
--- a/src/Avalonia.Visuals/Media/DashStyle.cs
+++ b/src/Avalonia.Visuals/Media/DashStyle.cs
@@ -1,72 +1,114 @@
namespace Avalonia.Media
{
+ using System;
using System.Collections.Generic;
+ using System.Linq;
using Avalonia.Animation;
+ using Avalonia.Media.Immutable;
- public class DashStyle : Animatable
+ ///
+ /// Represents the sequence of dashes and gaps that will be applied by a .
+ ///
+ public class DashStyle : Animatable, IDashStyle, IAffectsRender
{
- private static DashStyle dash;
- public static DashStyle Dash
- {
- get
- {
- if (dashDotDot == null)
- {
- dash = new DashStyle(new double[] { 2, 2 }, 1);
- }
-
- return dash;
- }
- }
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty> DashesProperty =
+ AvaloniaProperty.Register>(nameof(Dashes));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty OffsetProperty =
+ AvaloniaProperty.Register(nameof(Offset));
+ private static ImmutableDashStyle s_dash;
+ private static ImmutableDashStyle s_dot;
+ private static ImmutableDashStyle s_dashDot;
+ private static ImmutableDashStyle s_dashDotDot;
- private static DashStyle dot;
- public static DashStyle Dot
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DashStyle()
+ : this(null, 0)
{
- get { return dot ?? (dot = new DashStyle(new double[] {0, 2}, 0)); }
}
- private static DashStyle dashDot;
- public static DashStyle DashDot
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dashes collection.
+ /// The dash sequence offset.
+ public DashStyle(IEnumerable dashes, double offset)
{
- get
- {
- if (dashDot == null)
- {
- dashDot = new DashStyle(new double[] { 2, 2, 0, 2 }, 1);
- }
-
- return dashDot;
- }
+ Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty();
+ Offset = offset;
}
- private static DashStyle dashDotDot;
- public static DashStyle DashDotDot
+ static DashStyle()
{
- get
+ void RaiseInvalidated(AvaloniaPropertyChangedEventArgs e)
{
- if (dashDotDot == null)
- {
- dashDotDot = new DashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1);
- }
-
- return dashDotDot;
+ ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty);
}
+
+ DashesProperty.Changed.Subscribe(RaiseInvalidated);
+ OffsetProperty.Changed.Subscribe(RaiseInvalidated);
}
+ ///
+ /// Represents a dashed .
+ ///
+ public static IDashStyle Dash =>
+ s_dash ?? (s_dash = new ImmutableDashStyle(new double[] { 2, 2 }, 1));
+
+ ///
+ /// Represents a dotted .
+ ///
+ public static IDashStyle Dot =>
+ s_dot ?? (s_dot = new ImmutableDashStyle(new double[] { 0, 2 }, 0));
+
+ ///
+ /// Represents a dashed dotted .
+ ///
+ public static IDashStyle DashDot =>
+ s_dashDot ?? (s_dashDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1));
+
+ ///
+ /// Represents a dashed double dotted .
+ ///
+ public static IDashStyle DashDotDot =>
+ s_dashDotDot ?? (s_dashDotDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1));
- public DashStyle(IReadOnlyList dashes = null, double offset = 0.0)
+ ///
+ /// Gets or sets the length of alternating dashes and gaps.
+ ///
+ public IReadOnlyList Dashes
{
- this.Dashes = dashes;
- this.Offset = offset;
+ get => GetValue(DashesProperty);
+ set => SetValue(DashesProperty, value);
}
///
- /// Gets and sets the length of alternating dashes and gaps.
+ /// Gets or sets how far in the dash sequence the stroke will start.
///
- public IReadOnlyList Dashes { get; }
+ public double Offset
+ {
+ get => GetValue(OffsetProperty);
+ set => SetValue(OffsetProperty, value);
+ }
- public double Offset { get; }
+ ///
+ /// Raised when the dash style changes.
+ ///
+ public event EventHandler Invalidated;
+
+ ///
+ /// Returns an immutable clone of the .
+ ///
+ ///
+ public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset);
}
}
diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs
index d3af71ffcb..4c9bf9ebd4 100644
--- a/src/Avalonia.Visuals/Media/DrawingContext.cs
+++ b/src/Avalonia.Visuals/Media/DrawingContext.cs
@@ -94,7 +94,7 @@ namespace Avalonia.Media
/// The stroke pen.
/// The first point of the line.
/// The second point of the line.
- public void DrawLine(Pen pen, Point p1, Point p2)
+ public void DrawLine(IPen pen, Point p1, Point p2)
{
if (PenIsVisible(pen))
{
@@ -108,7 +108,7 @@ namespace Avalonia.Media
/// The fill brush.
/// The stroke pen.
/// The geometry.
- public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry)
+ public void DrawGeometry(IBrush brush, IPen pen, Geometry geometry)
{
Contract.Requires(geometry != null);
@@ -124,7 +124,7 @@ namespace Avalonia.Media
/// The pen.
/// The rectangle bounds.
/// The corner radius.
- public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f)
+ public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f)
{
if (PenIsVisible(pen))
{
@@ -328,7 +328,7 @@ namespace Avalonia.Media
PlatformImpl.Dispose();
}
- private static bool PenIsVisible(Pen pen)
+ private static bool PenIsVisible(IPen pen)
{
return pen?.Brush != null && pen.Thickness > 0;
}
diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs
index ac0cc1c17d..3dad10fb8f 100644
--- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs
+++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs
@@ -23,7 +23,7 @@
public static readonly StyledProperty PenProperty =
AvaloniaProperty.Register(nameof(Pen));
- public Pen Pen
+ public IPen Pen
{
get => GetValue(PenProperty);
set => SetValue(PenProperty, value);
diff --git a/src/Avalonia.Visuals/Media/IDashStyle.cs b/src/Avalonia.Visuals/Media/IDashStyle.cs
new file mode 100644
index 0000000000..7835c7a1e9
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/IDashStyle.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Represents the sequence of dashes and gaps that will be applied by a .
+ ///
+ public interface IDashStyle
+ {
+ ///
+ /// Gets or sets the length of alternating dashes and gaps.
+ ///
+ IReadOnlyList Dashes { get; }
+
+ ///
+ /// Gets or sets how far in the dash sequence the stroke will start.
+ ///
+ double Offset { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/IPen.cs b/src/Avalonia.Visuals/Media/IPen.cs
new file mode 100644
index 0000000000..0cdac312cc
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/IPen.cs
@@ -0,0 +1,39 @@
+namespace Avalonia.Media
+{
+ ///
+ /// Describes how a stroke is drawn.
+ ///
+ public interface IPen
+ {
+ ///
+ /// Gets the brush used to draw the stroke.
+ ///
+ IBrush Brush { get; }
+
+ ///
+ /// Gets the style of dashed lines drawn with a object.
+ ///
+ IDashStyle DashStyle { get; }
+
+ ///
+ /// Gets the type of shape to use on both ends of a line.
+ ///
+ PenLineCap LineCap { get; }
+
+ ///
+ /// Gets a value describing how to join consecutive line or curve segments in a
+ /// contained in a object.
+ ///
+ PenLineJoin LineJoin { get; }
+
+ ///
+ /// Gets the limit of the thickness of the join on a mitered corner.
+ ///
+ double MiterLimit { get; }
+
+ ///
+ /// Gets the stroke thickness.
+ ///
+ double Thickness { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs
new file mode 100644
index 0000000000..e9a52fe6ed
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Media.Immutable
+{
+ ///
+ /// Represents the sequence of dashes and gaps that will be applied by an
+ /// .
+ ///
+ public class ImmutableDashStyle : IDashStyle, IEquatable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dashes collection.
+ /// The dash sequence offset.
+ public ImmutableDashStyle(IEnumerable dashes, double offset)
+ {
+ Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty();
+ Offset = offset;
+ }
+
+ ///
+ public IReadOnlyList Dashes { get; }
+
+ ///
+ public double Offset { get; }
+
+ ///
+ public override bool Equals(object obj) => Equals(obj as IDashStyle);
+
+ ///
+ public bool Equals(IDashStyle other)
+ {
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+ else if (other is null)
+ {
+ return false;
+ }
+
+ if (Offset != other.Offset)
+ {
+ return false;
+ }
+
+ return SequenceEqual(Dashes, other.Dashes);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ var hashCode = 717868523;
+ hashCode = hashCode * -1521134295 + Offset.GetHashCode();
+
+ if (Dashes != null)
+ {
+ foreach (var i in Dashes)
+ {
+ hashCode = hashCode * -1521134295 + i.GetHashCode();
+ }
+ }
+
+ return hashCode;
+ }
+
+ private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right)
+ {
+ if (left == right)
+ {
+ return true;
+ }
+
+ if (left == null || right == null || left.Count != right.Count)
+ {
+ return false;
+ }
+
+ for (var c = 0; c < left.Count; c++)
+ {
+ if (left[c] != right[c])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
new file mode 100644
index 0000000000..4b3bd640cb
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
@@ -0,0 +1,118 @@
+// 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.Collections.Generic;
+
+namespace Avalonia.Media.Immutable
+{
+ ///
+ /// Describes how a stroke is drawn.
+ ///
+ public class ImmutablePen : IPen, IEquatable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The stroke color.
+ /// The stroke thickness.
+ /// The dash style.
+ /// Specifies the type of graphic shape to use on both ends of a line.
+ /// The line join.
+ /// The miter limit.
+ public ImmutablePen(
+ uint color,
+ double thickness = 1.0,
+ ImmutableDashStyle dashStyle = null,
+ PenLineCap lineCap = PenLineCap.Flat,
+ PenLineJoin lineJoin = PenLineJoin.Miter,
+ double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The brush used to draw.
+ /// The stroke thickness.
+ /// The dash style.
+ /// The line cap.
+ /// The line join.
+ /// The miter limit.
+ public ImmutablePen(
+ IBrush brush,
+ double thickness = 1.0,
+ ImmutableDashStyle dashStyle = null,
+ PenLineCap lineCap = PenLineCap.Flat,
+ PenLineJoin lineJoin = PenLineJoin.Miter,
+ double miterLimit = 10.0)
+ {
+ Brush = brush;
+ Thickness = thickness;
+ LineCap = lineCap;
+ LineJoin = lineJoin;
+ MiterLimit = miterLimit;
+ DashStyle = dashStyle;
+ }
+
+ ///
+ /// Gets the brush used to draw the stroke.
+ ///
+ public IBrush Brush { get; }
+
+ ///
+ /// Gets the stroke thickness.
+ ///
+ public double Thickness { get; }
+
+ ///
+ /// Specifies the style of dashed lines drawn with a object.
+ ///
+ public IDashStyle DashStyle { get; }
+
+ ///
+ /// Specifies the type of graphic shape to use on both ends of a line.
+ ///
+ public PenLineCap LineCap { get; }
+
+ ///
+ /// Specifies how to join consecutive line or curve segments in a
+ /// (subpaths) contained in a object.
+ ///
+ public PenLineJoin LineJoin { get; }
+
+ ///
+ /// The limit on the ratio of the miter length to half this pen's Thickness.
+ ///
+ public double MiterLimit { get; }
+
+ ///
+ public override bool Equals(object obj) => Equals(obj as IPen);
+
+ ///
+ public bool Equals(IPen other)
+ {
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+ else if (other is null)
+ {
+ return false;
+ }
+
+ return EqualityComparer.Default.Equals(Brush, other.Brush) &&
+ Thickness == other.Thickness &&
+ EqualityComparer.Default.Equals(DashStyle, other.DashStyle) &&
+ LineCap == other.LineCap &&
+ LineJoin == other.LineJoin &&
+ MiterLimit == other.MiterLimit;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return (Brush, Thickness, DashStyle, LineCap, LineJoin, MiterLimit).GetHashCode();
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs
index ee427c913b..b88fae28ff 100644
--- a/src/Avalonia.Visuals/Media/Pen.cs
+++ b/src/Avalonia.Visuals/Media/Pen.cs
@@ -1,13 +1,61 @@
// 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.Collections.Generic;
+using Avalonia.Media.Immutable;
+using Avalonia.Utilities;
+
namespace Avalonia.Media
{
///
/// Describes how a stroke is drawn.
///
- public class Pen
+ public class Pen : AvaloniaObject, IPen
{
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty BrushProperty =
+ AvaloniaProperty.Register(nameof(Brush));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ThicknessProperty =
+ AvaloniaProperty.Register(nameof(Thickness), 1.0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty DashStyleProperty =
+ AvaloniaProperty.Register(nameof(DashStyle));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty LineCapProperty =
+ AvaloniaProperty.Register(nameof(LineCap));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty LineJoinProperty =
+ AvaloniaProperty.Register(nameof(LineJoin));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MiterLimitProperty =
+ AvaloniaProperty.Register(nameof(MiterLimit), 10.0);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Pen()
+ {
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -20,7 +68,7 @@ namespace Avalonia.Media
public Pen(
uint color,
double thickness = 1.0,
- DashStyle dashStyle = null,
+ IDashStyle dashStyle = null,
PenLineCap lineCap = PenLineCap.Flat,
PenLineJoin lineJoin = PenLineJoin.Miter,
double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
@@ -39,7 +87,7 @@ namespace Avalonia.Media
public Pen(
IBrush brush,
double thickness = 1.0,
- DashStyle dashStyle = null,
+ IDashStyle dashStyle = null,
PenLineCap lineCap = PenLineCap.Flat,
PenLineJoin lineJoin = PenLineJoin.Miter,
double miterLimit = 10.0)
@@ -52,34 +100,139 @@ namespace Avalonia.Media
DashStyle = dashStyle;
}
+ static Pen()
+ {
+ AffectsRender(
+ BrushProperty,
+ ThicknessProperty,
+ DashStyleProperty,
+ LineCapProperty,
+ LineJoinProperty,
+ MiterLimitProperty);
+ }
+
+ ///
+ /// Gets or sets the brush used to draw the stroke.
+ ///
+ public IBrush Brush
+ {
+ get => GetValue(BrushProperty);
+ set => SetValue(BrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets the stroke thickness.
+ ///
+ public double Thickness
+ {
+ get => GetValue(ThicknessProperty);
+ set => SetValue(ThicknessProperty, value);
+ }
+
+ ///
+ /// Gets or sets the style of dashed lines drawn with a object.
+ ///
+ public IDashStyle DashStyle
+ {
+ get => GetValue(DashStyleProperty);
+ set => SetValue(DashStyleProperty, value);
+ }
+
+ ///
+ /// Gets or sets the type of shape to use on both ends of a line.
+ ///
+ public PenLineCap LineCap
+ {
+ get => GetValue(LineCapProperty);
+ set => SetValue(LineCapProperty, value);
+ }
+
///
- /// Gets the brush used to draw the stroke.
+ /// Gets or sets the join style for the ends of two consecutive lines drawn with this
+ /// .
///
- public IBrush Brush { get; }
+ public PenLineJoin LineJoin
+ {
+ get => GetValue(LineJoinProperty);
+ set => SetValue(LineJoinProperty, value);
+ }
///
- /// Gets the stroke thickness.
+ /// Gets or sets the limit of the thickness of the join on a mitered corner.
///
- public double Thickness { get; }
+ public double MiterLimit
+ {
+ get => GetValue(MiterLimitProperty);
+ set => SetValue(MiterLimitProperty, value);
+ }
///
- /// Specifies the style of dashed lines drawn with a object.
+ /// Raised when the pen changes.
///
- public DashStyle DashStyle { get; }
+ public event EventHandler Invalidated;
///
- /// Specifies the type of graphic shape to use on both ends of a line.
+ /// Creates an immutable clone of the brush.
///
- public PenLineCap LineCap { get; }
+ /// The immutable clone.
+ public ImmutablePen ToImmutable()
+ {
+ return new ImmutablePen(
+ Brush?.ToImmutable(),
+ Thickness,
+ DashStyle?.ToImmutable(),
+ LineCap,
+ LineJoin,
+ MiterLimit);
+ }
///
- /// Specifies how to join consecutive line or curve segments in a (subpath) contained in a object.
+ /// Marks a property as affecting the pen's visual representation.
///
- public PenLineJoin LineJoin { get; }
+ /// The properties.
+ ///
+ /// After a call to this method in a pen's static constructor, any change to the
+ /// property will cause the event to be raised on the pen.
+ ///
+ protected static void AffectsRender(params AvaloniaProperty[] properties)
+ where T : Pen
+ {
+ void Invalidate(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is T sender)
+ {
+ if (e.OldValue is IAffectsRender oldValue)
+ {
+ WeakEventHandlerManager.Unsubscribe(
+ oldValue,
+ nameof(oldValue.Invalidated),
+ sender.AffectsRenderInvalidated);
+ }
+
+ if (e.NewValue is IAffectsRender newValue)
+ {
+ WeakEventHandlerManager.Subscribe(
+ newValue,
+ nameof(newValue.Invalidated),
+ sender.AffectsRenderInvalidated);
+ }
+
+ sender.RaiseInvalidated(EventArgs.Empty);
+ }
+ }
+
+ foreach (var property in properties)
+ {
+ property.Changed.Subscribe(Invalidate);
+ }
+ }
///
- /// The limit on the ratio of the miter length to half this pen's Thickness.
+ /// Raises the event.
///
- public double MiterLimit { get; }
+ /// The event args.
+ protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
+
+ private void AffectsRenderInvalidated(object sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty);
}
}
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/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
index e5be04ebf9..f74c551fe0 100644
--- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
@@ -50,7 +50,7 @@ namespace Avalonia.Platform
/// The stroke pen.
/// The first point of the line.
/// The second point of the line.
- void DrawLine(Pen pen, Point p1, Point p2);
+ void DrawLine(IPen pen, Point p1, Point p2);
///
/// Draws a geometry.
@@ -58,7 +58,7 @@ namespace Avalonia.Platform
/// The fill brush.
/// The stroke pen.
/// The geometry.
- void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry);
+ void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry);
///
/// Draws the outline of a rectangle.
@@ -66,7 +66,7 @@ namespace Avalonia.Platform
/// The pen.
/// The rectangle bounds.
/// The corner radius.
- void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f);
+ void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f);
///
/// Draws text.
diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
index 4e8e6521bd..b762859d1d 100644
--- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
@@ -20,7 +20,7 @@ namespace Avalonia.Platform
///
/// The pen to use. May be null.
/// The bounding rectangle.
- Rect GetRenderBounds(Pen pen);
+ Rect GetRenderBounds(IPen pen);
///
/// Indicates whether the geometry's fill contains the specified point.
@@ -42,7 +42,7 @@ namespace Avalonia.Platform
/// The stroke to use.
/// The point.
/// true if the geometry contains the point; otherwise, false.
- bool StrokeContains(Pen pen, Point point);
+ bool StrokeContains(IPen pen, Point point);
///
/// Makes a clone of the geometry with the specified transform.
diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index 0d077d2a3a..bf1799bbdc 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
@@ -30,6 +30,7 @@ namespace Avalonia.Rendering
private bool _disposed;
private volatile IRef _scene;
private DirtyVisuals _dirty;
+ private HashSet _recalculateChildren;
private IRef _overlay;
private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
@@ -135,6 +136,8 @@ namespace Avalonia.Rendering
DisposeRenderTarget();
}
+ public void RecalculateChildren(IVisual visual) => _recalculateChildren?.Add(visual);
+
void DisposeRenderTarget()
{
using (var l = _lock.TryLock())
@@ -229,6 +232,8 @@ namespace Avalonia.Rendering
internal void UnitTestRender() => Render(false);
+ internal Scene UnitTestScene() => _scene.Item;
+
private void Render(bool forceComposite)
{
using (var l = _lock.TryLock())
@@ -516,10 +521,19 @@ namespace Avalonia.Rendering
if (_dirty == null)
{
_dirty = new DirtyVisuals();
+ _recalculateChildren = new HashSet();
_sceneBuilder.UpdateAll(scene);
}
- else if (_dirty.Count > 0)
+ else
{
+ foreach (var visual in _recalculateChildren)
+ {
+ var node = scene.FindNode(visual);
+ ((VisualNode)node)?.SortChildren(scene);
+ }
+
+ _recalculateChildren.Clear();
+
foreach (var visual in _dirty)
{
_sceneBuilder.Update(scene, visual);
@@ -547,7 +561,6 @@ namespace Avalonia.Rendering
}
}
- System.Diagnostics.Debug.WriteLine("Invalidated " + rect);
SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect));
}
}
diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs
index 36a1f7d220..9ad7186dca 100644
--- a/src/Avalonia.Visuals/Rendering/IRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs
@@ -50,6 +50,12 @@ namespace Avalonia.Rendering
/// The visuals at the specified point, topmost first.
IEnumerable HitTest(Point p, IVisual root, Func filter);
+ ///
+ /// Informs the renderer that the z-ordering of a visual's children has changed.
+ ///
+ /// The visual.
+ void RecalculateChildren(IVisual visual);
+
///
/// Called when a resize notification is received by the control being rendered.
///
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index 21129e38af..b2d242d4af 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -163,6 +163,9 @@ namespace Avalonia.Rendering
return HitTest(root, p, filter);
}
+ ///
+ public void RecalculateChildren(IVisual visual) => AddDirty(visual);
+
///
public void Start()
{
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
index 4c09dc2ddd..b2c0581388 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph
///
internal abstract class BrushDrawOperation : DrawOperation
{
- public BrushDrawOperation(Rect bounds, Matrix transform, Pen pen)
+ public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen)
: base(bounds, transform, pen)
{
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
index 0b33851911..3af56f5215 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
@@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph
}
///
- public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
var next = NextDrawAs();
@@ -137,7 +137,7 @@ namespace Avalonia.Rendering.SceneGraph
}
///
- public void DrawLine(Pen pen, Point p1, Point p2)
+ public void DrawLine(IPen pen, Point p1, Point p2)
{
var next = NextDrawAs();
@@ -152,7 +152,7 @@ namespace Avalonia.Rendering.SceneGraph
}
///
- public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+ public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0)
{
var next = NextDrawAs();
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
index 1a5a6fad3f..d9dfd8bd55 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
@@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph
///
internal abstract class DrawOperation : IDrawOperation
{
- public DrawOperation(Rect bounds, Matrix transform, Pen pen)
+ public DrawOperation(Rect bounds, Matrix transform, IPen pen)
{
bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform);
Bounds = new Rect(
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
index 2d01b117d9..d5aa1251f3 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using Avalonia.Media;
+using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.VisualTree;
@@ -24,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
public GeometryNode(
Matrix transform,
IBrush brush,
- Pen pen,
+ IPen pen,
IGeometryImpl geometry,
IDictionary childScenes = null)
: base(geometry.GetRenderBounds(pen), transform, null)
@@ -49,7 +50,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// Gets the stroke pen.
///
- public Pen Pen { get; }
+ public ImmutablePen Pen { get; }
///
/// Gets the geometry to draw.
@@ -71,11 +72,11 @@ namespace Avalonia.Rendering.SceneGraph
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
///
- public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry)
+ public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry)
{
return transform == Transform &&
Equals(brush, Brush) &&
- pen == Pen &&
+ Equals(Pen, pen) &&
Equals(geometry, Geometry);
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
index 11c763fcc9..9a65fac078 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using Avalonia.Media;
+using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.VisualTree;
@@ -23,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph
/// Child scenes for drawing visual brushes.
public LineNode(
Matrix transform,
- Pen pen,
+ IPen pen,
Point p1,
Point p2,
IDictionary childScenes = null)
@@ -44,7 +45,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// Gets the stroke pen.
///
- public Pen Pen { get; }
+ public ImmutablePen Pen { get; }
///
/// Gets the start point of the line.
@@ -71,9 +72,9 @@ namespace Avalonia.Rendering.SceneGraph
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
///
- public bool Equals(Matrix transform, Pen pen, Point p1, Point p2)
+ public bool Equals(Matrix transform, IPen pen, Point p1, Point p2)
{
- return transform == Transform && pen == Pen && p1 == P1 && p2 == P2;
+ return transform == Transform && Equals(Pen, pen) && p1 == P1 && p2 == P2;
}
public override void Render(IDrawingContextImpl context)
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
index c622dc8a43..0f3581b84c 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using Avalonia.Media;
+using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.VisualTree;
@@ -25,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph
public RectangleNode(
Matrix transform,
IBrush brush,
- Pen pen,
+ IPen pen,
Rect rect,
float cornerRadius,
IDictionary childScenes = null)
@@ -52,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// Gets the stroke pen.
///
- public Pen Pen { get; }
+ public ImmutablePen Pen { get; }
///
/// Gets the rectangle to draw.
@@ -80,11 +81,11 @@ namespace Avalonia.Rendering.SceneGraph
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
///
- public bool Equals(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius)
+ public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, float cornerRadius)
{
return transform == Transform &&
Equals(brush, Brush) &&
- pen == Pen &&
+ Equals(Pen, pen) &&
rect == Rect &&
cornerRadius == CornerRadius;
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
index 4e95d21a48..f579bf0a62 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
@@ -172,6 +172,42 @@ namespace Avalonia.Rendering.SceneGraph
old.Dispose();
}
+ ///
+ /// Sorts the collection according to the order of the visual's
+ /// children and their z-index.
+ ///
+ /// The scene that the node is a part of.
+ public void SortChildren(Scene scene)
+ {
+ if (_children == null || _children.Count <= 1)
+ {
+ return;
+ }
+
+ var keys = new List(Visual.VisualChildren.Count);
+
+ for (var i = 0; i < Visual.VisualChildren.Count; ++i)
+ {
+ var child = Visual.VisualChildren[i];
+ var zIndex = child.ZIndex;
+ keys.Add(((long)zIndex << 32) + i);
+ }
+
+ keys.Sort();
+ _children.Clear();
+
+ foreach (var i in keys)
+ {
+ var child = Visual.VisualChildren[(int)(i & 0xffffffff)];
+ var node = scene.FindNode(child);
+
+ if (node != null)
+ {
+ _children.Add(node);
+ }
+ }
+ }
+
///
/// Removes items in the collection from the specified index
/// to the end.
@@ -236,7 +272,7 @@ namespace Avalonia.Rendering.SceneGraph
{
foreach (var operation in DrawOperations)
{
- if (operation.Item.HitTest(p))
+ if (operation?.Item?.HitTest(p) == true)
{
return true;
}
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index 9e088cb136..1f2d67b69e 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -111,6 +111,7 @@ namespace Avalonia
IsVisibleProperty,
OpacityProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
+ ZIndexProperty.Changed.Subscribe(ZIndexChanged);
}
///
@@ -345,6 +346,12 @@ namespace Avalonia
}
}
+ protected override void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ base.LogicalChildrenCollectionChanged(sender, e);
+ VisualRoot?.Renderer?.RecalculateChildren(this);
+ }
+
///
/// Calls the method
/// for this control and all of its visual descendants.
@@ -501,6 +508,18 @@ namespace Avalonia
}
}
+ ///
+ /// Called when the property changes on any control.
+ ///
+ /// The event args.
+ private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var sender = e.Sender as IVisual;
+ var parent = sender?.VisualParent;
+ sender?.InvalidateVisual();
+ parent?.VisualRoot?.Renderer?.RecalculateChildren(parent);
+ }
+
///
/// Called when the 's event
/// is fired.
diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs
index 0a8b1ee9c4..bed6f4693b 100644
--- a/src/Avalonia.X11/X11CursorFactory.cs
+++ b/src/Avalonia.X11/X11CursorFactory.cs
@@ -24,7 +24,7 @@ namespace Avalonia.X11
{StandardCursorType.No, CursorFontShape.XC_X_cursor},
{StandardCursorType.Wait, CursorFontShape.XC_watch},
{StandardCursorType.AppStarting, CursorFontShape.XC_watch},
- {StandardCursorType.BottomSize, CursorFontShape.XC_bottom_side},
+ {StandardCursorType.BottomSide, CursorFontShape.XC_bottom_side},
{StandardCursorType.DragCopy, CursorFontShape.XC_center_ptr},
{StandardCursorType.DragLink, CursorFontShape.XC_fleur},
{StandardCursorType.DragMove, CursorFontShape.XC_diamond_cross},
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/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs
deleted file mode 100644
index f28dca81b8..0000000000
--- a/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-
-namespace Avalonia.LinuxFramebuffer
-{
- unsafe class EvDevDevice
- {
- private static readonly Lazy> AllMouseDevices = new Lazy>(()
- => OpenMouseDevices());
-
- private static List OpenMouseDevices()
- {
- var rv = new List();
- foreach (var dev in Directory.GetFiles("/dev/input", "event*").Select(Open))
- {
- if (!dev.IsMouse)
- NativeUnsafeMethods.close(dev.Fd);
- else
- rv.Add(dev);
- }
- return rv;
- }
-
- public static IReadOnlyList MouseDevices => AllMouseDevices.Value;
-
-
- public int Fd { get; }
- private IntPtr _dev;
- public string Name { get; }
- public List EventTypes { get; private set; } = new List();
- public input_absinfo? AbsX { get; }
- public input_absinfo? AbsY { get; }
-
- public EvDevDevice(int fd, IntPtr dev)
- {
- Fd = fd;
- _dev = dev;
- Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev));
- foreach (EvType type in Enum.GetValues(typeof(EvType)))
- {
- if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0)
- EventTypes.Add(type);
- }
- var ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int) AbsAxis.ABS_X);
- if (ptr != null)
- AbsX = *ptr;
- ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int)AbsAxis.ABS_Y);
- if (ptr != null)
- AbsY = *ptr;
- }
-
- public input_event? NextEvent()
- {
- input_event ev;
- if (NativeUnsafeMethods.libevdev_next_event(_dev, 2, out ev) == 0)
- return ev;
- return null;
- }
-
- public bool IsMouse => EventTypes.Contains(EvType.EV_REL);
-
- public static EvDevDevice Open(string device)
- {
- var fd = NativeUnsafeMethods.open(device, 2048, 0);
- if (fd <= 0)
- throw new Exception($"Unable to open {device} code {Marshal.GetLastWin32Error()}");
- IntPtr dev;
- var rc = NativeUnsafeMethods.libevdev_new_from_fd(fd, out dev);
- if (rc < 0)
- {
- NativeUnsafeMethods.close(fd);
- throw new Exception($"Unable to initialize evdev for {device} code {Marshal.GetLastWin32Error()}");
- }
- return new EvDevDevice(fd, dev);
- }
-
-
- }
-
- public class EvDevAxisInfo
- {
- public int Minimum { get; set; }
- public int Maximum { get; set; }
- }
-}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
index 78369a3648..2dc112f3d3 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
@@ -2,30 +2,35 @@
using System.Collections.Generic;
using Avalonia.Input;
using Avalonia.Input.Raw;
+using Avalonia.LinuxFramebuffer.Input;
+using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.LinuxFramebuffer
{
- class FramebufferToplevelImpl : IEmbeddableWindowImpl
+ class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider
{
- private readonly LinuxFramebuffer _fb;
+ private readonly IOutputBackend _outputBackend;
+ private readonly IInputBackend _inputBackend;
private bool _renderQueued;
public IInputRoot InputRoot { get; private set; }
- public FramebufferToplevelImpl(LinuxFramebuffer fb)
+ public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend)
{
- _fb = fb;
+ _outputBackend = outputBackend;
+ _inputBackend = inputBackend;
Invalidate(default(Rect));
- var mice = new Mice(this, ClientSize.Width, ClientSize.Height);
- mice.Start();
- mice.Event += e => Input?.Invoke(e);
+ _inputBackend.Initialize(this, e => Input?.Invoke(e));
}
public IRenderer CreateRenderer(IRenderRoot root)
{
- return new ImmediateRenderer(root);
+ return new DeferredRenderer(root, AvaloniaLocator.Current.GetService())
+ {
+
+ };
}
public void Dispose()
@@ -36,19 +41,12 @@ namespace Avalonia.LinuxFramebuffer
public void Invalidate(Rect rect)
{
- if(_renderQueued)
- return;
- _renderQueued = true;
- Dispatcher.UIThread.Post(() =>
- {
- Paint?.Invoke(new Rect(default(Point), ClientSize));
- _renderQueued = false;
- });
}
public void SetInputRoot(IInputRoot inputRoot)
{
InputRoot = inputRoot;
+ _inputBackend.SetInputRoot(inputRoot);
}
public Point PointToClient(PixelPoint p) => p.ToPoint(1);
@@ -59,10 +57,12 @@ namespace Avalonia.LinuxFramebuffer
{
}
- public Size ClientSize => _fb.PixelSize;
- public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice;
- public double Scaling => 1;
- public IEnumerable Surfaces => new object[] {_fb};
+ public Size ClientSize => ScaledSize;
+ public IMouseDevice MouseDevice => new MouseDevice();
+ 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; }
public Action Resized { get; set; }
@@ -73,5 +73,7 @@ namespace Avalonia.LinuxFramebuffer
add {}
remove {}
}
+
+ public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling);
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs
new file mode 100644
index 0000000000..84a903bb9d
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs
@@ -0,0 +1,12 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.LinuxFramebuffer.Input
+{
+ public interface IInputBackend
+ {
+ void Initialize(IScreenInfoProvider info, Action onInput);
+ void SetInputRoot(IInputRoot root);
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs
new file mode 100644
index 0000000000..cb0e51862a
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs
@@ -0,0 +1,7 @@
+namespace Avalonia.LinuxFramebuffer.Input
+{
+ public interface IScreenInfoProvider
+ {
+ Size ScaledSize { get; }
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
new file mode 100644
index 0000000000..723028c666
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Threading;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Threading;
+using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods;
+namespace Avalonia.LinuxFramebuffer.Input.LibInput
+{
+ public class LibInputBackend : IInputBackend
+ {
+ private IScreenInfoProvider _screen;
+ private IInputRoot _inputRoot;
+ private readonly Queue _inputThreadActions = new Queue();
+ private TouchDevice _touch = new TouchDevice();
+ private MouseDevice _mouse = new MouseDevice();
+ private Point _mousePosition;
+
+ private readonly Queue _inputQueue = new Queue();
+ private Action _onInput;
+ private Dictionary _pointers = new Dictionary();
+
+ public LibInputBackend()
+ {
+ var ctx = libinput_path_create_context();
+
+ new Thread(()=>InputThread(ctx)).Start();
+ }
+
+
+
+ private unsafe void InputThread(IntPtr ctx)
+ {
+ var fd = libinput_get_fd(ctx);
+
+ var timeval = stackalloc IntPtr[2];
+
+
+ foreach (var f in Directory.GetFiles("/dev/input", "event*"))
+ libinput_path_add_device(ctx, f);
+ while (true)
+ {
+
+ IntPtr ev;
+ libinput_dispatch(ctx);
+ while ((ev = libinput_get_event(ctx)) != IntPtr.Zero)
+ {
+
+ var type = libinput_event_get_type(ev);
+ if (type >= LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN &&
+ type <= LibInputEventType.LIBINPUT_EVENT_TOUCH_CANCEL)
+ HandleTouch(ev, type);
+
+ if (type >= LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION
+ && type <= LibInputEventType.LIBINPUT_EVENT_POINTER_AXIS)
+ HandlePointer(ev, type);
+
+ libinput_event_destroy(ev);
+ libinput_dispatch(ctx);
+ }
+
+ pollfd pfd = new pollfd {fd = fd, events = 1};
+ NativeUnsafeMethods.poll(&pfd, new IntPtr(1), 10);
+ }
+ }
+
+ private void ScheduleInput(RawInputEventArgs ev)
+ {
+ lock (_inputQueue)
+ {
+ _inputQueue.Enqueue(ev);
+ if (_inputQueue.Count == 1)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ while (true)
+ {
+ Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
+ RawInputEventArgs dequeuedEvent = null;
+ lock(_inputQueue)
+ if (_inputQueue.Count != 0)
+ dequeuedEvent = _inputQueue.Dequeue();
+ if (dequeuedEvent == null)
+ return;
+ _onInput?.Invoke(dequeuedEvent);
+ }
+ }, DispatcherPriority.Input);
+ }
+ }
+ }
+
+ private void HandleTouch(IntPtr ev, LibInputEventType type)
+ {
+ var tev = libinput_event_get_touch_event(ev);
+ if(tev == IntPtr.Zero)
+ return;
+ if (type < LibInputEventType.LIBINPUT_EVENT_TOUCH_FRAME)
+ {
+ var info = _screen.ScaledSize;
+ var slot = libinput_event_touch_get_slot(tev);
+ Point pt;
+
+ if (type == LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN
+ || type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION)
+ {
+ var x = libinput_event_touch_get_x_transformed(tev, (int)info.Width);
+ var y = libinput_event_touch_get_y_transformed(tev, (int)info.Height);
+ pt = new Point(x, y);
+ _pointers[slot] = pt;
+ }
+ else
+ {
+ _pointers.TryGetValue(slot, out pt);
+ _pointers.Remove(slot);
+ }
+
+ var ts = libinput_event_touch_get_time_usec(tev) / 1000;
+ if (_inputRoot == null)
+ return;
+ ScheduleInput(new RawTouchEventArgs(_touch, ts,
+ _inputRoot,
+ type == LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN ? RawPointerEventType.TouchBegin
+ : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_UP ? RawPointerEventType.TouchEnd
+ : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION ? RawPointerEventType.TouchUpdate
+ : RawPointerEventType.TouchCancel,
+ pt, InputModifiers.None, slot));
+ }
+ }
+
+ private void HandlePointer(IntPtr ev, LibInputEventType type)
+ {
+ //TODO: support input modifiers
+ var pev = libinput_event_get_pointer_event(ev);
+ var info = _screen.ScaledSize;
+ var ts = libinput_event_pointer_get_time_usec(pev) / 1000;
+ if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE)
+ {
+ _mousePosition = new Point(libinput_event_pointer_get_absolute_x_transformed(pev, (int)info.Width),
+ libinput_event_pointer_get_absolute_y_transformed(pev, (int)info.Height));
+ ScheduleInput(new RawPointerEventArgs(_mouse, ts, _inputRoot, RawPointerEventType.Move, _mousePosition,
+ InputModifiers.None));
+ }
+ else if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_BUTTON)
+ {
+ var button = (EvKey)libinput_event_pointer_get_button(pev);
+ var buttonState = libinput_event_pointer_get_button_state(pev);
+
+
+ var evnt = button == EvKey.BTN_LEFT ?
+ (buttonState == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) :
+ button == EvKey.BTN_MIDDLE ?
+ (buttonState == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) :
+ button == EvKey.BTN_RIGHT ?
+ (buttonState == 1 ?
+ RawPointerEventType.RightButtonDown :
+ RawPointerEventType.RightButtonUp) :
+ (RawPointerEventType)(-1);
+ if (evnt == (RawPointerEventType)(-1))
+ return;
+
+
+ ScheduleInput(
+ new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, InputModifiers.None));
+ }
+
+ }
+
+
+
+ public void Initialize(IScreenInfoProvider screen, Action onInput)
+ {
+ _screen = screen;
+ _onInput = onInput;
+ }
+
+ public void SetInputRoot(IInputRoot root)
+ {
+ _inputRoot = root;
+ }
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs
new file mode 100644
index 0000000000..0492090461
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.LinuxFramebuffer.Input.LibInput
+{
+ unsafe class LibInputNativeUnsafeMethods
+ {
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ delegate int OpenRestrictedCallbackDelegate(IntPtr path, int flags, IntPtr userData);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ delegate void CloseRestrictedCallbackDelegate(int fd, IntPtr userData);
+
+ static int OpenRestricted(IntPtr path, int flags, IntPtr userData)
+ {
+ var fd = NativeUnsafeMethods.open(Marshal.PtrToStringAnsi(path), flags, 0);
+ if (fd == -1)
+ return -Marshal.GetLastWin32Error();
+
+ return fd;
+ }
+
+ static void CloseRestricted(int fd, IntPtr userData)
+ {
+ NativeUnsafeMethods.close(fd);
+ }
+
+ private static readonly IntPtr* s_Interface;
+
+ static LibInputNativeUnsafeMethods()
+ {
+ s_Interface = (IntPtr*)Marshal.AllocHGlobal(IntPtr.Size * 2);
+
+ IntPtr Convert(TDelegate del)
+ {
+ GCHandle.Alloc(del);
+ return Marshal.GetFunctionPointerForDelegate(del);
+ }
+
+ s_Interface[0] = Convert(OpenRestricted);
+ s_Interface[1] = Convert(CloseRestricted);
+ }
+
+ private const string LibInput = "libinput.so.10";
+
+ [DllImport(LibInput)]
+ public extern static IntPtr libinput_path_create_context(IntPtr* iface, IntPtr userData);
+
+ public static IntPtr libinput_path_create_context() =>
+ libinput_path_create_context(s_Interface, IntPtr.Zero);
+
+ [DllImport(LibInput)]
+ public extern static IntPtr libinput_path_add_device(IntPtr ctx, [MarshalAs(UnmanagedType.LPStr)] string path);
+
+ [DllImport(LibInput)]
+ public extern static IntPtr libinput_path_remove_device(IntPtr device);
+
+ [DllImport(LibInput)]
+ public extern static int libinput_get_fd(IntPtr ctx);
+
+ [DllImport(LibInput)]
+ public extern static void libinput_dispatch(IntPtr ctx);
+
+ [DllImport(LibInput)]
+ public extern static IntPtr libinput_get_event(IntPtr ctx);
+
+ [DllImport(LibInput)]
+ public extern static LibInputEventType libinput_event_get_type(IntPtr ev);
+
+ public enum LibInputEventType
+ {
+ LIBINPUT_EVENT_NONE = 0,
+ LIBINPUT_EVENT_DEVICE_ADDED,
+ LIBINPUT_EVENT_DEVICE_REMOVED,
+ LIBINPUT_EVENT_KEYBOARD_KEY = 300,
+ LIBINPUT_EVENT_POINTER_MOTION = 400,
+ LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE,
+ LIBINPUT_EVENT_POINTER_BUTTON,
+ LIBINPUT_EVENT_POINTER_AXIS,
+ LIBINPUT_EVENT_TOUCH_DOWN = 500,
+ LIBINPUT_EVENT_TOUCH_UP,
+ LIBINPUT_EVENT_TOUCH_MOTION,
+ LIBINPUT_EVENT_TOUCH_CANCEL,
+ LIBINPUT_EVENT_TOUCH_FRAME,
+ LIBINPUT_EVENT_TABLET_TOOL_AXIS = 600,
+ LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY,
+ LIBINPUT_EVENT_TABLET_TOOL_TIP,
+ LIBINPUT_EVENT_TABLET_TOOL_BUTTON,
+ LIBINPUT_EVENT_TABLET_PAD_BUTTON = 700,
+ LIBINPUT_EVENT_TABLET_PAD_RING,
+ LIBINPUT_EVENT_TABLET_PAD_STRIP,
+ LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN = 800,
+ LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE,
+ LIBINPUT_EVENT_GESTURE_SWIPE_END,
+ LIBINPUT_EVENT_GESTURE_PINCH_BEGIN,
+ LIBINPUT_EVENT_GESTURE_PINCH_UPDATE,
+ LIBINPUT_EVENT_GESTURE_PINCH_END,
+ LIBINPUT_EVENT_SWITCH_TOGGLE = 900,
+ }
+
+
+ [DllImport(LibInput)]
+ public extern static void libinput_event_destroy(IntPtr ev);
+
+ [DllImport(LibInput)]
+ public extern static IntPtr libinput_event_get_touch_event(IntPtr ev);
+
+ [DllImport(LibInput)]
+ public extern static int libinput_event_touch_get_slot(IntPtr ev);
+
+ [DllImport(LibInput)]
+ public extern static ulong libinput_event_touch_get_time_usec(IntPtr ev);
+
+ [DllImport(LibInput)]
+ public extern static double libinput_event_touch_get_x_transformed(IntPtr ev, int width);
+
+ [DllImport(LibInput)]
+ public extern static double libinput_event_touch_get_y_transformed(IntPtr ev, int height);
+
+ [DllImport(LibInput)]
+ public extern static IntPtr libinput_event_get_pointer_event(IntPtr ev);
+
+
+ [DllImport(LibInput)]
+ public extern static ulong libinput_event_pointer_get_time_usec(IntPtr ev);
+
+ [DllImport(LibInput)]
+ public extern static double libinput_event_pointer_get_absolute_x_transformed(IntPtr ev, int width);
+
+ [DllImport(LibInput)]
+ public extern static double libinput_event_pointer_get_absolute_y_transformed(IntPtr ev, int height);
+
+ [DllImport(LibInput)]
+ public extern static int libinput_event_pointer_get_button(IntPtr ev);
+
+ [DllImport(LibInput)]
+ public extern static int libinput_event_pointer_get_button_state(IntPtr ev);
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
index 396942c8dd..8fc555aac2 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
@@ -8,6 +8,9 @@ using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.LinuxFramebuffer;
+using Avalonia.LinuxFramebuffer.Input.LibInput;
+using Avalonia.LinuxFramebuffer.Output;
+using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
@@ -16,34 +19,37 @@ namespace Avalonia.LinuxFramebuffer
{
class LinuxFramebufferPlatform
{
- LinuxFramebuffer _fb;
- public static KeyboardDevice KeyboardDevice = new KeyboardDevice();
- public static MouseDevice MouseDevice = new MouseDevice();
+ IOutputBackend _fb;
private static readonly Stopwatch St = Stopwatch.StartNew();
internal static uint Timestamp => (uint)St.ElapsedTicks;
public static InternalPlatformThreadingInterface Threading;
- LinuxFramebufferPlatform(string fbdev = null)
+ LinuxFramebufferPlatform(IOutputBackend backend)
{
- _fb = new LinuxFramebuffer(fbdev);
+ _fb = backend;
}
void Initialize()
{
Threading = new InternalPlatformThreadingInterface();
+ if (_fb is IWindowingPlatformGlFeature glFeature)
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(glFeature);
AvaloniaLocator.CurrentMutable
+ .Bind().ToConstant(Threading)
+ .Bind().ToConstant(new DefaultRenderTimer(60))
+ .Bind().ToConstant(new RenderLoop())
.Bind().ToTransient()
- .Bind().ToConstant(KeyboardDevice)
+ .Bind().ToConstant(new KeyboardDevice())
.Bind().ToSingleton()
- .Bind().ToConstant(Threading)
.Bind().ToConstant(new RenderLoop())
- .Bind().ToSingleton()
- .Bind().ToConstant(Threading);
+ .Bind().ToSingleton();
+
}
- internal static LinuxFramebufferLifetime Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new()
+
+ internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend) where T : AppBuilderBase, new()
{
- var platform = new LinuxFramebufferPlatform(fbdev);
+ var platform = new LinuxFramebufferPlatform(outputBackend);
builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev");
return new LinuxFramebufferLifetime(platform._fb);
}
@@ -51,12 +57,12 @@ namespace Avalonia.LinuxFramebuffer
class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime
{
- private readonly LinuxFramebuffer _fb;
+ private readonly IOutputBackend _fb;
private TopLevel _topLevel;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public CancellationToken Token => _cts.Token;
- public LinuxFramebufferLifetime(LinuxFramebuffer fb)
+ public LinuxFramebufferLifetime(IOutputBackend fb)
{
_fb = fb;
}
@@ -69,10 +75,12 @@ namespace Avalonia.LinuxFramebuffer
if (_topLevel == null)
{
- var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb));
+ var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend()));
tl.Prepare();
_topLevel = tl;
+ _topLevel.Renderer.Start();
}
+
_topLevel.Content = value;
}
}
@@ -99,10 +107,17 @@ namespace Avalonia.LinuxFramebuffer
public static class LinuxFramebufferPlatformExtensions
{
- public static int StartLinuxFramebuffer(this T builder, string[] args, string fbdev = null)
+ 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, 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()
{
- var lifetime = LinuxFramebufferPlatform.Initialize(builder, fbdev);
+ var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend);
builder.Instance.ApplicationLifetime = lifetime;
builder.SetupWithoutStarting();
lifetime.Start(args);
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs
deleted file mode 100644
index 2b82b4f4aa..0000000000
--- a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using Avalonia.Input;
-using Avalonia.Input.Raw;
-using Avalonia.Platform;
-
-namespace Avalonia.LinuxFramebuffer
-{
- unsafe class Mice
- {
- private readonly FramebufferToplevelImpl _topLevel;
- private readonly double _width;
- private readonly double _height;
- private double _x;
- private double _y;
-
- public event Action Event;
-
- public Mice(FramebufferToplevelImpl topLevel, double width, double height)
- {
- _topLevel = topLevel;
- _width = width;
- _height = height;
- }
-
- public void Start() => ThreadPool.UnsafeQueueUserWorkItem(_ => Worker(), null);
-
- private void Worker()
- {
-
- var mouseDevices = EvDevDevice.MouseDevices.Where(d => d.IsMouse).ToList();
- if (mouseDevices.Count == 0)
- return;
- var are = new AutoResetEvent(false);
- while (true)
- {
- try
- {
- var rfds = new fd_set {count = mouseDevices.Count};
- for (int c = 0; c < mouseDevices.Count; c++)
- rfds.fds[c] = mouseDevices[c].Fd;
- IntPtr* timeval = stackalloc IntPtr[2];
- timeval[0] = new IntPtr(0);
- timeval[1] = new IntPtr(100);
- are.WaitOne(30);
- foreach (var dev in mouseDevices)
- {
- while(true)
- {
- var ev = dev.NextEvent();
- if (!ev.HasValue)
- break;
-
- LinuxFramebufferPlatform.Threading.Send(() => ProcessEvent(dev, ev.Value));
- }
- }
- }
- catch (Exception e)
- {
- Console.Error.WriteLine(e.ToString());
- }
- }
- }
-
- static double TranslateAxis(input_absinfo axis, int value, double max)
- {
- return (value - axis.minimum) / (double) (axis.maximum - axis.minimum) * max;
- }
-
- private void ProcessEvent(EvDevDevice device, input_event ev)
- {
- if (ev.type == (short)EvType.EV_REL)
- {
- if (ev.code == (short) AxisEventCode.REL_X)
- _x = Math.Min(_width, Math.Max(0, _x + ev.value));
- else if (ev.code == (short) AxisEventCode.REL_Y)
- _y = Math.Min(_height, Math.Max(0, _y + ev.value));
- else
- return;
- Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
- LinuxFramebufferPlatform.Timestamp,
- _topLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y),
- InputModifiers.None));
- }
- if (ev.type ==(int) EvType.EV_ABS)
- {
- if (ev.code == (short) AbsAxis.ABS_X && device.AbsX.HasValue)
- _x = TranslateAxis(device.AbsX.Value, ev.value, _width);
- else if (ev.code == (short) AbsAxis.ABS_Y && device.AbsY.HasValue)
- _y = TranslateAxis(device.AbsY.Value, ev.value, _height);
- else
- return;
- Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
- LinuxFramebufferPlatform.Timestamp,
- _topLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y),
- InputModifiers.None));
- }
- if (ev.type == (short) EvType.EV_KEY)
- {
- RawPointerEventType? type = null;
- if (ev.code == (ushort) EvKey.BTN_LEFT)
- type = ev.value == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp;
- if (ev.code == (ushort)EvKey.BTN_RIGHT)
- type = ev.value == 1 ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp;
- if (ev.code == (ushort) EvKey.BTN_MIDDLE)
- type = ev.value == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp;
- if (!type.HasValue)
- return;
-
- Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
- LinuxFramebufferPlatform.Timestamp,
- _topLevel.InputRoot, type.Value, new Point(_x, _y), default(InputModifiers)));
- }
- }
- }
-}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs
index 5427af7d44..18db176bcd 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs
@@ -33,6 +33,10 @@ namespace Avalonia.LinuxFramebuffer
[DllImport("libc", EntryPoint = "select", SetLastError = true)]
public static extern int select(int nfds, void* rfds, void* wfds, void* exfds, IntPtr* timevals);
+
+ [DllImport("libc", EntryPoint = "poll", SetLastError = true)]
+ public static extern int poll(pollfd* fds, IntPtr nfds, int timeout);
+
[DllImport("libevdev.so.2", EntryPoint = "libevdev_new_from_fd", SetLastError = true)]
public static extern int libevdev_new_from_fd(int fd, out IntPtr dev);
@@ -48,6 +52,13 @@ namespace Avalonia.LinuxFramebuffer
public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code);
}
+ [StructLayout(LayoutKind.Sequential)]
+ struct pollfd {
+ public int fd; /* file descriptor */
+ public short events; /* requested events */
+ public short revents; /* returned events */
+ };
+
enum FbIoCtl : uint
{
FBIOGET_VSCREENINFO = 0x4600,
@@ -188,7 +199,7 @@ namespace Avalonia.LinuxFramebuffer
unsafe struct fd_set
{
public int count;
- public fixed int fds [256];
+ public fixed byte fds [256];
}
enum AxisEventCode
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs
new file mode 100644
index 0000000000..e266c5ee54
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs
@@ -0,0 +1,292 @@
+using System;
+using System.Runtime.InteropServices;
+// ReSharper disable FieldCanBeMadeReadOnly.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable FieldCanBeMadeReadOnly.Local
+
+namespace Avalonia.LinuxFramebuffer.Output
+{
+ public enum DrmModeConnection
+ {
+ DRM_MODE_CONNECTED = 1,
+ DRM_MODE_DISCONNECTED = 2,
+ DRM_MODE_UNKNOWNCONNECTION = 3
+ }
+
+ public enum DrmModeSubPixel{
+ DRM_MODE_SUBPIXEL_UNKNOWN = 1,
+ DRM_MODE_SUBPIXEL_HORIZONTAL_RGB = 2,
+ DRM_MODE_SUBPIXEL_HORIZONTAL_BGR = 3,
+ DRM_MODE_SUBPIXEL_VERTICAL_RGB = 4,
+ DRM_MODE_SUBPIXEL_VERTICAL_BGR = 5,
+ DRM_MODE_SUBPIXEL_NONE = 6
+ }
+
+ static unsafe class LibDrm
+ {
+ private const string libdrm = "libdrm.so.2";
+ private const string libgbm = "libgbm.so.1";
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public unsafe delegate void DrmEventVBlankHandlerDelegate(int fd,
+ uint sequence,
+ uint tv_sec,
+ uint tv_usec,
+ void* user_data);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public unsafe delegate void DrmEventPageFlipHandlerDelegate(int fd,
+ uint sequence,
+ uint tv_sec,
+ uint tv_usec,
+ void* user_data);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public unsafe delegate IntPtr DrmEventPageFlipHandler2Delegate(int fd,
+ uint sequence,
+ uint tv_sec,
+ uint tv_usec,
+ uint crtc_id,
+ void* user_data);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public unsafe delegate void DrmEventSequenceHandlerDelegate(int fd,
+ ulong sequence,
+ ulong ns,
+ ulong user_data);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DrmEventContext
+ {
+ public int version; //4
+ public IntPtr vblank_handler;
+ public IntPtr page_flip_handler;
+ public IntPtr page_flip_handler2;
+ public IntPtr sequence_handler;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct drmModeRes {
+
+ public int count_fbs;
+ public uint *fbs;
+
+ public int count_crtcs;
+ public uint *crtcs;
+
+ public int count_connectors;
+ public uint *connectors;
+
+ public int count_encoders;
+ public uint *encoders;
+
+ uint min_width, max_width;
+ uint min_height, max_height;
+ }
+
+ [Flags]
+ public enum DrmModeType
+ {
+ DRM_MODE_TYPE_BUILTIN = (1 << 0),
+ DRM_MODE_TYPE_CLOCK_C = ((1 << 1) | DRM_MODE_TYPE_BUILTIN),
+ DRM_MODE_TYPE_CRTC_C = ((1 << 2) | DRM_MODE_TYPE_BUILTIN),
+ DRM_MODE_TYPE_PREFERRED = (1 << 3),
+ DRM_MODE_TYPE_DEFAULT = (1 << 4),
+ DRM_MODE_TYPE_USERDEF = (1 << 5),
+ DRM_MODE_TYPE_DRIVER = (1 << 6)
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct drmModeModeInfo
+ {
+ public uint clock;
+ public ushort hdisplay, hsync_start, hsync_end, htotal, hskew;
+ public ushort vdisplay, vsync_start, vsync_end, vtotal, vscan;
+
+ public uint vrefresh;
+
+ public uint flags;
+ public DrmModeType type;
+ public fixed byte name[32];
+ public PixelSize Resolution => new PixelSize(hdisplay, vdisplay);
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct drmModeConnector {
+ public uint connector_id;
+ public uint encoder_id; /**< Encoder currently connected to */
+ public uint connector_type;
+ public uint connector_type_id;
+ public DrmModeConnection connection;
+ public uint mmWidth, mmHeight; /**< HxW in millimeters */
+ public DrmModeSubPixel subpixel;
+
+ public int count_modes;
+ public drmModeModeInfo* modes;
+
+ public int count_props;
+ public uint *props; /**< List of property ids */
+ public ulong *prop_values; /**< List of property values */
+
+ public int count_encoders;
+ public uint *encoders; /**< List of encoder ids */
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct drmModeEncoder {
+ public uint encoder_id;
+ public uint encoder_type;
+ public uint crtc_id;
+ public uint possible_crtcs;
+ public uint possible_clones;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct drmModeCrtc {
+ public uint crtc_id;
+ public uint buffer_id; /**< FB id to connect to 0 = disconnect */
+
+ public uint x, y; /**< Position on the framebuffer */
+ public uint width, height;
+ public int mode_valid;
+ public drmModeModeInfo mode;
+
+ public int gamma_size; /**< Number of gamma stops */
+
+ }
+
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern drmModeRes* drmModeGetResources(int fd);
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern void drmModeFreeResources(drmModeRes* res);
+
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern drmModeConnector* drmModeGetConnector(int fd, uint connector);
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern void drmModeFreeConnector(drmModeConnector* res);
+
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern drmModeEncoder* drmModeGetEncoder(int fd, uint id);
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern void drmModeFreeEncoder(drmModeEncoder* enc);
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern drmModeCrtc* drmModeGetCrtc(int fd, uint id);
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern void drmModeFreeCrtc(drmModeCrtc* enc);
+
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern int drmModeAddFB(int fd, uint width, uint height, byte depth,
+ byte bpp, uint pitch, uint bo_handle,
+ out uint buf_id);
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern int drmModeSetCrtc(int fd, uint crtcId, uint bufferId,
+ uint x, uint y, uint *connectors, int count,
+ drmModeModeInfo* mode);
+
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern void drmModeRmFB(int fd, int id);
+
+ [Flags]
+ public enum DrmModePageFlip
+ {
+ Event = 1,
+ Async = 2,
+ Absolute = 4,
+ Relative = 8,
+ }
+
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern void drmModePageFlip(int fd, uint crtc_id, uint fb_id,
+ DrmModePageFlip flags, void *user_data);
+
+
+ [DllImport(libdrm, SetLastError = true)]
+ public static extern void drmHandleEvent(int fd, DrmEventContext* context);
+
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern IntPtr gbm_create_device(int fd);
+
+
+ [Flags]
+ public enum GbmBoFlags {
+ /**
+ * Buffer is going to be presented to the screen using an API such as KMS
+ */
+ GBM_BO_USE_SCANOUT = (1 << 0),
+ /**
+ * Buffer is going to be used as cursor
+ */
+ GBM_BO_USE_CURSOR = (1 << 1),
+ /**
+ * Deprecated
+ */
+ GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR,
+ /**
+ * Buffer is to be used for rendering - for example it is going to be used
+ * as the storage for a color buffer
+ */
+ GBM_BO_USE_RENDERING = (1 << 2),
+ /**
+ * Buffer can be used for gbm_bo_write. This is guaranteed to work
+ * with GBM_BO_USE_CURSOR, but may not work for other combinations.
+ */
+ GBM_BO_USE_WRITE = (1 << 3),
+ /**
+ * Buffer is linear, i.e. not tiled.
+ */
+ GBM_BO_USE_LINEAR = (1 << 4),
+ };
+
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern IntPtr gbm_surface_create(IntPtr device, int width, int height, uint format, GbmBoFlags flags);
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern IntPtr gbm_surface_lock_front_buffer(IntPtr surface);
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern int gbm_surface_release_buffer(IntPtr surface, IntPtr bo);
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern IntPtr gbm_bo_get_user_data(IntPtr surface);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate void GbmBoUserDataDestroyCallbackDelegate(IntPtr bo, IntPtr data);
+
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern IntPtr gbm_bo_set_user_data(IntPtr bo, IntPtr userData,
+ GbmBoUserDataDestroyCallbackDelegate onFree);
+
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern uint gbm_bo_get_width(IntPtr bo);
+
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern uint gbm_bo_get_height(IntPtr bo);
+
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern uint gbm_bo_get_stride(IntPtr bo);
+
+
+ [StructLayout(LayoutKind.Explicit)]
+ public struct GbmBoHandle
+ {
+ [FieldOffset(0)]
+ public void *ptr;
+ [FieldOffset(0)]
+ public int s32;
+ [FieldOffset(0)]
+ public uint u32;
+ [FieldOffset(0)]
+ public long s64;
+ [FieldOffset(0)]
+ public ulong u64;
+ }
+
+ [DllImport(libgbm, SetLastError = true)]
+ public static extern ulong gbm_bo_get_handle(IntPtr bo);
+
+ public static class GbmColorFormats
+ {
+ public static uint FourCC(char a, char b, char c, char d) =>
+ (uint)a | ((uint)b) << 8 | ((uint)c) << 16 | ((uint)d) << 24;
+
+ public static uint GBM_FORMAT_XRGB8888 { get; } = FourCC('X', 'R', '2', '4');
+ }
+ }
+
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
new file mode 100644
index 0000000000..b5ebc4bcb7
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
+using static Avalonia.LinuxFramebuffer.Output.LibDrm;
+
+namespace Avalonia.LinuxFramebuffer.Output
+{
+ public unsafe class DrmConnector
+ {
+ private static string[] KnownConnectorTypes =
+ {
+ "None", "VGA", "DVI-I", "DVI-D", "DVI-A", "Composite", "S-Video", "LVDS", "Component", "DIN",
+ "DisplayPort", "HDMI-A", "HDMI-B", "TV", "eDP", "Virtual", "DSI"
+ };
+
+ public DrmModeConnection Connection { get; }
+ public uint Id { get; }
+ public string Name { get; }
+ public Size SizeMm { get; }
+ public DrmModeSubPixel SubPixel { get; }
+ internal uint EncoderId { get; }
+ internal List EncoderIds { get; } = new List();
+ public List Modes { get; } = new List();
+ internal DrmConnector(drmModeConnector* conn)
+ {
+ Connection = conn->connection;
+ Id = conn->connector_id;
+ SizeMm = new Size(conn->mmWidth, conn->mmHeight);
+ SubPixel = conn->subpixel;
+ for (var c = 0; c < conn->count_encoders;c++)
+ EncoderIds.Add(conn->encoders[c]);
+ EncoderId = conn->encoder_id;
+ for(var c=0; ccount_modes; c++)
+ Modes.Add(new DrmModeInfo(ref conn->modes[c]));
+
+ if (conn->connector_type > KnownConnectorTypes.Length - 1)
+ Name = $"Unknown({conn->connector_type})-{conn->connector_type_id}";
+ else
+ Name = KnownConnectorTypes[conn->connector_type] + "-" + conn->connector_type_id;
+ }
+ }
+
+ public unsafe class DrmModeInfo
+ {
+ internal drmModeModeInfo Mode;
+
+ internal DrmModeInfo(ref drmModeModeInfo info)
+ {
+ Mode = info;
+ fixed (void* pName = info.name)
+ Name = Marshal.PtrToStringAnsi(new IntPtr(pName));
+ }
+
+ public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay);
+ public bool IsPreferred => Mode.type.HasFlag(DrmModeType.DRM_MODE_TYPE_PREFERRED);
+
+ public string Name { get; }
+ }
+
+ unsafe class DrmEncoder
+ {
+ public drmModeEncoder Encoder { get; }
+ public List PossibleCrtcs { get; } = new List();
+
+ public DrmEncoder(drmModeEncoder encoder, drmModeCrtc[] crtcs)
+ {
+ Encoder = encoder;
+ for (var c = 0; c < crtcs.Length; c++)
+ {
+ var bit = 1 << c;
+ if ((encoder.possible_crtcs & bit) != 0)
+ PossibleCrtcs.Add(crtcs[c]);
+ }
+ }
+ }
+
+
+
+ public unsafe class DrmResources
+ {
+ public List Connectors { get; }= new List();
+ internal Dictionary Encoders { get; } = new Dictionary();
+ public DrmResources(int fd)
+ {
+ var res = drmModeGetResources(fd);
+ if (res == null)
+ throw new Win32Exception("drmModeGetResources failed");
+
+ var crtcs = new drmModeCrtc[res->count_crtcs];
+ for (var c = 0; c < res->count_crtcs; c++)
+ {
+ var crtc = drmModeGetCrtc(fd, res->crtcs[c]);
+ crtcs[c] = *crtc;
+ drmModeFreeCrtc(crtc);
+ }
+
+ for (var c = 0; c < res->count_encoders; c++)
+ {
+ var enc = drmModeGetEncoder(fd, res->encoders[c]);
+ Encoders[res->encoders[c]] = new DrmEncoder(*enc, crtcs);
+ drmModeFreeEncoder(enc);
+ }
+
+ for (var c = 0; c < res->count_connectors; c++)
+ {
+ var conn = drmModeGetConnector(fd, res->connectors[c]);
+ Connectors.Add(new DrmConnector(conn));
+ drmModeFreeConnector(conn);
+ }
+
+
+ }
+
+ public void Dump()
+ {
+ void Print(int off, string s)
+ {
+ for (var c = 0; c < off; c++)
+ Console.Write(" ");
+ Console.WriteLine(s);
+ }
+ Print(0, "Connectors");
+ foreach (var conn in Connectors)
+ {
+ Print(1, $"{conn.Name}:");
+ Print(2, $"Id: {conn.Id}");
+ Print(2, $"Size: {conn.SizeMm} mm");
+ Print(2, $"Encoder id: {conn.EncoderId}");
+ Print(2, "Modes");
+ foreach (var m in conn.Modes)
+ Print(3, $"{m.Name} {(m.IsPreferred ? "PREFERRED" : "")}");
+
+
+ }
+ }
+ }
+
+ public unsafe class DrmCard : IDisposable
+ {
+ public int Fd { get; private set; }
+ public DrmCard(string path = null)
+ {
+ path = path ?? "/dev/dri/card0";
+ Fd = open(path, 2, 0);
+ if (Fd == -1)
+ throw new Win32Exception("Couldn't open " + path);
+ }
+
+ public DrmResources GetResources() => new DrmResources(Fd);
+ public void Dispose()
+ {
+ close(Fd);
+ Fd = -1;
+ }
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
new file mode 100644
index 0000000000..273265a6dc
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
@@ -0,0 +1,252 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.OpenGL;
+using Avalonia.Platform.Interop;
+using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
+using static Avalonia.LinuxFramebuffer.Output.LibDrm;
+namespace Avalonia.LinuxFramebuffer.Output
+{
+ public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature
+ {
+ 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);
+
+ var resources = card.GetResources();
+
+
+ var connector =
+ resources.Connectors.FirstOrDefault(x => x.Connection == DrmModeConnection.DRM_MODE_CONNECTED);
+ if(connector == null)
+ throw new InvalidOperationException("Unable to find connected DRM connector");
+
+ var mode = connector.Modes.OrderByDescending(x => x.IsPreferred)
+ .ThenByDescending(x => x.Resolution.Width * x.Resolution.Height)
+ //.OrderByDescending(x => x.Resolution.Width * x.Resolution.Height)
+ .FirstOrDefault();
+ if(mode == null)
+ throw new InvalidOperationException("Unable to find a usable DRM mode");
+ Init(card, resources, connector, mode);
+ }
+
+ public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo)
+ {
+ Init(card, resources, connector, modeInfo);
+ }
+
+ [DllImport("libEGL.so.1")]
+ static extern IntPtr eglGetProcAddress(Utf8Buffer proc);
+
+ private GbmBoUserDataDestroyCallbackDelegate FbDestroyDelegate;
+ private drmModeModeInfo _mode;
+ private EglDisplay _eglDisplay;
+ private EglSurface _eglSurface;
+ private EglContext _immediateContext;
+ private EglContext _deferredContext;
+ private IntPtr _currentBo;
+ private IntPtr _gbmTargetSurface;
+ private uint _crtcId;
+
+ void FbDestroyCallback(IntPtr bo, IntPtr userData)
+ {
+ drmModeRmFB(_card.Fd, userData.ToInt32());
+ }
+
+ uint GetFbIdForBo(IntPtr bo)
+ {
+ if (bo == IntPtr.Zero)
+ throw new ArgumentException("bo is 0");
+ var data = gbm_bo_get_user_data(bo);
+ if (data != IntPtr.Zero)
+ return (uint)data.ToInt32();
+
+ var w = gbm_bo_get_width(bo);
+ var h = gbm_bo_get_height(bo);
+ var stride = gbm_bo_get_stride(bo);
+ var handle = gbm_bo_get_handle(bo);
+
+ var ret = drmModeAddFB(_card.Fd, w, h, 24, 32, stride, (uint)handle, out var fbHandle);
+ if (ret != 0)
+ throw new Win32Exception(ret, "drmModeAddFb failed");
+
+ gbm_bo_set_user_data(bo, new IntPtr((int)fbHandle), FbDestroyDelegate);
+
+
+ return fbHandle;
+ }
+
+
+ void Init(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo)
+ {
+ FbDestroyDelegate = FbDestroyCallback;
+ _card = card;
+ uint GetCrtc()
+ {
+ if (resources.Encoders.TryGetValue(connector.EncoderId, out var encoder))
+ {
+ // Not sure why that should work
+ return encoder.Encoder.crtc_id;
+ }
+ else
+ {
+ foreach (var encId in connector.EncoderIds)
+ {
+ if (resources.Encoders.TryGetValue(encId, out encoder)
+ && encoder.PossibleCrtcs.Count>0)
+ return encoder.PossibleCrtcs.First().crtc_id;
+ }
+
+ throw new InvalidOperationException("Unable to find CRTC matching the desired mode");
+ }
+ }
+
+ _crtcId = GetCrtc();
+ var device = gbm_create_device(card.Fd);
+ _gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height,
+ GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING);
+ if(_gbmTargetSurface == null)
+ throw new InvalidOperationException("Unable to create GBM surface");
+
+
+
+ _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), 0x31D7, device, null);
+ _eglSurface = _eglDisplay.CreateWindowSurface(_gbmTargetSurface);
+
+
+ EglContext CreateContext(EglContext share)
+ {
+ var offSurf = gbm_surface_create(device, 1, 1, GbmColorFormats.GBM_FORMAT_XRGB8888,
+ GbmBoFlags.GBM_BO_USE_RENDERING);
+ if (offSurf == null)
+ throw new InvalidOperationException("Unable to create 1x1 sized GBM surface");
+ return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf));
+ }
+
+ _immediateContext = CreateContext(null);
+ _deferredContext = CreateContext(_immediateContext);
+
+ _immediateContext.MakeCurrent(_eglSurface);
+ _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0);
+ _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT);
+ _eglSurface.SwapBuffers();
+ var bo = gbm_surface_lock_front_buffer(_gbmTargetSurface);
+ var fbId = GetFbIdForBo(bo);
+ var connectorId = connector.Id;
+ var mode = modeInfo.Mode;
+
+
+ var res = drmModeSetCrtc(_card.Fd, _crtcId, fbId, 0, 0, &connectorId, 1, &mode);
+ if (res != 0)
+ throw new Win32Exception(res, "drmModeSetCrtc failed");
+
+ _mode = mode;
+ _currentBo = bo;
+
+ // Go trough two cycles of buffer swapping (there are render artifacts otherwise)
+ for(var c=0;c<2;c++)
+ using (CreateGlRenderTarget().BeginDraw())
+ {
+ _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0);
+ _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT);
+ }
+ }
+
+ public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
+ {
+ return new RenderTarget(this);
+ }
+
+ class RenderTarget : IGlPlatformSurfaceRenderTarget
+ {
+ private readonly DrmOutput _parent;
+
+ public RenderTarget(DrmOutput parent)
+ {
+ _parent = parent;
+ }
+ public void Dispose()
+ {
+ // We are wrapping GBM buffer chain associated with CRTC, and don't free it on a whim
+ }
+
+ class RenderSession : IGlPlatformSurfaceRenderingSession
+ {
+ private readonly DrmOutput _parent;
+
+ public RenderSession(DrmOutput parent)
+ {
+ _parent = parent;
+ }
+
+ public void Dispose()
+ {
+ _parent._eglDisplay.GlInterface.Flush();
+ _parent._eglSurface.SwapBuffers();
+
+ var nextBo = gbm_surface_lock_front_buffer(_parent._gbmTargetSurface);
+ if (nextBo == IntPtr.Zero)
+ {
+ // Not sure what else can be done
+ Console.WriteLine("gbm_surface_lock_front_buffer failed");
+ }
+ else
+ {
+
+ var fb = _parent.GetFbIdForBo(nextBo);
+ bool waitingForFlip = true;
+
+ drmModePageFlip(_parent._card.Fd, _parent._crtcId, fb, DrmModePageFlip.Event, null);
+
+ DrmEventPageFlipHandlerDelegate flipCb =
+ (int fd, uint sequence, uint tv_sec, uint tv_usec, void* user_data) =>
+ {
+ waitingForFlip = false;
+ };
+ var cbHandle = GCHandle.Alloc(flipCb);
+ var ctx = new DrmEventContext
+ {
+ version = 4, page_flip_handler = Marshal.GetFunctionPointerForDelegate(flipCb)
+ };
+ while (waitingForFlip)
+ {
+ var pfd = new pollfd {events = 1, fd = _parent._card.Fd};
+ poll(&pfd, new IntPtr(1), -1);
+ drmHandleEvent(_parent._card.Fd, &ctx);
+ }
+
+ cbHandle.Free();
+ gbm_surface_release_buffer(_parent._gbmTargetSurface, _parent._currentBo);
+ _parent._currentBo = nextBo;
+ }
+ _parent._eglDisplay.ClearContext();
+ }
+
+
+ public IGlDisplay Display => _parent._eglDisplay;
+
+ public PixelSize Size => _parent._mode.Resolution;
+
+ public double Scaling => _parent.Scaling;
+ }
+
+ public IGlPlatformSurfaceRenderingSession BeginDraw()
+ {
+ _parent._deferredContext.MakeCurrent(_parent._eglSurface);
+ return new RenderSession(_parent);
+ }
+
+
+ }
+
+ IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext;
+ }
+
+
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
similarity index 90%
rename from src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs
rename to src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
index 1e25bd4a8a..b83fe6cbe8 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
@@ -2,22 +2,22 @@
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Platform;
namespace Avalonia.LinuxFramebuffer
{
- public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable
+ 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 LinuxFramebuffer(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)
@@ -85,14 +85,14 @@ namespace Avalonia.LinuxFramebuffer
public string Id { get; private set; }
- public Size PixelSize
+ public PixelSize PixelSize
{
get
{
fb_var_screeninfo nfo;
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, &nfo))
throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error());
- return new Size(nfo.xres, nfo.yres);
+ return new PixelSize((int)nfo.xres, (int)nfo.yres);
}
}
@@ -100,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);
}
@@ -123,7 +123,7 @@ namespace Avalonia.LinuxFramebuffer
GC.SuppressFinalize(this);
}
- ~LinuxFramebuffer()
+ ~FbdevOutput()
{
ReleaseUnmanagedResources();
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs
new file mode 100644
index 0000000000..17a39b0219
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs
@@ -0,0 +1,8 @@
+namespace Avalonia.LinuxFramebuffer.Output
+{
+ public interface IOutputBackend
+ {
+ PixelSize PixelSize { get; }
+ double Scaling { get; set; }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
index 1c2fa17643..83d70122b3 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
@@ -149,11 +149,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
[Obsolete("Don't use", true)]
public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider(null);
- [DebuggerStepThrough]
+ // Don't emit debug symbols for this code so debugger will be forced to step into XAML instead
+ #line hidden
public static IServiceProvider CreateRootServiceProviderV2()
{
return new RootServiceProvider(new NameScope());
}
+ #line default
class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider
{
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 262d87d8b6..47e651ce91 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -154,7 +154,7 @@ namespace Avalonia.Skia
}
///
- public void DrawLine(Pen pen, Point p1, Point p2)
+ public void DrawLine(IPen pen, Point p1, Point p2)
{
using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
@@ -163,7 +163,7 @@ namespace Avalonia.Skia
}
///
- public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
@@ -184,7 +184,7 @@ namespace Avalonia.Skia
}
///
- public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+ public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0)
{
using (var paint = CreatePaint(pen, rect.Size))
{
@@ -561,7 +561,7 @@ namespace Avalonia.Skia
/// Source pen.
/// Target size.
///
- private PaintWrapper CreatePaint(Pen pen, Size targetSize)
+ private PaintWrapper CreatePaint(IPen pen, Size targetSize)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs
index 5940de418e..23980fb913 100644
--- a/src/Skia/Avalonia.Skia/GeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs
@@ -26,7 +26,7 @@ namespace Avalonia.Skia
}
///
- public bool StrokeContains(Pen pen, Point point)
+ public bool StrokeContains(IPen pen, Point point)
{
// Skia requires to compute stroke path to check for point containment.
// Due to that we are caching using stroke width.
@@ -89,7 +89,7 @@ namespace Avalonia.Skia
}
///
- public Rect GetRenderBounds(Pen pen)
+ public Rect GetRenderBounds(IPen pen)
{
var strokeWidth = (float)(pen?.Thickness ?? 0);
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.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index e90d444c44..39d801eb2f 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -174,7 +174,7 @@ namespace Avalonia.Direct2D1.Media
/// The stroke pen.
/// The first point of the line.
/// The second point of the line.
- public void DrawLine(Pen pen, Point p1, Point p2)
+ public void DrawLine(IPen pen, Point p1, Point p2)
{
if (pen != null)
{
@@ -202,7 +202,7 @@ namespace Avalonia.Direct2D1.Media
/// The fill brush.
/// The stroke pen.
/// The geometry.
- public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
if (brush != null)
{
@@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media
/// The pen.
/// The rectangle bounds.
/// The corner radius.
- public void DrawRectangle(Pen pen, Rect rect, float cornerRadius)
+ public void DrawRectangle(IPen pen, Rect rect, float cornerRadius)
{
using (var brush = CreateBrush(pen.Brush, rect.Size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
index 7c8ddaca3f..51ca2520ad 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media
public Geometry Geometry { get; }
///
- public Rect GetRenderBounds(Avalonia.Media.Pen pen)
+ public Rect GetRenderBounds(Avalonia.Media.IPen pen)
{
return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia();
}
@@ -46,7 +46,7 @@ namespace Avalonia.Direct2D1.Media
}
///
- public bool StrokeContains(Avalonia.Media.Pen pen, Point point)
+ public bool StrokeContains(Avalonia.Media.IPen pen, Point point)
{
return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0));
}
diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
index 6b0d30f250..065895859d 100644
--- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
+++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
@@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1
/// The pen to convert.
/// The render target.
/// The Direct2D brush.
- public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget)
+ public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, SharpDX.Direct2D1.RenderTarget renderTarget)
{
return pen.ToDirect2DStrokeStyle(renderTarget.Factory);
}
@@ -120,7 +120,7 @@ namespace Avalonia.Direct2D1
/// The pen to convert.
/// The factory associated with this resource.
/// The Direct2D brush.
- public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory)
+ public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, Factory factory)
{
var d2dLineCap = pen.LineCap.ToDirect2D();
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/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs
index f1fd74f931..b45138c27a 100644
--- a/src/Windows/Avalonia.Win32/CursorFactory.cs
+++ b/src/Windows/Avalonia.Win32/CursorFactory.cs
@@ -56,7 +56,7 @@ namespace Avalonia.Win32
{StandardCursorType.Wait, 32514},
//Same as SizeNorthSouth
{StandardCursorType.TopSide, 32645},
- {StandardCursorType.BottomSize, 32645},
+ {StandardCursorType.BottomSide, 32645},
//Same as SizeWestEast
{StandardCursorType.LeftSide, 32644},
{StandardCursorType.RightSide, 32644},
diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs
index a1bc5023a5..a8d74571a1 100644
--- a/src/Windows/Avalonia.Win32/DragSource.cs
+++ b/src/Windows/Avalonia.Win32/DragSource.cs
@@ -8,10 +8,11 @@ namespace Avalonia.Win32
{
class DragSource : IPlatformDragSource
{
- public Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+ public Task DoDragDrop(PointerEventArgs triggerEvent,
+ IDataObject data, DragDropEffects allowedEffects)
{
Dispatcher.UIThread.VerifyAccess();
-
+ triggerEvent.Pointer.Capture(null);
OleDragSource src = new OleDragSource();
DataObject dataObject = new DataObject(data);
int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects);
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/RenderTimer.cs b/src/Windows/Avalonia.Win32/RenderTimer.cs
deleted file mode 100644
index 7dbb745a23..0000000000
--- a/src/Windows/Avalonia.Win32/RenderTimer.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System;
-using System.Reactive.Disposables;
-using System.Threading;
-using Avalonia.Rendering;
-using Avalonia.Win32.Interop;
-
-namespace Avalonia.Win32
-{
- internal class RenderTimer : DefaultRenderTimer
- {
- private UnmanagedMethods.WaitOrTimerCallback timerDelegate;
-
- private static IntPtr _timerQueue;
-
- private static void EnsureTimerQueueCreated()
- {
- if (Volatile.Read(ref _timerQueue) == null)
- {
- var queue = UnmanagedMethods.CreateTimerQueue();
- if (Interlocked.CompareExchange(ref _timerQueue, queue, IntPtr.Zero) != IntPtr.Zero)
- {
- UnmanagedMethods.DeleteTimerQueueEx(queue, IntPtr.Zero);
- }
- }
- }
-
- public RenderTimer(int framesPerSecond)
- : base(framesPerSecond)
- {
- }
-
- protected override IDisposable StartCore(Action tick)
- {
- EnsureTimerQueueCreated();
- var msPerFrame = 1000 / FramesPerSecond;
-
- timerDelegate = (_, __) => tick(TimeSpan.FromMilliseconds(Environment.TickCount));
-
- UnmanagedMethods.CreateTimerQueueTimer(
- out var timer,
- _timerQueue,
- timerDelegate,
- IntPtr.Zero,
- (uint)msPerFrame,
- (uint)msPerFrame,
- 0
- );
-
- return Disposable.Create(() =>
- {
- timerDelegate = null;
- UnmanagedMethods.DeleteTimerQueueTimer(_timerQueue, timer, IntPtr.Zero);
- });
- }
- }
-}
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index c45bf6389e..bc40ec2ff7 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(
@@ -84,7 +86,7 @@ namespace Avalonia.Win32
.Bind().ToConstant(s_instance)
.Bind().ToConstant(s_instance)
.Bind().ToConstant(new RenderLoop())
- .Bind().ToConstant(new RenderTimer(60))
+ .Bind().ToConstant(new DefaultRenderTimer(60))
.Bind().ToSingleton()
.Bind().ToConstant(s_instance)
.Bind().ToSingleton()
@@ -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.Base.UnitTests/Data/DefaultValueConverterTests.cs b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
index eeb502d730..ecf559951a 100644
--- a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
@@ -8,6 +8,7 @@ using Xunit;
using System.Windows.Input;
using System;
using Avalonia.Data.Converters;
+using Avalonia.Layout;
namespace Avalonia.Base.UnitTests.Data.Converters
{
diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
index 2f1b7862a7..63e1790cce 100644
--- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
+++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
@@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests
private static Mock GetMockOwner()
{
var owner = new Mock();
- owner.SetupGet(o => o.Setter).Returns(new DeferredSetter());
+ owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny())).Returns(new DeferredSetter());
return owner;
}
}
diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
index 6550a23b7b..f503bf66a7 100644
--- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
+++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
new file mode 100644
index 0000000000..02f9397e2c
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
@@ -0,0 +1,65 @@
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class DirectPropertyBenchmark
+ {
+ [Benchmark(Baseline = true)]
+ public void SetAndRaiseOriginal()
+ {
+ var obj = new DirectClass();
+
+ for (var i = 0; i < 100; ++i)
+ {
+ obj.IntValue += 1;
+ }
+ }
+
+ [Benchmark]
+ public void SetAndRaiseSimple()
+ {
+ var obj = new DirectClass();
+
+ for (var i = 0; i < 100; ++i)
+ {
+ obj.IntValueSimple += 1;
+ }
+ }
+
+ class DirectClass : AvaloniaObject
+ {
+ private int _intValue;
+
+ public static readonly DirectProperty IntValueProperty =
+ AvaloniaProperty.RegisterDirect(nameof(IntValue),
+ o => o.IntValue,
+ (o, v) => o.IntValue = v);
+
+ public int IntValue
+ {
+ get => _intValue;
+ set => SetAndRaise(IntValueProperty, ref _intValue, value);
+ }
+
+ public int IntValueSimple
+ {
+ get => _intValue;
+ set
+ {
+ VerifyAccess();
+
+ if (_intValue == value)
+ {
+ return;
+ }
+
+ var old = _intValue;
+ _intValue = value;
+
+ RaisePropertyChanged(IntValueProperty, old, _intValue);
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/Properties.cs b/tests/Avalonia.Benchmarks/Base/Properties.cs
index 0a020961d5..45fc68ac96 100644
--- a/tests/Avalonia.Benchmarks/Base/Properties.cs
+++ b/tests/Avalonia.Benchmarks/Base/Properties.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Reactive.Subjects;
+using System.Reactive.Subjects;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base
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/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs
index df804d5d8c..2b9197e20b 100644
--- a/tests/Avalonia.Controls.UnitTests/GridTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs
@@ -1357,5 +1357,36 @@ namespace Avalonia.Controls.UnitTests
PrintColumnDefinitions(grid);
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth));
}
+
+ [Fact]
+ public void Correct_Grid_Bounds_When_Child_Control_Has_DesiredSize_Larger_Than_Available_Space()
+ {
+ // Issue #2746
+ var grid = new Grid
+ {
+ RowDefinitions = RowDefinitions.Parse("Auto"),
+ Children =
+ {
+ new TestControl
+ {
+ MeasureSize = new Size(150, 150),
+ }
+ }
+ };
+
+ var parent = new Decorator { Child = grid };
+
+ parent.Measure(new Size(100, 100));
+ parent.Arrange(new Rect(grid.DesiredSize));
+
+ Assert.Equal(new Size(100, 100), grid.Bounds.Size);
+ }
+
+ private class TestControl : Control
+ {
+ public Size MeasureSize { get; set; }
+
+ protected override Size MeasureOverride(Size availableSize) => MeasureSize;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
index 2a61ff1566..27ddd95d20 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
+++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
@@ -9,8 +9,8 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
-using Avalonia.Markup.Data;
using Avalonia.Styling;
+using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
index 7d05547799..6ab9c345d4 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
@@ -1,7 +1,11 @@
// 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.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
+using System.Reactive.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
@@ -256,7 +260,6 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.IsType
diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs
deleted file mode 100644
index 69bdf58f9d..0000000000
--- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs
+++ /dev/null
@@ -1,112 +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 System.Collections.Generic;
-using Avalonia.Controls;
-using Avalonia.Controls.UnitTests;
-using Avalonia.Input;
-using Xunit;
-
-namespace Avalonia.Interactivity.UnitTests
-{
- public class GestureTests
- {
- private MouseTestHelper _mouse = new MouseTestHelper();
-
- [Fact]
- public void Tapped_Should_Follow_Pointer_Pressed_Released()
- {
- Border border = new Border();
- var decorator = new Decorator
- {
- Child = border
- };
- var result = new List();
-
- decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp"));
- decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr"));
- decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt"));
- border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp"));
- border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br"));
- border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
-
- _mouse.Click(border);
-
- Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result);
- }
-
- [Fact]
- public void Tapped_Should_Be_Raised_Even_When_PointerPressed_Handled()
- {
- Border border = new Border();
- var decorator = new Decorator
- {
- Child = border
- };
- var result = new List();
-
- border.AddHandler(Border.PointerPressedEvent, (s, e) => e.Handled = true);
- decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt"));
- border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
-
- _mouse.Click(border);
-
- Assert.Equal(new[] { "bt", "dt" }, result);
- }
-
- [Fact]
- public void DoubleTapped_Should_Follow_Pointer_Pressed_Released_Pressed()
- {
- Border border = new Border();
- var decorator = new Decorator
- {
- Child = border
- };
- var result = new List();
-
- decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp"));
- decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr"));
- decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt"));
- decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt"));
- border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp"));
- border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br"));
- border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
- border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt"));
-
- _mouse.Click(border);
- _mouse.Down(border, clickCount: 2);
-
- Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result);
- }
-
- [Fact]
- public void DoubleTapped_Should_Not_Be_Rasied_if_Pressed_is_Handled()
- {
- Border border = new Border();
- var decorator = new Decorator
- {
- Child = border
- };
- var result = new List();
-
- decorator.AddHandler(Border.PointerPressedEvent, (s, e) =>
- {
- result.Add("dp");
- e.Handled = true;
- });
-
- decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr"));
- decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt"));
- decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt"));
- border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp"));
- border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br"));
- border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
- border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt"));
-
- _mouse.Click(border);
- _mouse.Down(border, clickCount: 2);
-
- Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp" }, result);
- }
- }
-}
diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs
index a841174d2d..1da4746516 100644
--- a/tests/Avalonia.LeakTests/ControlTests.cs
+++ b/tests/Avalonia.LeakTests/ControlTests.cs
@@ -401,6 +401,10 @@ namespace Avalonia.LeakTests
{
}
+ public void RecalculateChildren(IVisual visual)
+ {
+ }
+
public void Resized(Size size)
{
}
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 abe6fa84b0..cdd40ed80f 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
@@ -1,4 +1,5 @@
using Avalonia.Controls;
+using Avalonia.Layout;
using Avalonia.UnitTests;
using Xunit;
@@ -10,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 f065fcb63d..272b1fc489 100644
--- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
+++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
@@ -1,9 +1,11 @@
netstandard2.0
+ latest
false
Library
false
+ latest
diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
index 63da9ed3f0..4fa3fbf523 100644
--- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
+++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
@@ -47,12 +47,12 @@ namespace Avalonia.UnitTests
return _context.FillContains(point);
}
- public bool StrokeContains(Pen pen, Point point)
+ public bool StrokeContains(IPen pen, Point point)
{
return false;
}
- public Rect GetRenderBounds(Pen pen) => Bounds;
+ public Rect GetRenderBounds(IPen pen) => Bounds;
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
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.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs
similarity index 98%
rename from tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
rename to tests/Avalonia.UnitTests/MouseTestHelper.cs
index 373bbaed75..00ad850cf8 100644
--- a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
+++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs
@@ -1,9 +1,8 @@
-using System.Reactive;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
-namespace Avalonia.Controls.UnitTests
+namespace Avalonia.UnitTests
{
public class MouseTestHelper
{
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs
new file mode 100644
index 0000000000..418ac7576b
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs
@@ -0,0 +1,91 @@
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class PenTests
+ {
+ [Fact]
+ public void Changing_Thickness_Raises_Invalidated()
+ {
+ var target = new Pen();
+ var raised = false;
+
+ target.Invalidated += (s, e) => raised = true;
+ target.Thickness = 18;
+
+ Assert.True(raised);
+ }
+
+ [Fact]
+ public void Changing_Brush_Color_Raises_Invalidated()
+ {
+ var brush = new SolidColorBrush(Colors.Red);
+ var target = new Pen { Brush = brush };
+ var raised = false;
+
+ target.Invalidated += (s, e) => raised = true;
+ brush.Color = Colors.Green;
+
+ Assert.True(raised);
+ }
+
+ [Fact]
+ public void Changing_DashStyle_Dashes_Raises_Invalidated()
+ {
+ var dashes = new DashStyle();
+ var target = new Pen { DashStyle = dashes };
+ var raised = false;
+
+ target.Invalidated += (s, e) => raised = true;
+ dashes.Dashes = new[] { 0.1, 0.2 };
+
+ Assert.True(raised);
+ }
+
+ [Fact]
+ public void Equality_Is_Implemented_Between_Immutable_And_Mmutable_Pens()
+ {
+ var brush = new SolidColorBrush(Colors.Red);
+ var target1 = new ImmutablePen(
+ brush: brush,
+ thickness: 2,
+ dashStyle: (ImmutableDashStyle)DashStyle.Dash,
+ lineCap: PenLineCap.Round,
+ lineJoin: PenLineJoin.Round,
+ miterLimit: 21);
+ var target2 = new Pen(
+ brush: brush,
+ thickness: 2,
+ dashStyle: DashStyle.Dash,
+ lineCap: PenLineCap.Round,
+ lineJoin: PenLineJoin.Round,
+ miterLimit: 21);
+
+ Assert.True(Equals(target1, target2));
+ }
+
+ [Fact]
+ public void Equality_Is_Implemented_Between_Mutable_And_Immutable_DashStyles()
+ {
+ var brush = new SolidColorBrush(Colors.Red);
+ var target1 = new ImmutablePen(
+ brush: brush,
+ thickness: 2,
+ dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
+ lineCap: PenLineCap.Round,
+ lineJoin: PenLineJoin.Round,
+ miterLimit: 21);
+ var target2 = new Pen(
+ brush: brush,
+ thickness: 2,
+ dashStyle: new DashStyle(new[] { 0.1, 0.2 }, 5),
+ lineCap: PenLineCap.Round,
+ lineJoin: PenLineJoin.Round,
+ miterLimit: 21);
+
+ Assert.True(Equals(target1, target2));
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
index f094d9c78d..4c302a24a2 100644
--- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
@@ -96,6 +96,180 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Assert.Equal(new List { root, decorator, border, canvas }, result);
}
+ [Fact]
+ public void Should_Update_VisualNode_Order_On_Child_Remove_Insert()
+ {
+ var dispatcher = new ImmediateDispatcher();
+ var loop = new Mock();
+
+ StackPanel stack;
+ Canvas canvas1;
+ Canvas canvas2;
+ var root = new TestRoot
+ {
+ Child = stack = new StackPanel
+ {
+ Children=
+ {
+ (canvas1 = new Canvas()),
+ (canvas2 = new Canvas()),
+ }
+ }
+ };
+
+ var sceneBuilder = new SceneBuilder();
+ var target = new DeferredRenderer(
+ root,
+ loop.Object,
+ sceneBuilder: sceneBuilder,
+ dispatcher: dispatcher);
+
+ root.Renderer = target;
+ target.Start();
+ RunFrame(target);
+
+ stack.Children.Remove(canvas2);
+ stack.Children.Insert(0, canvas2);
+
+ RunFrame(target);
+
+ var scene = target.UnitTestScene();
+ var stackNode = scene.FindNode(stack);
+
+ Assert.Same(stackNode.Children[0].Visual, canvas2);
+ Assert.Same(stackNode.Children[1].Visual, canvas1);
+ }
+
+ [Fact]
+ public void Should_Update_VisualNode_Order_On_Child_Move()
+ {
+ var dispatcher = new ImmediateDispatcher();
+ var loop = new Mock();
+
+ StackPanel stack;
+ Canvas canvas1;
+ Canvas canvas2;
+ var root = new TestRoot
+ {
+ Child = stack = new StackPanel
+ {
+ Children =
+ {
+ (canvas1 = new Canvas()),
+ (canvas2 = new Canvas()),
+ }
+ }
+ };
+
+ var sceneBuilder = new SceneBuilder();
+ var target = new DeferredRenderer(
+ root,
+ loop.Object,
+ sceneBuilder: sceneBuilder,
+ dispatcher: dispatcher);
+
+ root.Renderer = target;
+ target.Start();
+ RunFrame(target);
+
+ stack.Children.Move(1, 0);
+
+ RunFrame(target);
+
+ var scene = target.UnitTestScene();
+ var stackNode = scene.FindNode(stack);
+
+ Assert.Same(stackNode.Children[0].Visual, canvas2);
+ Assert.Same(stackNode.Children[1].Visual, canvas1);
+ }
+
+ [Fact]
+ public void Should_Update_VisualNode_Order_On_ZIndex_Change()
+ {
+ var dispatcher = new ImmediateDispatcher();
+ var loop = new Mock();
+
+ StackPanel stack;
+ Canvas canvas1;
+ Canvas canvas2;
+ var root = new TestRoot
+ {
+ Child = stack = new StackPanel
+ {
+ Children =
+ {
+ (canvas1 = new Canvas { ZIndex = 1 }),
+ (canvas2 = new Canvas { ZIndex = 2 }),
+ }
+ }
+ };
+
+ var sceneBuilder = new SceneBuilder();
+ var target = new DeferredRenderer(
+ root,
+ loop.Object,
+ sceneBuilder: sceneBuilder,
+ dispatcher: dispatcher);
+
+ root.Renderer = target;
+ target.Start();
+ RunFrame(target);
+
+ canvas1.ZIndex = 3;
+
+ RunFrame(target);
+
+ var scene = target.UnitTestScene();
+ var stackNode = scene.FindNode(stack);
+
+ Assert.Same(stackNode.Children[0].Visual, canvas2);
+ Assert.Same(stackNode.Children[1].Visual, canvas1);
+ }
+
+ [Fact]
+ public void Should_Update_VisualNode_Order_On_ZIndex_Change_With_Dirty_Ancestor()
+ {
+ var dispatcher = new ImmediateDispatcher();
+ var loop = new Mock();
+
+ StackPanel stack;
+ Canvas canvas1;
+ Canvas canvas2;
+ var root = new TestRoot
+ {
+ Child = stack = new StackPanel
+ {
+ Children =
+ {
+ (canvas1 = new Canvas { ZIndex = 1 }),
+ (canvas2 = new Canvas { ZIndex = 2 }),
+ }
+ }
+ };
+
+ var sceneBuilder = new SceneBuilder();
+ var target = new DeferredRenderer(
+ root,
+ loop.Object,
+ sceneBuilder: sceneBuilder,
+ dispatcher: dispatcher);
+
+ root.Renderer = target;
+ target.Start();
+ RunFrame(target);
+
+ root.InvalidateVisual();
+ canvas1.ZIndex = 3;
+
+ RunFrame(target);
+
+ var scene = target.UnitTestScene();
+ var stackNode = scene.FindNode(stack);
+
+ Assert.Same(stackNode.Children[0].Visual, canvas2);
+ Assert.Same(stackNode.Children[1].Visual, canvas1);
+ }
+
[Fact]
public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity()
{
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);
+ }
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs
index 504f0ada86..936a5d16a2 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs
@@ -282,5 +282,52 @@ namespace Avalonia.Visuals.UnitTests
Assert.True(called);
}
+
+ [Fact]
+ public void Changing_ZIndex_Should_InvalidateVisual()
+ {
+ Canvas canvas1;
+ var renderer = new Mock();
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children =
+ {
+ (canvas1 = new Canvas()),
+ new Canvas(),
+ },
+ },
+ };
+
+ root.Renderer = renderer.Object;
+ canvas1.ZIndex = 10;
+
+ renderer.Verify(x => x.AddDirty(canvas1));
+ }
+
+ [Fact]
+ public void Changing_ZIndex_Should_Recalculate_Parent_Children()
+ {
+ Canvas canvas1;
+ StackPanel stackPanel;
+ var renderer = new Mock();
+ var root = new TestRoot
+ {
+ Child = stackPanel = new StackPanel
+ {
+ Children =
+ {
+ (canvas1 = new Canvas()),
+ new Canvas(),
+ },
+ },
+ };
+
+ root.Renderer = renderer.Object;
+ canvas1.ZIndex = 10;
+
+ renderer.Verify(x => x.RecalculateChildren(stackPanel));
+ }
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
index 03470670d2..d31210bc71 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
@@ -96,7 +96,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
return _impl.FillContains(point);
}
- public Rect GetRenderBounds(Pen pen)
+ public Rect GetRenderBounds(IPen pen)
{
throw new NotImplementedException();
}
@@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
return _impl;
}
- public bool StrokeContains(Pen pen, Point point)
+ public bool StrokeContains(IPen pen, Point point)
{
throw new NotImplementedException();
}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs
index 867d4d7450..139a7925b1 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using Avalonia.Controls;
+using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;